Sfoglia il codice sorgente

functional (but ugly) UI

Chris Mullins 8 anni fa
parent
commit
f2d45546cb
5 ha cambiato i file con 334 aggiunte e 42 eliminazioni
  1. 2 2
      lib/MiLight/MiLightClient.cpp
  2. 1 1
      lib/WebServer/WebServer.cpp
  3. 1 2
      lib/WebServer/WebServer.h
  4. 50 37
      src/main.cpp
  5. 280 0
      web/index.html

+ 2 - 2
lib/MiLight/MiLightClient.cpp

@@ -61,8 +61,8 @@ void MiLightClient::write(
   const uint8_t groupId,
   const MiLightButton button) {
     
-  // Expect an input value in [0, 255]. Map it down to [0, 25].
-  const uint8_t adjustedBrightness = round(brightness * (25 / 255.0));
+  // Expect an input value in [0, 100]. Map it down to [0, 25].
+  const uint8_t adjustedBrightness = round(brightness * (25 / 100.0));
   
   // The actual protocol uses a bizarre range where min is 16, max is 23:
   // [16, 15, ..., 0, 31, ..., 23]

+ 1 - 1
lib/WebServer/WebServer.cpp

@@ -101,4 +101,4 @@ void WebServer::checkPatterns() {
 
 void WebServer::onPattern(const String& pattern, const HTTPMethod method, const THandlerFunction fn) {
   handlers.push_back(new PatternHandler(pattern, method, fn));
-}
+}

+ 1 - 2
lib/WebServer/WebServer.h

@@ -31,7 +31,6 @@ public:
   }
   
   bool matchesPattern(const String& pattern, const String& url);
-  
   void onPattern(const String& pattern, const HTTPMethod method, const THandlerFunction fn);
   
   String arg(String key) {
@@ -54,7 +53,7 @@ private:
   
   void resetPathMatches();
   void checkPatterns();
-
+  
   DynamicJsonBuffer* buffer;
   JsonObject* pathMatches;
   Vector<PatternHandler*> handlers;

+ 50 - 37
src/main.cpp

@@ -30,13 +30,13 @@ void handleUpdateGateway() {
   DynamicJsonBuffer buffer;
   JsonObject& request = buffer.parse(server.arg("plain"));
   
-  const uint16_t gatewayId = parseInt<uint16_t>(server.arg("gateway_id"));
+  const uint16_t deviceId = parseInt<uint16_t>(server.arg("device_id"));
   
   if (request.containsKey("status")) {
     if (request["status"] == "on") {
-      milightClient.allOn(gatewayId);
+      milightClient.allOn(deviceId);
     } else if (request["status"] == "off") {
-      milightClient.allOff(gatewayId);
+      milightClient.allOff(deviceId);
     }
   }
   
@@ -47,41 +47,42 @@ void handleUpdateGroup() {
   DynamicJsonBuffer buffer;
   JsonObject& request = buffer.parse(server.arg("plain"));
   
-  const uint16_t gatewayId = parseInt<uint16_t>(server.arg("gateway_id"));
+  const uint16_t deviceId = parseInt<uint16_t>(server.arg("device_id"));
   const uint8_t groupId = server.arg("group_id").toInt();
   
   if (request.containsKey("status")) {
-    MiLightStatus status = (request.get<String>("status") == "on") ? ON : OFF;
-    milightClient.updateStatus(gatewayId, groupId, status);
+    const String& statusStr = request.get<String>("status");
+    MiLightStatus status = (statusStr == "on" || statusStr == "true") ? ON : OFF;
+    milightClient.updateStatus(deviceId, groupId, status);
   }
   
   if (request.containsKey("hue")) {
-    milightClient.updateColor(gatewayId, groupId, request["hue"]);
+    milightClient.updateColor(deviceId, groupId, request["hue"]);
   }
   
   if (request.containsKey("level")) {
-    milightClient.updateBrightness(gatewayId, groupId, request["level"]);
+    milightClient.updateBrightness(deviceId, groupId, request["level"]);
   }
   
   if (request.containsKey("command")) {
     if (request["command"] == "set_white") {
-      milightClient.updateColorWhite(gatewayId, groupId);
+      milightClient.updateColorWhite(deviceId, groupId);
     }
     
     if (request["command"] == "all_on") {
-      milightClient.allOn(gatewayId);
+      milightClient.allOn(deviceId);
     }
     
     if (request["command"] == "all_off") {
-      milightClient.allOff(gatewayId);
+      milightClient.allOff(deviceId);
     }
     
     if (request["command"] == "unpair") {
-      milightClient.unpair(gatewayId, groupId);
+      milightClient.unpair(deviceId, groupId);
     }
     
     if (request["command"] == "pair") {
-      milightClient.pair(gatewayId, groupId);
+      milightClient.pair(deviceId, groupId);
     }
   }
   
@@ -115,39 +116,51 @@ void handleListenGateway() {
   server.send(200, "text/plain", response);
 }
 
-void serveFile(const String& file, const char* contentType = "text/html") {
+bool serveFile(const char* file, const char* contentType = "text/html") {
   if (SPIFFS.exists(file)) {
     File f = SPIFFS.open(file, "r");
-    server.send(200, "text/html", f.readString());
+    server.send(200, contentType, f.readString());
     f.close();
-  } else {
-    server.send(404);
+    return true;
   }
+  
+  return false;
 }
 
-void handleIndex() {
-  serveFile(WEB_INDEX_FILENAME);
+ESP8266WebServer::THandlerFunction handleServeFile(const char* filename, 
+  const char* contentType, 
+  const char* defaultText = NULL) {
+    
+  return [filename, contentType, defaultText]() {
+    if (!serveFile(filename)) {
+      if (defaultText) {
+        server.send(200, contentType, defaultText);
+      } else {
+        server.send(404);
+      }
+    }
+  };
 }
 
-void handleWebUpdate() {
-  HTTPUpload& upload = server.upload();
-  
-  if (upload.status == UPLOAD_FILE_START) {
-    updateFile = SPIFFS.open(WEB_INDEX_FILENAME, "w");
-  } else if(upload.status == UPLOAD_FILE_WRITE){
-    if (updateFile.write(upload.buf, upload.currentSize)) {
-      Serial.println("Error updating web file");
+ESP8266WebServer::THandlerFunction handleUpdateFile(const char* filename) {
+  return [filename]() {
+    HTTPUpload& upload = server.upload();
+    
+    if (upload.status == UPLOAD_FILE_START) {
+      updateFile = SPIFFS.open(filename, "w");
+    } else if(upload.status == UPLOAD_FILE_WRITE){
+      if (updateFile.write(upload.buf, upload.currentSize)) {
+        Serial.println("Error updating web file");
+      }
+    } else if (upload.status == UPLOAD_FILE_END) {
+      updateFile.close();
     }
-  } else if (upload.status == UPLOAD_FILE_END) {
-    updateFile.close();
-  }
-  
+  };
   yield();
 }
 
 void onWebUpdated() {
-  server.sendHeader("Location", "/");
-  server.send(302);
+  server.send(200, "text/plain", "success");
 }
 
 void setup() {
@@ -156,11 +169,11 @@ void setup() {
   mlr.begin();
   SPIFFS.begin();
   
-  server.on("/", HTTP_GET, handleIndex);
+  server.on("/", HTTP_GET, handleServeFile(WEB_INDEX_FILENAME, "text/html"));
   server.on("/gateway_traffic", HTTP_GET, handleListenGateway);
-  server.onPattern("/gateway/:gateway_id/:group_id", HTTP_PUT, handleUpdateGroup);
-  server.onPattern("/gateway/:gateway_id", HTTP_PUT, handleUpdateGateway);
-  server.on("/web", HTTP_POST, onWebUpdated, handleWebUpdate);
+  server.onPattern("/gateways/:device_id/:group_id", HTTP_PUT, handleUpdateGroup);
+  server.onPattern("/gateways/:device_id", HTTP_PUT, handleUpdateGateway);
+  server.on("/web", HTTP_POST, onWebUpdated, handleUpdateFile(WEB_INDEX_FILENAME));
   server.on("/firmware", HTTP_POST, 
     [](){
       server.sendHeader("Connection", "close");

+ 280 - 0
web/index.html

@@ -0,0 +1,280 @@
+<!doctype html>
+
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+
+  <title>MiLight Hub</title>
+  <meta name="description" content="The HTML5 Herald">
+  <meta name="author" content="SitePoint">
+
+  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"/>
+  <link href="https://gitcdn.github.io/bootstrap-toggle/2.2.2/css/bootstrap-toggle.min.css" rel="stylesheet"/>
+  <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-slider/9.7.0/css/bootstrap-slider.min.css" rel="stylesheet"/>
+
+  <!--[if lt IE 9]>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.js"></script>
+  <![endif]-->
+  
+  <style>
+    .header-row { border-bottom: 1px solid #ccc; }
+    label { display: block; }
+    .radio-option { padding: 0 5px; cursor: pointer; }
+    .command-buttons { list-style: none; margin: 0; padding: 0; }
+    .command-buttons li { display: inline-block; margin-right: 1em; }
+    .btn-secondary { 
+      background-color: #fff; 
+      border: 1px solid #ccc;
+    }
+    .hue-picker {
+      height: 2em;
+      width: 100%;
+      display: block;
+    }
+    .hue-picker-inner {
+      height: 2em;
+      width: calc(100% - 3em);
+      display: inline-block;
+      cursor: pointer;
+      background: linear-gradient(to right, 
+        rgb(255, 0, 0) 0%, 
+        rgb(255, 255, 0) 16.6667%, 
+        rgb(0, 255, 0) 33.3333%, 
+        rgb(0, 255, 255) 50%, 
+        rgb(0, 0, 255) 66.6667%, 
+        rgb(255, 0, 255) 83.3333%, 
+        rgb(255, 0, 0) 100%
+      );
+    }
+    .hue-value-display { 
+      border: 1px solid #000;
+      margin-left: 0.5em;
+      width: 2em;
+      height: 2em;
+      display: inline-block;
+    }
+  </style>
+</head>
+
+<body>
+  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
+  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
+  <script src="https://gitcdn.github.io/bootstrap-toggle/2.2.2/js/bootstrap-toggle.min.js"></script>
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-slider/9.7.0/bootstrap-slider.min.js"></script>
+  
+  <script lang="text/javascript">
+    var activeUrl = function() {
+      return "/gateways/" +
+        $('input[name="deviceId"]').val() +
+        "/" +
+        $('#groupId input:checked').data('value');
+    }
+    
+    var updateGroup = _.throttle(
+      function(params) {
+        $.ajax(
+          activeUrl(),
+          {
+            method: 'PUT',
+            data: JSON.stringify(params),
+            contentType: 'application/json'
+          }
+        );
+      },
+      1000
+    );
+    
+    var sniffRequest;
+    var sniffing = false;
+    var getTraffic = function() {
+      sniffRequest = $.get('/gateway_traffic', function(data) {
+        $('#sniffed-traffic').html(data + $('#sniffed-traffic').html());
+        getTraffic();
+      });
+    };
+    
+    
+    $(function() {
+      $('.radio-option').click(function() {
+        $(this).prev().prop('checked', true);
+      });
+      
+      var hueDragging = false;
+      var colorUpdated = function(e) {
+        var x = e.pageX - $(this).offset().left
+          , pct = x/(1.0*$(this).width())
+          , hue = Math.round(360*pct)
+          ;
+          
+        $('.hue-value-display').css({
+          backgroundColor: "hsl(" + hue + ",100%,50%)"
+        });
+        
+        updateGroup({hue: hue});
+      };
+      
+      $('.hue-picker-inner')
+        .mousedown(function(e) {
+          hueDragging = true;
+          colorUpdated.call(this, e);
+        })
+        .mouseup(function(e) {
+          hueDragging = false;
+        })
+        .mouseout(function(e) {
+          hueDragging = false;
+        })
+        .mousemove(function(e) {
+          if (hueDragging) {
+            colorUpdated.call(this, e);
+          }
+        });
+        
+      $('.slider').slider();
+      
+      $('.raw-update').change(function() {
+        var data = {}
+          , val = $(this).attr('type') == 'checkbox' ? $(this).is(':checked') : $(this).val()
+          ;
+          
+        data[$(this).attr('name')] = val;
+        updateGroup(data);
+      });
+      
+      $('.command-btn').click(function() {
+        updateGroup({command: $(this).data('command')});
+      });
+      
+      $('#sniff').click(function() {
+        if (sniffing) {
+          sniffRequest.abort();
+          sniffing = false;
+          $(this).html('Start Sniffing');
+        } else {
+          sniffing = true;
+          getTraffic();
+          $(this).html('Stop Sniffing');
+        }
+      });
+    });
+  </script>
+  
+  <div class="container">
+    <div class="row header-row">
+      <div class="col-sm-12">
+        <h1>
+          Control Lights
+        </h1>
+      </div>
+    </div>
+    
+    <div>&nbsp;</div>
+    
+    <div class="row">
+      <div class="col-sm-4">
+        <label for="deviceId">Device Id</label>
+        <input type="text" class="input" name="deviceId" />
+      </div>
+      
+      <div class="col-sm-8">
+        <label for="groupId">Group</label>
+        
+        <div class="btn-group" id="groupId" data-toggle="buttons">
+          <label class="btn btn-secondary active">
+            <input type="radio" name="options" autocomplete="off" data-value="1" checked> 1
+          </label>
+          <label class="btn btn-secondary">
+            <input type="radio" name="options" autocomplete="off" data-value="2"> 2
+          </label>
+          <label class="btn btn-secondary">
+            <input type="radio" name="options" autocomplete="off" data-value="3"> 3
+          </label>
+          <label class="btn btn-secondary">
+            <input type="radio" name="options" autocomplete="off" data-value="4"> 4
+          </label>
+        </div>
+      </div>
+    </div>
+    
+    <div class="row">
+      <div class="col-sm-12">
+        <h5>Hue</h5>
+      </div>
+    </div>
+    
+    <div class="row">
+      <div class="col-sm-6">
+        <span class="hue-picker">
+          <span class="hue-picker-inner"></span>
+          <span class="hue-value-display"></span>
+        </span>
+      </div>
+    </div>
+    
+    <div class="row">
+      <div class="col-sm-6">
+        <h5>Brightness</h5>
+      </div>
+    </div>
+      
+    <div class="row">
+      <div class="col-sm-12">
+        <input class="slider raw-update" name="level"
+            data-slider-min="0"
+            data-slider-max="100"
+            data-slider-value="100"
+        />
+      </div>
+    </div>
+    
+    <div class="row">
+      <div class="col-sm-12">
+        <h5>Commands</h5>
+      </div>
+    </div>
+    
+    <div class="row">
+      <div class="col-sm-12">
+        <ul class="command-buttons">
+          <li>
+            <input type="checkbox" name="status" class="raw-update" data-toggle="toggle" checked/>
+          </li>
+          <li>
+            <button type="button" class="btn btn-secondary command-btn" data-command="set_white">White</button>
+          </li>
+          <li>
+            <button type="button" class="btn btn-success command-btn" data-command="pair">
+              <i class="glyphicon glyphicon-plus"></i>
+              Pair
+            </button>
+          </li>
+          <li>
+            <button type="button" class="btn btn-danger command-btn" data-command="unpair">
+              <i class="glyphicon glyphicon-remove"></i>
+              Unpair
+            </button>
+          </li>
+        </ul>
+      </div>
+    </div>
+    
+    
+    <div class="row header-row">
+      <div class="col-sm-12">
+        <h1>Sniff Traffic</h1>
+      </div>
+    </div>
+    
+    <div>&nbsp;</div>
+    
+    <div class="row">
+      <div class="col-sm-12">
+        <button type="button" id="sniff" class="btn btn-primary">Start Sniffing</button>
+        
+        <pre id="sniffed-traffic"></pre>
+      </div>
+    </div>
+  </div>
+</body>
+</html>