Prechádzať zdrojové kódy

Merge pull request #111 from sidoh/v1.5.0

v1.5.0
Chris Mullins 8 rokov pred
rodič
commit
52b88a397c
38 zmenil súbory, kde vykonal 1354 pridanie a 3115 odobranie
  1. 34 0
      .build_web.py
  2. 2 1
      .gitignore
  3. 6 0
      .travis.yml
  4. 2 4
      README.md
  5. 0 1067
      data/web/index.html
  6. 2 0
      dist/index.html.gz.h
  7. 0 145
      lib/ESP8266WebServer/examples/AdvancedWebServer/AdvancedWebServer.ino
  8. 0 238
      lib/ESP8266WebServer/examples/FSBrowser/FSBrowser.ino
  9. BIN
      lib/ESP8266WebServer/examples/FSBrowser/data/edit.htm.gz
  10. BIN
      lib/ESP8266WebServer/examples/FSBrowser/data/favicon.ico
  11. BIN
      lib/ESP8266WebServer/examples/FSBrowser/data/graphs.js.gz
  12. 0 97
      lib/ESP8266WebServer/examples/FSBrowser/data/index.htm
  13. 0 72
      lib/ESP8266WebServer/examples/HelloServer/HelloServer.ino
  14. 0 40
      lib/ESP8266WebServer/examples/HttpBasicAuth/HttpBasicAuth.ino
  15. 0 269
      lib/ESP8266WebServer/examples/SDWebServer/SDWebServer.ino
  16. 0 674
      lib/ESP8266WebServer/examples/SDWebServer/SdRoot/edit/index.htm
  17. 0 22
      lib/ESP8266WebServer/examples/SDWebServer/SdRoot/index.htm
  18. BIN
      lib/ESP8266WebServer/examples/SDWebServer/SdRoot/pins.png
  19. 0 126
      lib/ESP8266WebServer/examples/SimpleAuthentification/SimpleAuthentification.ino
  20. 0 71
      lib/ESP8266WebServer/examples/WebUpdate/WebUpdate.ino
  21. 0 114
      lib/GithubClient/GithubClient.cpp
  22. 0 40
      lib/GithubClient/GithubClient.h
  23. 22 7
      lib/Helpers/IntParsing.h
  24. 27 10
      lib/MiLight/MiLightClient.cpp
  25. 4 2
      lib/MiLight/MiLightClient.h
  26. 1 1
      lib/MiLight/RgbCctPacketFormatter.h
  27. 4 4
      lib/Udp/V6ComamndHandler.cpp
  28. 1 1
      lib/Udp/V6CommandHandler.h
  29. 11 11
      lib/Udp/V6MiLightUdpServer.h
  30. 75 90
      lib/WebServer/MiLightHttpServer.cpp
  31. 9 6
      lib/WebServer/MiLightHttpServer.h
  32. 8 2
      platformio.ini
  33. 1 1
      src/main.cpp
  34. 65 0
      web/gulpfile.js
  35. 23 0
      web/package.json
  36. 103 0
      web/src/css/style.css
  37. 440 0
      web/src/index.html
  38. 514 0
      web/src/js/script.js

+ 34 - 0
.build_web.py

@@ -0,0 +1,34 @@
+from shutil import copyfile
+from subprocess import check_output
+import sys
+import os
+import platform
+import subprocess
+
+Import("env")
+
+def is_tool(name):
+    cmd = "where" if platform.system() == "Windows" else "which"
+    try:
+        check_output([cmd, name])
+        return True
+    except:
+        return False;
+
+def pre_build(source, target, env):
+    if is_tool("npm"):
+        os.chdir("web")
+        print("Attempting to build webpage...")
+        try:
+            print check_output(["npm", "install"])
+            print check_output(["node_modules/.bin/gulp"])
+            copyfile("build/index.html.gz.h", "../dist/index.html.gz.h")
+
+        except Exception as e:
+            print "Encountered error building webpage: ", e
+            print "WARNING: Failed to build web package. Using pre-built page."
+            pass
+        finally:
+            os.chdir("..");
+
+env.Execute(pre_build)

+ 2 - 1
.gitignore

@@ -2,4 +2,5 @@
 .piolibdeps
 .clang_complete
 .gcc-flags.json
-/dist
+/web/node_modules
+/web/build

+ 6 - 0
.travis.yml

@@ -1,3 +1,4 @@
+dist: precise
 language: python
 python:
 - '2.7'
@@ -5,9 +6,14 @@ sudo: false
 cache:
   directories:
   - "~/.platformio"
+env:
+  - NODE_VERSION="6"
+before_install:
+  - nvm install $NODE_VERSION
 install:
 - pip install -U platformio
 - platformio lib install
+- cd web && npm install && cd ..
 script:
 - platformio run
 before_deploy:

+ 2 - 4
README.md

@@ -48,12 +48,11 @@ Connect SPI pins (CS, SCK, MOSI, MISO) to appropriate SPI pins on the ESP8266. W
 
 #### Setting up the ESP
 
-You'll need to flash the firmware and a SPIFFS image. It's really easy to do this with [PlatformIO](http://platformio.org/):
+The goal here is to flash your ESP with the firmware. It's really easy to do this with [PlatformIO](http://platformio.org/):
 
 ```
 export ESP_BOARD=nodemcuv2
 platformio run -e $ESP_BOARD --target upload
-platformio run -e $ESP_BOARD --target uploadfs
 ```
 
 Of course make sure to substitute `nodemcuv2` with the board that you're using.
@@ -76,7 +75,7 @@ Both mDNS and SSDP are supported.
 
 #### Use it!
 
-The HTTP endpoints (shown below) will be fully functional at this point. You should also be able to navigate to `http://<ip_of_esp>`. The UI should look like this:
+The HTTP endpoints (shown below) will be fully functional at this point. You should also be able to navigate to `http://<ip_of_esp>`, or `http://milight-hub.local` if your client supports mDNS. The UI should look like this:
 
 ![Web UI](http://imgur.com/XNNigvL.png)
 
@@ -86,7 +85,6 @@ The HTTP endpoints (shown below) will be fully functional at this point. You sho
 1. `GET /about`. Return information about current firmware version.
 1. `POST /system`. Post commands in the form `{"comamnd": <command>}`. Currently supports the commands: `restart`.
 1. `POST /firmware`. OTA firmware update.
-1. `POST /web`. Update web UI.
 1. `GET /settings`. Gets current settings as JSON.
 1. `PUT /settings`. Patches settings (e.g., doesn't overwrite keys that aren't present). Accepts a JSON blob in the body.
 1. `GET /radio_configs`. Get a list of supported radio configs (aka `device_type`s).

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 1067
data/web/index.html


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 2 - 0
dist/index.html.gz.h


+ 0 - 145
lib/ESP8266WebServer/examples/AdvancedWebServer/AdvancedWebServer.ino

@@ -1,145 +0,0 @@
-/*
- * Copyright (c) 2015, Majenko Technologies
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- *
- * * Redistributions of source code must retain the above copyright notice, this
- *   list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright notice, this
- *   list of conditions and the following disclaimer in the documentation and/or
- *   other materials provided with the distribution.
- *
- * * Neither the name of Majenko Technologies nor the names of its
- *   contributors may be used to endorse or promote products derived from
- *   this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-#include <ESP8266WiFi.h>
-#include <WiFiClient.h>
-#include <ESP8266WebServer.h>
-#include <ESP8266mDNS.h>
-
-const char *ssid = "YourSSIDHere";
-const char *password = "YourPSKHere";
-
-ESP8266WebServer server ( 80 );
-
-const int led = 13;
-
-void handleRoot() {
-	digitalWrite ( led, 1 );
-	char temp[400];
-	int sec = millis() / 1000;
-	int min = sec / 60;
-	int hr = min / 60;
-
-	snprintf ( temp, 400,
-
-"<html>\
-  <head>\
-    <meta http-equiv='refresh' content='5'/>\
-    <title>ESP8266 Demo</title>\
-    <style>\
-      body { background-color: #cccccc; font-family: Arial, Helvetica, Sans-Serif; Color: #000088; }\
-    </style>\
-  </head>\
-  <body>\
-    <h1>Hello from ESP8266!</h1>\
-    <p>Uptime: %02d:%02d:%02d</p>\
-    <img src=\"/test.svg\" />\
-  </body>\
-</html>",
-
-		hr, min % 60, sec % 60
-	);
-	server.send ( 200, "text/html", temp );
-	digitalWrite ( led, 0 );
-}
-
-void handleNotFound() {
-	digitalWrite ( led, 1 );
-	String message = "File Not Found\n\n";
-	message += "URI: ";
-	message += server.uri();
-	message += "\nMethod: ";
-	message += ( server.method() == HTTP_GET ) ? "GET" : "POST";
-	message += "\nArguments: ";
-	message += server.args();
-	message += "\n";
-
-	for ( uint8_t i = 0; i < server.args(); i++ ) {
-		message += " " + server.argName ( i ) + ": " + server.arg ( i ) + "\n";
-	}
-
-	server.send ( 404, "text/plain", message );
-	digitalWrite ( led, 0 );
-}
-
-void setup ( void ) {
-	pinMode ( led, OUTPUT );
-	digitalWrite ( led, 0 );
-	Serial.begin ( 115200 );
-	WiFi.begin ( ssid, password );
-	Serial.println ( "" );
-
-	// Wait for connection
-	while ( WiFi.status() != WL_CONNECTED ) {
-		delay ( 500 );
-		Serial.print ( "." );
-	}
-
-	Serial.println ( "" );
-	Serial.print ( "Connected to " );
-	Serial.println ( ssid );
-	Serial.print ( "IP address: " );
-	Serial.println ( WiFi.localIP() );
-
-	if ( MDNS.begin ( "esp8266" ) ) {
-		Serial.println ( "MDNS responder started" );
-	}
-
-	server.on ( "/", handleRoot );
-	server.on ( "/test.svg", drawGraph );
-	server.on ( "/inline", []() {
-		server.send ( 200, "text/plain", "this works as well" );
-	} );
-	server.onNotFound ( handleNotFound );
-	server.begin();
-	Serial.println ( "HTTP server started" );
-}
-
-void loop ( void ) {
-	server.handleClient();
-}
-
-void drawGraph() {
-	String out = "";
-	char temp[100];
-	out += "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" width=\"400\" height=\"150\">\n";
- 	out += "<rect width=\"400\" height=\"150\" fill=\"rgb(250, 230, 210)\" stroke-width=\"1\" stroke=\"rgb(0, 0, 0)\" />\n";
- 	out += "<g stroke=\"black\">\n";
- 	int y = rand() % 130;
- 	for (int x = 10; x < 390; x+= 10) {
- 		int y2 = rand() % 130;
- 		sprintf(temp, "<line x1=\"%d\" y1=\"%d\" x2=\"%d\" y2=\"%d\" stroke-width=\"1\" />\n", x, 140 - y, x + 10, 140 - y2);
- 		out += temp;
- 		y = y2;
- 	}
-	out += "</g>\n</svg>\n";
-
-	server.send ( 200, "image/svg+xml", out);
-}

+ 0 - 238
lib/ESP8266WebServer/examples/FSBrowser/FSBrowser.ino

@@ -1,238 +0,0 @@
-/* 
-  FSWebServer - Example WebServer with SPIFFS backend for esp8266
-  Copyright (c) 2015 Hristo Gochkov. All rights reserved.
-  This file is part of the ESP8266WebServer library for Arduino environment.
- 
-  This library is free software; you can redistribute it and/or
-  modify it under the terms of the GNU Lesser General Public
-  License as published by the Free Software Foundation; either
-  version 2.1 of the License, or (at your option) any later version.
-  This library is distributed in the hope that it will be useful,
-  but WITHOUT ANY WARRANTY; without even the implied warranty of
-  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-  Lesser General Public License for more details.
-  You should have received a copy of the GNU Lesser General Public
-  License along with this library; if not, write to the Free Software
-  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
-  
-  upload the contents of the data folder with MkSPIFFS Tool ("ESP8266 Sketch Data Upload" in Tools menu in Arduino IDE)
-  or you can upload the contents of a folder if you CD in that folder and run the following command:
-  for file in `ls -A1`; do curl -F "file=@$PWD/$file" esp8266fs.local/edit; done
-  
-  access the sample web page at http://esp8266fs.local
-  edit the page by going to http://esp8266fs.local/edit
-*/
-#include <ESP8266WiFi.h>
-#include <WiFiClient.h>
-#include <ESP8266WebServer.h>
-#include <ESP8266mDNS.h>
-#include <FS.h>
-
-#define DBG_OUTPUT_PORT Serial
-
-const char* ssid = "wifi-ssid";
-const char* password = "wifi-password";
-const char* host = "esp8266fs";
-
-ESP8266WebServer server(80);
-//holds the current upload
-File fsUploadFile;
-
-//format bytes
-String formatBytes(size_t bytes){
-  if (bytes < 1024){
-    return String(bytes)+"B";
-  } else if(bytes < (1024 * 1024)){
-    return String(bytes/1024.0)+"KB";
-  } else if(bytes < (1024 * 1024 * 1024)){
-    return String(bytes/1024.0/1024.0)+"MB";
-  } else {
-    return String(bytes/1024.0/1024.0/1024.0)+"GB";
-  }
-}
-
-String getContentType(String filename){
-  if(server.hasArg("download")) return "application/octet-stream";
-  else if(filename.endsWith(".htm")) return "text/html";
-  else if(filename.endsWith(".html")) return "text/html";
-  else if(filename.endsWith(".css")) return "text/css";
-  else if(filename.endsWith(".js")) return "application/javascript";
-  else if(filename.endsWith(".png")) return "image/png";
-  else if(filename.endsWith(".gif")) return "image/gif";
-  else if(filename.endsWith(".jpg")) return "image/jpeg";
-  else if(filename.endsWith(".ico")) return "image/x-icon";
-  else if(filename.endsWith(".xml")) return "text/xml";
-  else if(filename.endsWith(".pdf")) return "application/x-pdf";
-  else if(filename.endsWith(".zip")) return "application/x-zip";
-  else if(filename.endsWith(".gz")) return "application/x-gzip";
-  return "text/plain";
-}
-
-bool handleFileRead(String path){
-  DBG_OUTPUT_PORT.println("handleFileRead: " + path);
-  if(path.endsWith("/")) path += "index.htm";
-  String contentType = getContentType(path);
-  String pathWithGz = path + ".gz";
-  if(SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)){
-    if(SPIFFS.exists(pathWithGz))
-      path += ".gz";
-    File file = SPIFFS.open(path, "r");
-    size_t sent = server.streamFile(file, contentType);
-    file.close();
-    return true;
-  }
-  return false;
-}
-
-void handleFileUpload(){
-  if(server.uri() != "/edit") return;
-  HTTPUpload& upload = server.upload();
-  if(upload.status == UPLOAD_FILE_START){
-    String filename = upload.filename;
-    if(!filename.startsWith("/")) filename = "/"+filename;
-    DBG_OUTPUT_PORT.print("handleFileUpload Name: "); DBG_OUTPUT_PORT.println(filename);
-    fsUploadFile = SPIFFS.open(filename, "w");
-    filename = String();
-  } else if(upload.status == UPLOAD_FILE_WRITE){
-    //DBG_OUTPUT_PORT.print("handleFileUpload Data: "); DBG_OUTPUT_PORT.println(upload.currentSize);
-    if(fsUploadFile)
-      fsUploadFile.write(upload.buf, upload.currentSize);
-  } else if(upload.status == UPLOAD_FILE_END){
-    if(fsUploadFile)
-      fsUploadFile.close();
-    DBG_OUTPUT_PORT.print("handleFileUpload Size: "); DBG_OUTPUT_PORT.println(upload.totalSize);
-  }
-}
-
-void handleFileDelete(){
-  if(server.args() == 0) return server.send(500, "text/plain", "BAD ARGS");
-  String path = server.arg(0);
-  DBG_OUTPUT_PORT.println("handleFileDelete: " + path);
-  if(path == "/")
-    return server.send(500, "text/plain", "BAD PATH");
-  if(!SPIFFS.exists(path))
-    return server.send(404, "text/plain", "FileNotFound");
-  SPIFFS.remove(path);
-  server.send(200, "text/plain", "");
-  path = String();
-}
-
-void handleFileCreate(){
-  if(server.args() == 0)
-    return server.send(500, "text/plain", "BAD ARGS");
-  String path = server.arg(0);
-  DBG_OUTPUT_PORT.println("handleFileCreate: " + path);
-  if(path == "/")
-    return server.send(500, "text/plain", "BAD PATH");
-  if(SPIFFS.exists(path))
-    return server.send(500, "text/plain", "FILE EXISTS");
-  File file = SPIFFS.open(path, "w");
-  if(file)
-    file.close();
-  else
-    return server.send(500, "text/plain", "CREATE FAILED");
-  server.send(200, "text/plain", "");
-  path = String();
-}
-
-void handleFileList() {
-  if(!server.hasArg("dir")) {server.send(500, "text/plain", "BAD ARGS"); return;}
-  
-  String path = server.arg("dir");
-  DBG_OUTPUT_PORT.println("handleFileList: " + path);
-  Dir dir = SPIFFS.openDir(path);
-  path = String();
-
-  String output = "[";
-  while(dir.next()){
-    File entry = dir.openFile("r");
-    if (output != "[") output += ',';
-    bool isDir = false;
-    output += "{\"type\":\"";
-    output += (isDir)?"dir":"file";
-    output += "\",\"name\":\"";
-    output += String(entry.name()).substring(1);
-    output += "\"}";
-    entry.close();
-  }
-  
-  output += "]";
-  server.send(200, "text/json", output);
-}
-
-void setup(void){
-  DBG_OUTPUT_PORT.begin(115200);
-  DBG_OUTPUT_PORT.print("\n");
-  DBG_OUTPUT_PORT.setDebugOutput(true);
-  SPIFFS.begin();
-  {
-    Dir dir = SPIFFS.openDir("/");
-    while (dir.next()) {    
-      String fileName = dir.fileName();
-      size_t fileSize = dir.fileSize();
-      DBG_OUTPUT_PORT.printf("FS File: %s, size: %s\n", fileName.c_str(), formatBytes(fileSize).c_str());
-    }
-    DBG_OUTPUT_PORT.printf("\n");
-  }
-  
-
-  //WIFI INIT
-  DBG_OUTPUT_PORT.printf("Connecting to %s\n", ssid);
-  if (String(WiFi.SSID()) != String(ssid)) {
-    WiFi.begin(ssid, password);
-  }
-  
-  while (WiFi.status() != WL_CONNECTED) {
-    delay(500);
-    DBG_OUTPUT_PORT.print(".");
-  }
-  DBG_OUTPUT_PORT.println("");
-  DBG_OUTPUT_PORT.print("Connected! IP address: ");
-  DBG_OUTPUT_PORT.println(WiFi.localIP());
-
-  MDNS.begin(host);
-  DBG_OUTPUT_PORT.print("Open http://");
-  DBG_OUTPUT_PORT.print(host);
-  DBG_OUTPUT_PORT.println(".local/edit to see the file browser");
-  
-  
-  //SERVER INIT
-  //list directory
-  server.on("/list", HTTP_GET, handleFileList);
-  //load editor
-  server.on("/edit", HTTP_GET, [](){
-    if(!handleFileRead("/edit.htm")) server.send(404, "text/plain", "FileNotFound");
-  });
-  //create file
-  server.on("/edit", HTTP_PUT, handleFileCreate);
-  //delete file
-  server.on("/edit", HTTP_DELETE, handleFileDelete);
-  //first callback is called after the request has ended with all parsed arguments
-  //second callback handles file uploads at that location
-  server.on("/edit", HTTP_POST, [](){ server.send(200, "text/plain", ""); }, handleFileUpload);
-
-  //called when the url is not defined here
-  //use it to load content from SPIFFS
-  server.onNotFound([](){
-    if(!handleFileRead(server.uri()))
-      server.send(404, "text/plain", "FileNotFound");
-  });
-
-  //get heap status, analog input value and all GPIO statuses in one json call
-  server.on("/all", HTTP_GET, [](){
-    String json = "{";
-    json += "\"heap\":"+String(ESP.getFreeHeap());
-    json += ", \"analog\":"+String(analogRead(A0));
-    json += ", \"gpio\":"+String((uint32_t)(((GPI | GPO) & 0xFFFF) | ((GP16I & 0x01) << 16)));
-    json += "}";
-    server.send(200, "text/json", json);
-    json = String();
-  });
-  server.begin();
-  DBG_OUTPUT_PORT.println("HTTP server started");
-
-}
- 
-void loop(void){
-  server.handleClient();
-}

BIN
lib/ESP8266WebServer/examples/FSBrowser/data/edit.htm.gz


BIN
lib/ESP8266WebServer/examples/FSBrowser/data/favicon.ico


BIN
lib/ESP8266WebServer/examples/FSBrowser/data/graphs.js.gz


+ 0 - 97
lib/ESP8266WebServer/examples/FSBrowser/data/index.htm

@@ -1,97 +0,0 @@
-<!-- 
-  FSWebServer - Example Index Page
-  Copyright (c) 2015 Hristo Gochkov. All rights reserved.
-  This file is part of the ESP8266WebServer library for Arduino environment.
- 
-  This library is free software; you can redistribute it and/or
-  modify it under the terms of the GNU Lesser General Public
-  License as published by the Free Software Foundation; either
-  version 2.1 of the License, or (at your option) any later version.
-  This library is distributed in the hope that it will be useful,
-  but WITHOUT ANY WARRANTY; without even the implied warranty of
-  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-  Lesser General Public License for more details.
-  You should have received a copy of the GNU Lesser General Public
-  License along with this library; if not, write to the Free Software
-  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
--->
-<!DOCTYPE html>
-<html>
-<head>
-  <meta http-equiv="Content-type" content="text/html; charset=utf-8">
-  <title>ESP Monitor</title>
-  <script type="text/javascript" src="graphs.js"></script>
-  <script type="text/javascript">
-    var heap,temp,digi;
-    var reloadPeriod = 1000;
-    var running = false;
-    
-    function loadValues(){
-      if(!running) return;
-      var xh = new XMLHttpRequest();
-      xh.onreadystatechange = function(){
-        if (xh.readyState == 4){
-          if(xh.status == 200) {
-            var res = JSON.parse(xh.responseText);
-            heap.add(res.heap);
-            temp.add(res.analog);
-            digi.add(res.gpio);
-            if(running) setTimeout(loadValues, reloadPeriod);
-          } else running = false;
-        }
-      };
-      xh.open("GET", "/all", true);
-      xh.send(null);
-    };
-    
-    function run(){
-      if(!running){
-        running = true;
-        loadValues();
-      }
-    }
-    
-    function onBodyLoad(){
-      var refreshInput = document.getElementById("refresh-rate");
-      refreshInput.value = reloadPeriod;
-      refreshInput.onchange = function(e){
-        var value = parseInt(e.target.value);
-        reloadPeriod = (value > 0)?value:0;
-        e.target.value = reloadPeriod;
-      }
-      var stopButton = document.getElementById("stop-button");
-      stopButton.onclick = function(e){
-        running = false;
-      }
-      var startButton = document.getElementById("start-button");
-      startButton.onclick = function(e){
-        run();
-      }
-      
-      // Example with 10K thermistor
-      //function calcThermistor(v) {
-      //  var t = Math.log(((10230000 / v) - 10000));
-      //  t = (1/(0.001129148+(0.000234125*t)+(0.0000000876741*t*t*t)))-273.15;
-      //  return (t>120)?0:Math.round(t*10)/10;
-      //}
-      //temp = createGraph(document.getElementById("analog"), "Temperature", 100, 128, 10, 40, false, "cyan", calcThermistor);
-      
-      temp = createGraph(document.getElementById("analog"), "Analog Input", 100, 128, 0, 1023, false, "cyan");
-      heap = createGraph(document.getElementById("heap"), "Current Heap", 100, 125, 0, 30000, true, "orange");
-      digi = createDigiGraph(document.getElementById("digital"), "GPIO", 100, 146, [0, 4, 5, 16], "gold");
-      run();
-    }
-  </script>
-</head>
-<body id="index" style="margin:0; padding:0;" onload="onBodyLoad()">
-  <div id="controls" style="display: block; border: 1px solid rgb(68, 68, 68); padding: 5px; margin: 5px; width: 362px; background-color: rgb(238, 238, 238);">
-    <label>Period (ms):</label>
-    <input type="number" id="refresh-rate"/>
-    <input type="button" id="start-button" value="Start"/>
-    <input type="button" id="stop-button" value="Stop"/>
-  </div>
-  <div id="heap"></div>
-  <div id="analog"></div>
-  <div id="digital"></div>
-</body>
-</html>

+ 0 - 72
lib/ESP8266WebServer/examples/HelloServer/HelloServer.ino

@@ -1,72 +0,0 @@
-#include <ESP8266WiFi.h>
-#include <WiFiClient.h>
-#include <ESP8266WebServer.h>
-#include <ESP8266mDNS.h>
-
-const char* ssid = "........";
-const char* password = "........";
-
-ESP8266WebServer server(80);
-
-const int led = 13;
-
-void handleRoot() {
-  digitalWrite(led, 1);
-  server.send(200, "text/plain", "hello from esp8266!");
-  digitalWrite(led, 0);
-}
-
-void handleNotFound(){
-  digitalWrite(led, 1);
-  String message = "File Not Found\n\n";
-  message += "URI: ";
-  message += server.uri();
-  message += "\nMethod: ";
-  message += (server.method() == HTTP_GET)?"GET":"POST";
-  message += "\nArguments: ";
-  message += server.args();
-  message += "\n";
-  for (uint8_t i=0; i<server.args(); i++){
-    message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
-  }
-  server.send(404, "text/plain", message);
-  digitalWrite(led, 0);
-}
-
-void setup(void){
-  pinMode(led, OUTPUT);
-  digitalWrite(led, 0);
-  Serial.begin(115200);
-  WiFi.begin(ssid, password);
-  Serial.println("");
-
-  // Wait for connection
-  while (WiFi.status() != WL_CONNECTED) {
-    delay(500);
-    Serial.print(".");
-  }
-  Serial.println("");
-  Serial.print("Connected to ");
-  Serial.println(ssid);
-  Serial.print("IP address: ");
-  Serial.println(WiFi.localIP());
-
-  if (MDNS.begin("esp8266")) {
-    Serial.println("MDNS responder started");
-  }
-
-  server.on("/", handleRoot);
-
-  server.on("/inline", [](){
-    server.send(200, "text/plain", "this works as well");
-  });
-
-  server.onNotFound(handleNotFound);
-
-  server.begin();
-  Serial.println("HTTP server started");
-}
-
-void loop(void){
-  server.handleClient();
-}

+ 0 - 40
lib/ESP8266WebServer/examples/HttpBasicAuth/HttpBasicAuth.ino

@@ -1,40 +0,0 @@
-#include <ESP8266WiFi.h>
-#include <ESP8266mDNS.h>
-#include <ArduinoOTA.h>
-#include <ESP8266WebServer.h>
-
-const char* ssid = "........";
-const char* password = "........";
-
-ESP8266WebServer server(80);
-
-const char* www_username = "admin";
-const char* www_password = "esp8266";
-
-void setup() {
-  Serial.begin(115200);
-  WiFi.mode(WIFI_STA);
-  WiFi.begin(ssid, password);
-  if(WiFi.waitForConnectResult() != WL_CONNECTED) {
-    Serial.println("WiFi Connect Failed! Rebooting...");
-    delay(1000);
-    ESP.restart();
-  }
-  ArduinoOTA.begin();
-
-  server.on("/", [](){
-    if(!server.authenticate(www_username, www_password))
-      return server.requestAuthentication();
-    server.send(200, "text/plain", "Login OK");
-  });
-  server.begin();
-
-  Serial.print("Open http://");
-  Serial.print(WiFi.localIP());
-  Serial.println("/ in your browser to see it working");
-}
-
-void loop() {
-  ArduinoOTA.handle();
-  server.handleClient();
-}

+ 0 - 269
lib/ESP8266WebServer/examples/SDWebServer/SDWebServer.ino

@@ -1,269 +0,0 @@
-/*
-  SDWebServer - Example WebServer with SD Card backend for esp8266
-
-  Copyright (c) 2015 Hristo Gochkov. All rights reserved.
-  This file is part of the ESP8266WebServer library for Arduino environment.
-
-  This library is free software; you can redistribute it and/or
-  modify it under the terms of the GNU Lesser General Public
-  License as published by the Free Software Foundation; either
-  version 2.1 of the License, or (at your option) any later version.
-
-  This library is distributed in the hope that it will be useful,
-  but WITHOUT ANY WARRANTY; without even the implied warranty of
-  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-  Lesser General Public License for more details.
-
-  You should have received a copy of the GNU Lesser General Public
-  License along with this library; if not, write to the Free Software
-  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
-
-  Have a FAT Formatted SD Card connected to the SPI port of the ESP8266
-  The web root is the SD Card root folder
-  File extensions with more than 3 charecters are not supported by the SD Library
-  File Names longer than 8 charecters will be truncated by the SD library, so keep filenames shorter
-  index.htm is the default index (works on subfolders as well)
-
-  upload the contents of SdRoot to the root of the SDcard and access the editor by going to http://esp8266sd.local/edit
-
-*/
-#include <ESP8266WiFi.h>
-#include <WiFiClient.h>
-#include <ESP8266WebServer.h>
-#include <ESP8266mDNS.h>
-#include <SPI.h>
-#include <SD.h>
-
-#define DBG_OUTPUT_PORT Serial
-
-const char* ssid = "**********";
-const char* password = "**********";
-const char* host = "esp8266sd";
-
-ESP8266WebServer server(80);
-
-static bool hasSD = false;
-File uploadFile;
-
-
-void returnOK() {
-  server.send(200, "text/plain", "");
-}
-
-void returnFail(String msg) {
-  server.send(500, "text/plain", msg + "\r\n");
-}
-
-bool loadFromSdCard(String path){
-  String dataType = "text/plain";
-  if(path.endsWith("/")) path += "index.htm";
-
-  if(path.endsWith(".src")) path = path.substring(0, path.lastIndexOf("."));
-  else if(path.endsWith(".htm")) dataType = "text/html";
-  else if(path.endsWith(".css")) dataType = "text/css";
-  else if(path.endsWith(".js")) dataType = "application/javascript";
-  else if(path.endsWith(".png")) dataType = "image/png";
-  else if(path.endsWith(".gif")) dataType = "image/gif";
-  else if(path.endsWith(".jpg")) dataType = "image/jpeg";
-  else if(path.endsWith(".ico")) dataType = "image/x-icon";
-  else if(path.endsWith(".xml")) dataType = "text/xml";
-  else if(path.endsWith(".pdf")) dataType = "application/pdf";
-  else if(path.endsWith(".zip")) dataType = "application/zip";
-
-  File dataFile = SD.open(path.c_str());
-  if(dataFile.isDirectory()){
-    path += "/index.htm";
-    dataType = "text/html";
-    dataFile = SD.open(path.c_str());
-  }
-
-  if (!dataFile)
-    return false;
-
-  if (server.hasArg("download")) dataType = "application/octet-stream";
-
-  if (server.streamFile(dataFile, dataType) != dataFile.size()) {
-    DBG_OUTPUT_PORT.println("Sent less data than expected!");
-  }
-
-  dataFile.close();
-  return true;
-}
-
-void handleFileUpload(){
-  if(server.uri() != "/edit") return;
-  HTTPUpload& upload = server.upload();
-  if(upload.status == UPLOAD_FILE_START){
-    if(SD.exists((char *)upload.filename.c_str())) SD.remove((char *)upload.filename.c_str());
-    uploadFile = SD.open(upload.filename.c_str(), FILE_WRITE);
-    DBG_OUTPUT_PORT.print("Upload: START, filename: "); DBG_OUTPUT_PORT.println(upload.filename);
-  } else if(upload.status == UPLOAD_FILE_WRITE){
-    if(uploadFile) uploadFile.write(upload.buf, upload.currentSize);
-    DBG_OUTPUT_PORT.print("Upload: WRITE, Bytes: "); DBG_OUTPUT_PORT.println(upload.currentSize);
-  } else if(upload.status == UPLOAD_FILE_END){
-    if(uploadFile) uploadFile.close();
-    DBG_OUTPUT_PORT.print("Upload: END, Size: "); DBG_OUTPUT_PORT.println(upload.totalSize);
-  }
-}
-
-void deleteRecursive(String path){
-  File file = SD.open((char *)path.c_str());
-  if(!file.isDirectory()){
-    file.close();
-    SD.remove((char *)path.c_str());
-    return;
-  }
-
-  file.rewindDirectory();
-  while(true) {
-    File entry = file.openNextFile();
-    if (!entry) break;
-    String entryPath = path + "/" +entry.name();
-    if(entry.isDirectory()){
-      entry.close();
-      deleteRecursive(entryPath);
-    } else {
-      entry.close();
-      SD.remove((char *)entryPath.c_str());
-    }
-    yield();
-  }
-
-  SD.rmdir((char *)path.c_str());
-  file.close();
-}
-
-void handleDelete(){
-  if(server.args() == 0) return returnFail("BAD ARGS");
-  String path = server.arg(0);
-  if(path == "/" || !SD.exists((char *)path.c_str())) {
-    returnFail("BAD PATH");
-    return;
-  }
-  deleteRecursive(path);
-  returnOK();
-}
-
-void handleCreate(){
-  if(server.args() == 0) return returnFail("BAD ARGS");
-  String path = server.arg(0);
-  if(path == "/" || SD.exists((char *)path.c_str())) {
-    returnFail("BAD PATH");
-    return;
-  }
-
-  if(path.indexOf('.') > 0){
-    File file = SD.open((char *)path.c_str(), FILE_WRITE);
-    if(file){
-      file.write((const char *)0);
-      file.close();
-    }
-  } else {
-    SD.mkdir((char *)path.c_str());
-  }
-  returnOK();
-}
-
-void printDirectory() {
-  if(!server.hasArg("dir")) return returnFail("BAD ARGS");
-  String path = server.arg("dir");
-  if(path != "/" && !SD.exists((char *)path.c_str())) return returnFail("BAD PATH");
-  File dir = SD.open((char *)path.c_str());
-  path = String();
-  if(!dir.isDirectory()){
-    dir.close();
-    return returnFail("NOT DIR");
-  }
-  dir.rewindDirectory();
-  server.setContentLength(CONTENT_LENGTH_UNKNOWN);
-  server.send(200, "text/json", "");
-  WiFiClient client = server.client();
-
-  server.sendContent("[");
-  for (int cnt = 0; true; ++cnt) {
-    File entry = dir.openNextFile();
-    if (!entry)
-    break;
-
-    String output;
-    if (cnt > 0)
-      output = ',';
-
-    output += "{\"type\":\"";
-    output += (entry.isDirectory()) ? "dir" : "file";
-    output += "\",\"name\":\"";
-    output += entry.name();
-    output += "\"";
-    output += "}";
-    server.sendContent(output);
-    entry.close();
- }
- server.sendContent("]");
- dir.close();
-}
-
-void handleNotFound(){
-  if(hasSD && loadFromSdCard(server.uri())) return;
-  String message = "SDCARD Not Detected\n\n";
-  message += "URI: ";
-  message += server.uri();
-  message += "\nMethod: ";
-  message += (server.method() == HTTP_GET)?"GET":"POST";
-  message += "\nArguments: ";
-  message += server.args();
-  message += "\n";
-  for (uint8_t i=0; i<server.args(); i++){
-    message += " NAME:"+server.argName(i) + "\n VALUE:" + server.arg(i) + "\n";
-  }
-  server.send(404, "text/plain", message);
-  DBG_OUTPUT_PORT.print(message);
-}
-
-void setup(void){
-  DBG_OUTPUT_PORT.begin(115200);
-  DBG_OUTPUT_PORT.setDebugOutput(true);
-  DBG_OUTPUT_PORT.print("\n");
-  WiFi.begin(ssid, password);
-  DBG_OUTPUT_PORT.print("Connecting to ");
-  DBG_OUTPUT_PORT.println(ssid);
-
-  // Wait for connection
-  uint8_t i = 0;
-  while (WiFi.status() != WL_CONNECTED && i++ < 20) {//wait 10 seconds
-    delay(500);
-  }
-  if(i == 21){
-    DBG_OUTPUT_PORT.print("Could not connect to");
-    DBG_OUTPUT_PORT.println(ssid);
-    while(1) delay(500);
-  }
-  DBG_OUTPUT_PORT.print("Connected! IP address: ");
-  DBG_OUTPUT_PORT.println(WiFi.localIP());
-
-  if (MDNS.begin(host)) {
-    MDNS.addService("http", "tcp", 80);
-    DBG_OUTPUT_PORT.println("MDNS responder started");
-    DBG_OUTPUT_PORT.print("You can now connect to http://");
-    DBG_OUTPUT_PORT.print(host);
-    DBG_OUTPUT_PORT.println(".local");
-  }
-
-
-  server.on("/list", HTTP_GET, printDirectory);
-  server.on("/edit", HTTP_DELETE, handleDelete);
-  server.on("/edit", HTTP_PUT, handleCreate);
-  server.on("/edit", HTTP_POST, [](){ returnOK(); }, handleFileUpload);
-  server.onNotFound(handleNotFound);
-
-  server.begin();
-  DBG_OUTPUT_PORT.println("HTTP server started");
-
-  if (SD.begin(SS)){
-     DBG_OUTPUT_PORT.println("SD Card initialized.");
-     hasSD = true;
-  }
-}
-
-void loop(void){
-  server.handleClient();
-}

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 674
lib/ESP8266WebServer/examples/SDWebServer/SdRoot/edit/index.htm


+ 0 - 22
lib/ESP8266WebServer/examples/SDWebServer/SdRoot/index.htm

@@ -1,22 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-  <meta http-equiv="Content-type" content="text/html; charset=utf-8">
-  <title>ESP Index</title>
-  <style>
-    body {
-      background-color:black;
-      color:white;
-    }
-  </style>
-  <script type="text/javascript">
-    function onBodyLoad(){
-      console.log("we are loaded!!");
-    }
-  </script>
-</head>
-<body id="index" onload="onBodyLoad()">
-  <h1>ESP8266 Pin Functions</h1>
-<img src="pins.png" />
-</body>
-</html>

BIN
lib/ESP8266WebServer/examples/SDWebServer/SdRoot/pins.png


+ 0 - 126
lib/ESP8266WebServer/examples/SimpleAuthentification/SimpleAuthentification.ino

@@ -1,126 +0,0 @@
-#include <ESP8266WiFi.h>
-#include <WiFiClient.h>
-#include <ESP8266WebServer.h>
-
-const char* ssid = "........";
-const char* password = "........";
-
-ESP8266WebServer server(80);
-
-//Check if header is present and correct
-bool is_authentified(){
-  Serial.println("Enter is_authentified");
-  if (server.hasHeader("Cookie")){   
-    Serial.print("Found cookie: ");
-    String cookie = server.header("Cookie");
-    Serial.println(cookie);
-    if (cookie.indexOf("ESPSESSIONID=1") != -1) {
-      Serial.println("Authentification Successful");
-      return true;
-    }
-  }
-  Serial.println("Authentification Failed");
-  return false;	
-}
-
-//login page, also called for disconnect
-void handleLogin(){
-  String msg;
-  if (server.hasHeader("Cookie")){   
-    Serial.print("Found cookie: ");
-    String cookie = server.header("Cookie");
-    Serial.println(cookie);
-  }
-  if (server.hasArg("DISCONNECT")){
-    Serial.println("Disconnection");
-    String header = "HTTP/1.1 301 OK\r\nSet-Cookie: ESPSESSIONID=0\r\nLocation: /login\r\nCache-Control: no-cache\r\n\r\n";
-    server.sendContent(header);
-    return;
-  }
-  if (server.hasArg("USERNAME") && server.hasArg("PASSWORD")){
-    if (server.arg("USERNAME") == "admin" &&  server.arg("PASSWORD") == "admin" ){
-      String header = "HTTP/1.1 301 OK\r\nSet-Cookie: ESPSESSIONID=1\r\nLocation: /\r\nCache-Control: no-cache\r\n\r\n";
-      server.sendContent(header);
-      Serial.println("Log in Successful");
-      return;
-    }
-  msg = "Wrong username/password! try again.";
-  Serial.println("Log in Failed");
-  }
-  String content = "<html><body><form action='/login' method='POST'>To log in, please use : admin/admin<br>";
-  content += "User:<input type='text' name='USERNAME' placeholder='user name'><br>";
-  content += "Password:<input type='password' name='PASSWORD' placeholder='password'><br>";
-  content += "<input type='submit' name='SUBMIT' value='Submit'></form>" + msg + "<br>";
-  content += "You also can go <a href='/inline'>here</a></body></html>";
-  server.send(200, "text/html", content);
-}
-
-//root page can be accessed only if authentification is ok
-void handleRoot(){
-  Serial.println("Enter handleRoot");
-  String header;
-  if (!is_authentified()){
-    String header = "HTTP/1.1 301 OK\r\nLocation: /login\r\nCache-Control: no-cache\r\n\r\n";
-    server.sendContent(header);
-    return;
-  }
-  String content = "<html><body><H2>hello, you successfully connected to esp8266!</H2><br>";
-  if (server.hasHeader("User-Agent")){
-    content += "the user agent used is : " + server.header("User-Agent") + "<br><br>";
-  }
-  content += "You can access this page until you <a href=\"/login?DISCONNECT=YES\">disconnect</a></body></html>";
-  server.send(200, "text/html", content);
-}
-
-//no need authentification
-void handleNotFound(){
-  String message = "File Not Found\n\n";
-  message += "URI: ";
-  message += server.uri();
-  message += "\nMethod: ";
-  message += (server.method() == HTTP_GET)?"GET":"POST";
-  message += "\nArguments: ";
-  message += server.args();
-  message += "\n";
-  for (uint8_t i=0; i<server.args(); i++){
-    message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
-  }
-  server.send(404, "text/plain", message);
-}
-
-void setup(void){
-  Serial.begin(115200);
-  WiFi.begin(ssid, password);
-  Serial.println("");
-
-  // Wait for connection
-  while (WiFi.status() != WL_CONNECTED) {
-    delay(500);
-    Serial.print(".");
-  }
-  Serial.println("");
-  Serial.print("Connected to ");
-  Serial.println(ssid);
-  Serial.print("IP address: ");
-  Serial.println(WiFi.localIP());
-
-
-  server.on("/", handleRoot);
-  server.on("/login", handleLogin);
-  server.on("/inline", [](){
-    server.send(200, "text/plain", "this works without need of authentification");
-  });
-
-  server.onNotFound(handleNotFound);
-  //here the list of headers to be recorded
-  const char * headerkeys[] = {"User-Agent","Cookie"} ;
-  size_t headerkeyssize = sizeof(headerkeys)/sizeof(char*);
-  //ask server to track these headers
-  server.collectHeaders(headerkeys, headerkeyssize );
-  server.begin();
-  Serial.println("HTTP server started");
-}
-
-void loop(void){
-  server.handleClient();
-}

+ 0 - 71
lib/ESP8266WebServer/examples/WebUpdate/WebUpdate.ino

@@ -1,71 +0,0 @@
-/*
-  To upload through terminal you can use: curl -F "image=@firmware.bin" esp8266-webupdate.local/update
-*/
-
-#include <ESP8266WiFi.h>
-#include <WiFiClient.h>
-#include <ESP8266WebServer.h>
-#include <ESP8266mDNS.h>
-
-const char* host = "esp8266-webupdate";
-const char* ssid = "........";
-const char* password = "........";
-
-ESP8266WebServer server(80);
-const char* serverIndex = "<form method='POST' action='/update' enctype='multipart/form-data'><input type='file' name='update'><input type='submit' value='Update'></form>";
-
-void setup(void){
-  Serial.begin(115200);
-  Serial.println();
-  Serial.println("Booting Sketch...");
-  WiFi.mode(WIFI_AP_STA);
-  WiFi.begin(ssid, password);
-  if(WiFi.waitForConnectResult() == WL_CONNECTED){
-    MDNS.begin(host);
-    server.on("/", HTTP_GET, [](){
-      server.sendHeader("Connection", "close");
-      server.sendHeader("Access-Control-Allow-Origin", "*");
-      server.send(200, "text/html", serverIndex);
-    });
-    server.on("/update", HTTP_POST, [](){
-      server.sendHeader("Connection", "close");
-      server.sendHeader("Access-Control-Allow-Origin", "*");
-      server.send(200, "text/plain", (Update.hasError())?"FAIL":"OK");
-      ESP.restart();
-    },[](){
-      HTTPUpload& upload = server.upload();
-      if(upload.status == UPLOAD_FILE_START){
-        Serial.setDebugOutput(true);
-        WiFiUDP::stopAll();
-        Serial.printf("Update: %s\n", upload.filename.c_str());
-        uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
-        if(!Update.begin(maxSketchSpace)){//start with max available size
-          Update.printError(Serial);
-        }
-      } else if(upload.status == UPLOAD_FILE_WRITE){
-        if(Update.write(upload.buf, upload.currentSize) != upload.currentSize){
-          Update.printError(Serial);
-        }
-      } else if(upload.status == UPLOAD_FILE_END){
-        if(Update.end(true)){ //true to set the size to the current progress
-          Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
-        } else {
-          Update.printError(Serial);
-        }
-        Serial.setDebugOutput(false);
-      }
-      yield();
-    });
-    server.begin();
-    MDNS.addService("http", "tcp", 80);
-  
-    Serial.printf("Ready! Open http://%s.local in your browser\n", host);
-  } else {
-    Serial.println("WiFi Failed");
-  }
-}
- 
-void loop(void){
-  server.handleClient();
-  delay(1);
-} 

+ 0 - 114
lib/GithubClient/GithubClient.cpp

@@ -1,114 +0,0 @@
-#include <GithubClient.h>
-#include <FS.h>
-
-Stream& GithubClient::stream(const String& path) {
-  if (!client.connect(domain.c_str(), 443)) {
-    Serial.println(F("Failed to connect to github over HTTPS."));
-    return client;
-  }
-  
-  if (!client.verify(sslFingerprint.c_str(), domain.c_str())) {
-    Serial.println(F("Failed to verify github certificate"));
-    return client;
-  }
-  
-  client.printf(
-    "GET %s HTTP/1.1\r\nHost: %s\r\nUser-Agent: esp8266_milight_hub\r\nConnection: close\r\n\r\n",
-    path.c_str(), 
-    domain.c_str()
-  );
-               
-  return client;
-}
-  
-bool GithubClient::download(const String& path, Stream& dest) {
-  Stream& client = stream(path);
-  
-  if (client.available()) {
-    if (!client.find("\r\n\r\n")) {
-      Serial.println(F("Error seeking to body"));
-      return false;
-    }
-  } else {
-    Serial.println(F("Failed to open stream to Github"));
-    return false;
-  }
-  
-  Serial.println(F("Downloading..."));
-  
-  size_t bytes = 0;
-  size_t nextCheckpoint = 4096;
-               
-  while (client.available()) {
-    size_t l = client.readBytes(buffer, GITHUB_CLIENT_BUFFER_SIZE);
-    size_t w = dest.write(buffer, l);
-    
-    dest.flush();
-    
-    if (w != l) {
-      printf_P(PSTR("Error writing to stream. Expected to write %d bytes, but only wrote %d\n"), l, w);
-      return false;
-    }
-    
-    bytes += w;
-    
-    if (bytes % 10 == 0) {
-      printf_P(".");
-    }
-    
-    if (bytes >= nextCheckpoint) {
-      printf("[%d KB]\n", bytes/1024);
-      nextCheckpoint += 4096;
-    }
-    
-    yield();
-  }
-  
-  Serial.println(F("\n"));
-  
-  return true;
-}
-
-bool GithubClient::download(const String& path, const String& fsPath) {
-  String tmpFile = fsPath + ".download_tmp";
-  File f = SPIFFS.open(tmpFile.c_str(), "w");
-  
-  if (!f) {
-    Serial.print(F("ERROR - could not open file for downloading: "));
-    Serial.println(fsPath);
-    return false;
-  }
-  printf(".");
-  
-  if (!download(path, f)) {
-    f.close();
-    return false;
-  }
-  
-  f.flush();
-  f.close();
-  
-  SPIFFS.remove(fsPath);
-  SPIFFS.rename(tmpFile, fsPath);
-  
-  printf("Finished downloading file: %s\n", fsPath.c_str());
-  
-  return true;
-}
-  
-String GithubClient::buildRepoPath(const String& username, const String& repo, const String& repoPath) {
-  String path = String("/") + username + "/" + repo + "/master/" + repoPath;
-  return path;
-}
-
-String GithubClient::buildApiRequest(const String &username, const String &repo, const String &path) {
-  return String("/repos/") + username + "/" + repo + path;
-}
-  
-GithubClient GithubClient::rawDownloader() {
-  return GithubClient(GITHUB_RAW_DOMAIN, GITHUB_RAW_FINGERPRINT);
-}
-
-GithubClient GithubClient::apiClient() {
-  return GithubClient(GITHUB_API_DOMAIN, GITHUB_API_FINGERPRINT);
-}

+ 0 - 40
lib/GithubClient/GithubClient.h

@@ -1,40 +0,0 @@
-#include <Arduino.h>
-#include <WiFiClientSecure.h>
-
-#ifndef _GITHUB_CLIENT
-#define _GITHUB_CLIENT 
-
-#define GITHUB_CLIENT_BUFFER_SIZE 32
-
-#define GITHUB_RAW_FINGERPRINT "CC AA 48 48 66 46 0E 91 53 2C 9C 7C 23 2A B1 74 4D 29 9D 33"
-#define GITHUB_RAW_DOMAIN "raw.githubusercontent.com"
-
-#define GITHUB_API_FINGERPRINT "35 85 74 EF 67 35 A7 CE 40 69 50 F3 C0 F6 80 CF 80 3B 2E 19"
-#define GITHUB_API_DOMAIN "api.github.com"
-
-class GithubClient {
-public:
-  GithubClient(const char* domain, const char* sslFingerprint)
-    : domain(String(domain)),
-      sslFingerprint(String(sslFingerprint))
-  { }
-  
-  Stream& stream(const String& path);
-  bool download(const String& path, Stream& dest);
-  bool download(const String& path, const String& fsPath);
-  
-  static GithubClient rawDownloader();
-  static GithubClient apiClient();
-  
-  static String buildRepoPath(const String& username, const String& repo, const String& path);
-  static String buildApiRequest(const String& username, const String& repo, const String& path);
-  
-  uint8_t buffer[GITHUB_CLIENT_BUFFER_SIZE];
-  
-private:
-  WiFiClientSecure client;
-  const String domain;
-  const String sslFingerprint;
-};
-
-#endif

+ 22 - 7
lib/Helpers/IntParsing.h

@@ -7,10 +7,10 @@ template <typename T>
 const T strToHex(const char* s, size_t length) {
   T value = 0;
   T base = 1;
-  
+
   for (int i = length-1; i >= 0; i--) {
     const char c = s[i];
-    
+
     if (c >= '0' && c <= '9') {
       value += ((c - '0') * base);
     } else if (c >= 'a' && c <= 'f') {
@@ -20,10 +20,10 @@ const T strToHex(const char* s, size_t length) {
     } else {
       break;
     }
-    
+
     base <<= 4;
   }
-  
+
   return value;
 }
 
@@ -44,15 +44,30 @@ const T parseInt(const String& s) {
 template <typename T>
 void hexStrToBytes(const char* s, const size_t sLen, T* buffer, size_t maxLen) {
   int idx = 0;
-  
+
   for (int i = 0; i < sLen && idx < maxLen; ) {
     buffer[idx++] = strToHex<T>(s+i, 2);
     i+= 2;
-    
+
     while (i < (sLen - 1) && s[i] == ' ') {
       i++;
     }
   }
 }
 
-#endif
+class IntParsing {
+public:
+  static void bytesToHexStr(const uint8_t* bytes, const size_t len, char* buffer, size_t maxLen) {
+    char* p = buffer;
+
+    for (size_t i = 0; i < len && (p - buffer) < (maxLen - 3); i++) {
+      p += sprintf(p, "%02X", bytes[i]);
+
+      if (i < (len - 1)) {
+        p += sprintf(p, " ");
+      }
+    }
+  }
+};
+
+#endif

+ 27 - 10
lib/MiLight/MiLightClient.cpp

@@ -253,6 +253,11 @@ void MiLightClient::update(const JsonObject& request) {
     }
   }
 
+  //Homeassistant - Handle effect
+  if (request.containsKey("effect")) {
+    this->handleEffect(request["effect"]);
+  }
+
   if (request.containsKey("hue")) {
     this->updateHue(request["hue"]);
   }
@@ -267,16 +272,20 @@ void MiLightClient::update(const JsonObject& request) {
     uint8_t r = color["r"];
     uint8_t g = color["g"];
     uint8_t b = color["b"];
-
-    double hsv[3];
-    RGBConverter converter;
-    converter.rgbToHsv(r, g, b, hsv);
-
-    uint16_t hue = round(hsv[0]*360);
-    uint8_t saturation = round(hsv[1]*100);
-
-    this->updateHue(hue);
-    this->updateSaturation(saturation);
+    //If close to white
+    if( r > 256 - RGB_WHITE_BOUNDARY && g > 256 - RGB_WHITE_BOUNDARY && b > 256 - RGB_WHITE_BOUNDARY) {
+        this->updateColorWhite();
+    } else {
+      double hsv[3];
+      RGBConverter converter;
+      converter.rgbToHsv(r, g, b, hsv);
+
+      uint16_t hue = round(hsv[0]*360);
+      uint8_t saturation = round(hsv[1]*100);
+
+      this->updateHue(hue);
+      this->updateSaturation(saturation);
+    }
   }
 
   if (request.containsKey("level")) {
@@ -336,6 +345,14 @@ void MiLightClient::handleCommand(const String& command) {
   }
 }
 
+void MiLightClient::handleEffect(const String& effect) {
+  if (effect == "night_mode") {
+    this->enableNightMode();
+  } else if (effect == "white") {
+    this->updateColorWhite();
+  }
+}
+
 uint8_t MiLightClient::parseStatus(const JsonObject& object) {
   String strStatus;
 

+ 4 - 2
lib/MiLight/MiLightClient.h

@@ -11,7 +11,8 @@
 //#define DEBUG_PRINTF
 
 #define MILIGHT_DEFAULT_RESEND_COUNT 10
-
+//Used to determine close to white
+#define RGB_WHITE_BOUNDARY 40
 
 class MiLightClient {
 public:
@@ -66,7 +67,8 @@ public:
 
   void update(const JsonObject& object);
   void handleCommand(const String& command);
-
+  void handleEffect(const String& effect);
+  
   void onPacketSent(PacketSentHandler handler);
 
 protected:

+ 1 - 1
lib/MiLight/RgbCctPacketFormatter.h

@@ -35,7 +35,7 @@ enum MiLightRgbCctArguments {
 
 class RgbCctPacketFormatter : public PacketFormatter {
 public:
-  static uint8_t const V2_OFFSETS[][4] PROGMEM;
+  static uint8_t const V2_OFFSETS[][4];
 
   RgbCctPacketFormatter()
     : PacketFormatter(RGB_CCT_PACKET_LEN),

+ 4 - 4
lib/Udp/V6ComamndHandler.cpp

@@ -6,10 +6,10 @@
 #include <Size.h>
 
 V6CommandHandler* V6CommandHandler::ALL_HANDLERS[] = {
-  new V6RgbCctCommandHandler() PROGMEM,
-  new V6RgbwCommandHandler() PROGMEM,
-  new V6RgbCommandHandler() PROGMEM,
-  new V6CctCommandHandler() PROGMEM,
+  new V6RgbCctCommandHandler(),
+  new V6RgbwCommandHandler(),
+  new V6RgbCommandHandler(),
+  new V6CctCommandHandler()
 };
 
 const size_t V6CommandHandler::NUM_HANDLERS = size(ALL_HANDLERS);

+ 1 - 1
lib/Udp/V6CommandHandler.h

@@ -13,7 +13,7 @@ enum V6CommandTypes {
 
 class V6CommandHandler {
 public:
-  static V6CommandHandler* ALL_HANDLERS[] PROGMEM;
+  static V6CommandHandler* ALL_HANDLERS[];
   static const size_t NUM_HANDLERS;
 
   V6CommandHandler(uint16_t commandId, MiLightRadioConfig& radioConfig)

+ 11 - 11
lib/Udp/V6MiLightUdpServer.h

@@ -48,20 +48,20 @@ public:
   static uint8_t* writeInt(const T& value, uint8_t* packet);
 
 protected:
-  static V6CommandDemuxer COMMAND_DEMUXER PROGMEM;
+  static V6CommandDemuxer COMMAND_DEMUXER;
 
-  static uint8_t START_SESSION_COMMAND[] PROGMEM;
-  static uint8_t START_SESSION_RESPONSE[] PROGMEM;
-  static uint8_t COMMAND_HEADER[] PROGMEM;
-  static uint8_t COMMAND_RESPONSE[] PROGMEM;
-  static uint8_t LOCAL_SEARCH_COMMAND[] PROGMEM;
-  static uint8_t HEARTBEAT_HEADER[] PROGMEM;
-  static uint8_t HEARTBEAT_HEADER2[] PROGMEM;
+  static uint8_t START_SESSION_COMMAND[];
+  static uint8_t START_SESSION_RESPONSE[];
+  static uint8_t COMMAND_HEADER[];
+  static uint8_t COMMAND_RESPONSE[];
+  static uint8_t LOCAL_SEARCH_COMMAND[];
+  static uint8_t HEARTBEAT_HEADER[];
+  static uint8_t HEARTBEAT_HEADER2[];
 
-  static uint8_t SEARCH_COMMAND[] PROGMEM;
-  static uint8_t SEARCH_RESPONSE[] PROGMEM;
+  static uint8_t SEARCH_COMMAND[];
+  static uint8_t SEARCH_RESPONSE[];
 
-  static uint8_t OPEN_COMMAND_RESPONSE[] PROGMEM;
+  static uint8_t OPEN_COMMAND_RESPONSE[];
 
   V6Session* firstSession;
   size_t numSessions;

+ 75 - 90
lib/WebServer/MiLightHttpServer.cpp

@@ -4,14 +4,14 @@
 #include <Settings.h>
 #include <MiLightHttpServer.h>
 #include <MiLightRadioConfig.h>
-#include <GithubClient.h>
 #include <string.h>
 #include <TokenIterator.h>
+#include <index.html.gz.h>
 
 void MiLightHttpServer::begin() {
   applySettings(settings);
 
-  server.on("/", HTTP_GET, handleServeFile(WEB_INDEX_FILENAME, "text/html", DEFAULT_INDEX_PAGE));
+  server.on("/", HTTP_GET, handleServe_P(index_html_gz, index_html_gz_len));
   server.on("/settings", HTTP_GET, handleServeFile(SETTINGS_FILE, APPLICATION_JSON));
   server.on("/settings", HTTP_PUT, [this]() { handleUpdateSettings(); });
   server.on("/settings", HTTP_POST, [this]() { server.send_P(200, TEXT_PLAIN, PSTR("success. rebooting")); ESP.restart(); }, handleUpdateFile(SETTINGS_FILE));
@@ -22,10 +22,8 @@ void MiLightHttpServer::begin() {
 
   server.onPattern("/gateways/:device_id/:type/:group_id", HTTP_ANY, [this](const UrlTokenBindings* b) { handleUpdateGroup(b); });
   server.onPattern("/raw_commands/:type", HTTP_ANY, [this](const UrlTokenBindings* b) { handleSendRaw(b); });
-  server.onPattern("/download_update/:component", HTTP_GET, [this](const UrlTokenBindings* b) { handleDownloadUpdate(b); });
-  server.on("/web", HTTP_POST, [this]() { server.send(200, TEXT_PLAIN, "success"); }, handleUpdateFile(WEB_INDEX_FILENAME));
+  server.on("/web", HTTP_POST, [this]() { server.send_P(200, TEXT_PLAIN, PSTR("success")); }, handleUpdateFile(WEB_INDEX_FILENAME));
   server.on("/about", HTTP_GET, [this]() { handleAbout(); });
-  server.on("/latest_release", HTTP_GET, [this]() { handleGetLatestRelease(); });
   server.on("/system", HTTP_POST, [this]() { handleSystemPost(); });
   server.on("/firmware", HTTP_POST,
     [this](){
@@ -69,44 +67,19 @@ void MiLightHttpServer::begin() {
       yield();
     }
   );
-
-  server.begin();
-}
-
-void MiLightHttpServer::handleGetLatestRelease() {
-  GithubClient client = GithubClient::apiClient();
-  String path = GithubClient::buildApiRequest(
-    MILIGHT_GITHUB_USER,
-    MILIGHT_GITHUB_REPO,
-    "/releases/latest"
+  wsServer.onEvent(
+    [this](uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
+      handleWsEvent(num, type, payload, length);
+    }
   );
+  wsServer.begin();
 
-  Serial.println(path);
-
-  // This is an ugly hack, but probably not worth optimizing. The nice way
-  // to do this would be to extract the content len from GitHub's response
-  // and stream the body to the server directly. But this would require parsing
-  // headers in the response from GitHub, which seems like more trouble than
-  // it's worth.
-  const String& fsPath = "/_cv.json";
-  size_t tries = 0;
-
-  while (tries++ < MAX_DOWNLOAD_ATTEMPTS && !client.download(path, fsPath)) {
-    Serial.println(F("Failed download attempt."));
-  }
-
-  if (!SPIFFS.exists(fsPath)) {
-    server.send_P(500, TEXT_PLAIN, PSTR("Failed to stream API request from GitHub. Check Serial logs for more information."));
-    return;
-  }
-
-  File file = SPIFFS.open(fsPath, "r");
-  server.streamFile(file, APPLICATION_JSON);
-  SPIFFS.remove(fsPath);
+  server.begin();
 }
 
 void MiLightHttpServer::handleClient() {
   server.handleClient();
+  wsServer.loop();
 }
 
 void MiLightHttpServer::on(const char* path, HTTPMethod method, ESP8266WebServer::THandlerFunction handler) {
@@ -126,7 +99,7 @@ void MiLightHttpServer::handleSystemPost() {
   if (request.containsKey("command")) {
     if (request["command"] == "restart") {
       Serial.println(F("Restarting..."));
-      server.send(200, TEXT_PLAIN, "true");
+      server.send_P(200, TEXT_PLAIN, PSTR("true"));
 
       delay(100);
 
@@ -135,7 +108,7 @@ void MiLightHttpServer::handleSystemPost() {
       handled = true;
     } else if (request["command"] == "clear_wifi_config") {
         Serial.println(F("Resetting Wifi and then Restarting..."));
-        server.send(200, TEXT_PLAIN, "true");
+        server.send_P(200, TEXT_PLAIN, PSTR("true"));
 
         delay(100);
         ESP.eraseConfig();
@@ -147,46 +120,9 @@ void MiLightHttpServer::handleSystemPost() {
   }
 
   if (handled) {
-    server.send(200, TEXT_PLAIN, "true");
+    server.send_P(200, TEXT_PLAIN, PSTR("true"));
   } else {
-    server.send(400, TEXT_PLAIN, F("{\"error\":\"Unhandled command\"}"));
-  }
-}
-
-void MiLightHttpServer::handleDownloadUpdate(const UrlTokenBindings* bindings) {
-  GithubClient downloader = GithubClient::rawDownloader();
-  const String& component = bindings->get("component");
-
-  if (component.equalsIgnoreCase("web")) {
-    Serial.println(F("Attempting to update web UI..."));
-
-    bool result = false;
-    size_t tries = 0;
-
-    while (!result && tries++ <= MAX_DOWNLOAD_ATTEMPTS) {
-      Serial.println(F("building url\n"));
-      String urlPath = GithubClient::buildRepoPath(
-        MILIGHT_GITHUB_USER,
-        MILIGHT_GITHUB_REPO,
-        MILIGHT_REPO_WEB_PATH
-      );
-
-      printf_P(PSTR("URL: %s\n"), urlPath.c_str());
-
-      result = downloader.download(urlPath, WEB_INDEX_FILENAME);
-    }
-
-    Serial.println(F("Download complete!"));
-
-    if (result) {
-      server.sendHeader("Location", "/");
-      server.send(302);
-    } else {
-      server.send(500, TEXT_PLAIN, F("Failed to download update from Github. Check serial logs for more information."));
-    }
-  } else {
-    String body = String("Unknown component: ") + component;
-    server.send(400, TEXT_PLAIN, body);
+    server.send_P(400, TEXT_PLAIN, PSTR("{\"error\":\"Unhandled command\"}"));
   }
 }
 
@@ -211,11 +147,13 @@ void MiLightHttpServer::handleAbout() {
   response["version"] = QUOTE(MILIGHT_HUB_VERSION);
   response["variant"] = QUOTE(FIRMWARE_VARIANT);
   response["free_heap"] = ESP.getFreeHeap();
+  response["arduino_version"] = ESP.getCoreVersion();
+  response["reset_reason"] = ESP.getResetReason();
 
   String body;
   response.printTo(body);
 
-  server.send(200, "application", body);
+  server.send(200, APPLICATION_JSON, body);
 }
 
 void MiLightHttpServer::handleGetRadioConfigs() {
@@ -307,7 +245,7 @@ void MiLightHttpServer::handleListenGateway(const UrlTokenBindings* bindings) {
     String body = "Unknown device type: ";
     body += bindings->get("type");
 
-    server.send(400, TEXT_PLAIN, body);
+    server.send(400, "text/plain", body);
     return;
   }
 
@@ -344,7 +282,7 @@ void MiLightHttpServer::handleListenGateway(const UrlTokenBindings* bindings) {
   );
   milightClient->formatPacket(packet, responseBuffer);
 
-  server.send(200, TEXT_PLAIN, response);
+  server.send(200, "text/plain", response);
 }
 
 void MiLightHttpServer::handleUpdateGroup(const UrlTokenBindings* urlBindings) {
@@ -352,7 +290,7 @@ void MiLightHttpServer::handleUpdateGroup(const UrlTokenBindings* urlBindings) {
   JsonObject& request = buffer.parse(server.arg("plain"));
 
   if (!request.success()) {
-    server.send(400, TEXT_PLAIN, F("Invalid JSON"));
+    server.send_P(400, TEXT_PLAIN, PSTR("Invalid JSON"));
     return;
   }
 
@@ -379,9 +317,9 @@ void MiLightHttpServer::handleUpdateGroup(const UrlTokenBindings* urlBindings) {
     MiLightRadioConfig* config = MiLightRadioConfig::fromString(_radioType);
 
     if (config == NULL) {
-      String body = "Unknown device type: ";
-      body += String(_radioType);
-      server.send(400, TEXT_PLAIN, body);
+      char buffer[40];
+      sprintf_P(buffer, PSTR("Unknown device type: %s"), _radioType);
+      server.send(400, "text/plain", buffer);
       return;
     }
 
@@ -412,10 +350,9 @@ void MiLightHttpServer::handleSendRaw(const UrlTokenBindings* bindings) {
   MiLightRadioConfig* config = MiLightRadioConfig::fromString(bindings->get("type"));
 
   if (config == NULL) {
-    String body = "Unknown device type: ";
-    body += bindings->get("type");
-
-    server.send(400, TEXT_PLAIN, body);
+    char buffer[50];
+    sprintf_P(buffer, PSTR("Unknown device type: %s"), bindings->get("type"));
+    server.send(400, "text/plain", buffer);
     return;
   }
 
@@ -434,5 +371,53 @@ void MiLightHttpServer::handleSendRaw(const UrlTokenBindings* bindings) {
     milightClient->write(packet);
   }
 
-  server.send(200, TEXT_PLAIN, "true");
+  server.send_P(200, TEXT_PLAIN, PSTR("true"));
+}
+
+void MiLightHttpServer::handleWsEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) {
+  switch (type) {
+    case WStype_DISCONNECTED:
+      if (numWsClients > 0) {
+        numWsClients--;
+      }
+      break;
+
+    case WStype_CONNECTED:
+      numWsClients++;
+      break;
+  }
+}
+
+void MiLightHttpServer::handlePacketSent(uint8_t *packet, const MiLightRadioConfig& config) {
+  if (numWsClients > 0) {
+    size_t packetLen = config.packetFormatter->getPacketLength();
+    char buffer[packetLen*3];
+    IntParsing::bytesToHexStr(packet, packetLen, buffer, packetLen*3);
+
+    char formattedPacket[200];
+    config.packetFormatter->format(packet, formattedPacket);
+
+    char responseBuffer[300];
+    sprintf_P(
+      responseBuffer,
+      PSTR("\n%s packet received (%d bytes):\n%s"),
+      config.name,
+      sizeof(packet),
+      formattedPacket
+    );
+
+    wsServer.broadcastTXT(reinterpret_cast<uint8_t*>(responseBuffer));
+  }
+}
+
+ESP8266WebServer::THandlerFunction MiLightHttpServer::handleServe_P(const char* data, size_t length) {
+  return [this, data, length]() {
+    server.sendHeader("Content-Encoding", "gzip");
+    server.sendHeader("Content-Length", String(length));
+    server.setContentLength(CONTENT_LENGTH_UNKNOWN);
+    server.send(200, "text/html", "");
+    server.setContentLength(length);
+    server.sendContent_P(data, length);
+    server.client().stop();
+  };
 }

+ 9 - 6
lib/WebServer/MiLightHttpServer.h

@@ -1,6 +1,7 @@
 #include <WebServer.h>
 #include <MiLightClient.h>
 #include <Settings.h>
+#include <WebSocketsServer.h>
 
 #ifndef _MILIGHT_HTTP_SERVER
 #define _MILIGHT_HTTP_SERVER
@@ -9,16 +10,15 @@
 
 typedef std::function<void(void)> SettingsSavedHandler;
 
-const char DEFAULT_INDEX_PAGE[] PROGMEM
-  = "Web app not installed. Click <a href=\"/download_update/web\">here</a> to attempt to download it from GitHub.";
-
 const char TEXT_PLAIN[] PROGMEM = "text/plain";
-const char APPLICATION_JSON[] PROGMEM = "application/json";
+const char APPLICATION_JSON[] = "application/json";
 
 class MiLightHttpServer {
 public:
   MiLightHttpServer(Settings& settings, MiLightClient*& milightClient)
     : server(WebServer(80)),
+      wsServer(WebSocketsServer(81)),
+      numWsClients(0),
       milightClient(milightClient),
       settings(settings)
   {
@@ -29,6 +29,7 @@ public:
   void handleClient();
   void onSettingsSaved(SettingsSavedHandler handler);
   void on(const char* path, HTTPMethod method, ESP8266WebServer::THandlerFunction handler);
+  void handlePacketSent(uint8_t* packet, const MiLightRadioConfig& config);
   WiFiClient client();
 
 protected:
@@ -39,26 +40,28 @@ protected:
 
   bool serveFile(const char* file, const char* contentType = "text/html");
   ESP8266WebServer::THandlerFunction handleUpdateFile(const char* filename);
+  ESP8266WebServer::THandlerFunction handleServe_P(const char* data, size_t length);
   void applySettings(Settings& settings);
 
   void handleUpdateSettings();
   void handleGetRadioConfigs();
   void handleAbout();
-  void handleGetLatestRelease();
   void handleSystemPost();
   void handleListenGateway(const UrlTokenBindings* urlBindings);
   void handleSendRaw(const UrlTokenBindings* urlBindings);
   void handleUpdateGroup(const UrlTokenBindings* urlBindings);
-  void handleDownloadUpdate(const UrlTokenBindings* urlBindings);
 
   void handleRequest(const JsonObject& request);
+  void handleWsEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length);
 
   File updateFile;
 
   WebServer server;
+  WebSocketsServer wsServer;
   Settings& settings;
   MiLightClient*& milightClient;
   SettingsSavedHandler settingsSavedHandler;
+  size_t numWsClients;
 
 };
 

+ 8 - 2
platformio.ini

@@ -13,12 +13,16 @@ board_f_cpu = 160000000L
 lib_deps_builtin =
   SPI
 lib_deps_external =
-  RF24
+  sidoh/RF24
   WiFiManager
   ArduinoJson
   PubSubClient
   https://github.com/ratkins/RGBConverter
-build_flags = !python .get_version.py -DMQTT_MAX_PACKET_SIZE=200
+  Hash
+  WebSockets
+build_flags = !python .get_version.py -DMQTT_MAX_PACKET_SIZE=200 -Idist
+extra_script =
+  .build_web.py
 # -D MQTT_DEBUG
 # -D MILIGHT_UDP_DEBUG
 # -D DEBUG_PRINTF
@@ -27,7 +31,9 @@ build_flags = !python .get_version.py -DMQTT_MAX_PACKET_SIZE=200
 platform = espressif8266
 framework = arduino
 board = nodemcuv2
+; upload_speed = 115200
 build_flags = ${common.build_flags} -Wl,-Tesp8266.flash.4m1m.ld -D FIRMWARE_VARIANT=nodemcuv2
+extra_script = ${common.extra_script}
 lib_deps =
   ${common.lib_deps_builtin}
   ${common.lib_deps_external}

+ 1 - 1
src/main.cpp

@@ -3,7 +3,6 @@
 #include <ArduinoJson.h>
 #include <stdlib.h>
 #include <FS.h>
-#include <GithubClient.h>
 #include <IntParsing.h>
 #include <Size.h>
 #include <MiLightRadioConfig.h>
@@ -80,6 +79,7 @@ void onPacketSentHandler(uint8_t* packet, const MiLightRadioConfig& config) {
   if (mqttClient) {
     mqttClient->sendUpdate(type, deviceId, groupId, output);
   }
+  httpServer->handlePacketSent(packet, config);
 }
 
 void handleListen() {

+ 65 - 0
web/gulpfile.js

@@ -0,0 +1,65 @@
+const fs = require('fs');
+const gulp = require('gulp');
+const htmlmin = require('gulp-htmlmin');
+const cleancss = require('gulp-clean-css');
+const uglify = require('gulp-uglify');
+const gzip = require('gulp-gzip');
+const del = require('del');
+const inline = require('gulp-inline');
+const inlineImages = require('gulp-css-base64');
+const favicon = require('gulp-base64-favicon');
+
+const dataFolder = 'build/';
+
+gulp.task('clean', function() {
+    del([ dataFolder + '*']);
+    return true;
+});
+
+gulp.task('buildfs_embeded', ['buildfs_inline'], function() {
+
+    var source = dataFolder + 'index.html.gz';
+    var destination = dataFolder + 'index.html.gz.h';
+
+    var wstream = fs.createWriteStream(destination);
+    wstream.on('error', function (err) {
+        console.log(err);
+    });
+
+    var data = fs.readFileSync(source);
+
+    wstream.write('#define index_html_gz_len ' + data.length + '\n');
+    wstream.write('static const char index_html_gz[] PROGMEM = {')
+
+    for (i=0; i<data.length; i++) {
+        wstream.write(data[i].toString());
+        if (i<data.length-1) wstream.write(',');
+    }
+
+    wstream.write('};')
+    wstream.end();
+
+    del();
+
+});
+
+gulp.task('buildfs_inline', ['clean'], function() {
+    return gulp.src('src/*.html')
+        // .pipe(favicon())
+        .pipe(inline({
+            base: 'src/',
+            js: uglify,
+            css: [cleancss, inlineImages],
+            disabledTypes: ['svg', 'img']
+        }))
+        .pipe(htmlmin({
+            collapseWhitespace: true,
+            removeComments: true,
+            minifyCSS: true,
+            minifyJS: true
+        }))
+        .pipe(gzip())
+        .pipe(gulp.dest(dataFolder));
+})
+
+gulp.task('default', ['buildfs_embeded']);

+ 23 - 0
web/package.json

@@ -0,0 +1,23 @@
+{
+  "name": "esp8266-milight-hub-web",
+  "version": "1.0.0",
+  "description": "",
+  "main": "gulpfile.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "devDependencies": {
+    "del": "^2.2.1",
+    "gulp": "^3.9.1",
+    "gulp-base64-favicon": "^1.0.2",
+    "gulp-clean-css": "^3.4.2",
+    "gulp-css-base64": "^1.3.4",
+    "gulp-gzip": "^1.4.0",
+    "gulp-htmlmin": "^2.0.0",
+    "gulp-inline": "^0.1.1",
+    "gulp-uglify": "^1.5.3"
+  }
+}

+ 103 - 0
web/src/css/style.css

@@ -0,0 +1,103 @@
+.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; }
+.form-entry { margin: 0 0 20px 0; }
+.form-entry .form-control { width: 20em; }
+.form-entry label { display: inline-block; }
+label:not(.error) .error-info { display: none; }
+.error-info { color: #f00; font-size: 0.7em; }
+.error-info:before { content: '('; }
+.error-info:after { content: ')'; }
+.header-btn { margin: 20px; }
+#sniffed-traffic { max-height: 50em; overflow-y: auto; }
+.btn-secondary {
+  background-color: #fff;
+  border: 1px solid #ccc;
+}
+.inline { display: inline-block; }
+.white-temp-picker {
+  height: 2em;
+  background: linear-gradient(to right,
+    rgb(166, 209, 255) 0%,
+    rgb(255, 255, 255) 50%,
+    rgb(255, 160, 0) 100%
+  );
+  display: inline-block;
+  padding: 3px 0;
+}
+.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;
+}
+.plus-minus-group {
+  overflow: auto;
+  width: 100%;
+  clear: both;
+  display: block;
+}
+.plus-minus-group button:first-of-type {
+  border-bottom-right-radius: 0;
+  border-top-right-radius: 0;
+  float: left;
+  display: block;
+}
+.plus-minus-group button:last-of-type {
+  border-bottom-left-radius: 0;
+  border-top-left-radius: 0;
+  display: block;
+}
+.plus-minus-group .title {
+  border-width: 1px 0;
+  border-color: #ccc;
+  border-style: solid;
+  padding: 5px 5px 5px 7px;
+  margin: 0;
+  height: 34px;
+  line-height: 1.49;
+  float: left;
+  display: block;
+}
+.field-help {
+  display: inline-block;
+  font-size: 1.25em;
+  margin-left: 0.5em;
+}
+.glyphicon.spinning {
+  animation: spin 1s infinite linear;
+  -webkit-animation: spin2 1s infinite linear;
+}
+
+@keyframes spin {
+  from { transform: scale(1) rotate(0deg); }
+  to { transform: scale(1) rotate(360deg); }
+}
+
+@-webkit-keyframes spin2 {
+  from { -webkit-transform: rotate(0deg); }
+  to { -webkit-transform: rotate(360deg); }
+}

+ 440 - 0
web/src/index.html

@@ -0,0 +1,440 @@
+<!doctype html>
+
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+
+  <title>MiLight Hub</title>
+
+  <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"/>
+  <link href="https://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.12.4/css/selectize.bootstrap3.min.css" rel="stylesheet"/>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <!--[if lt IE 9]>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.js"></script>
+  <![endif]-->
+  <link href="css/style.css" rel="stylesheet" />
+</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 src="https://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.12.4/js/standalone/selectize.min.js"></script>
+
+  <script src="js/script.js" lang="text/javascript"></script>
+
+  <div id="update-firmware-modal" class="modal fade" role="dialog">
+    <div class="modal-dialog">
+      <!-- Modal content-->
+      <div class="modal-content">
+        <div class="modal-header">
+          <button type="button" class="close" data-dismiss="modal">&times;</button>
+          <h2 class="modal-title">Update Firmware</h2>
+        </div>
+        <div class="modal-body">
+          <div class="alert alert-warning">
+            <p>
+            Download firmware binaries from the
+            <a href="https://github.com/sidoh/esp8266_milight_hub/releases">GitHub releases page</a>.
+            Check for a new version by clicking on the "Check for Updates" button.
+            </p>
+
+            <p>
+              <b>Make sure the binary you're uploading was compiled for your board!</b>
+              Firmware with incompatible settings could prevent boots. If this happens,
+              reflash the board with USB.
+            </p>
+          </div>
+          <form action="/firmware" method="post" enctype="multipart/form-data">
+            <input type="file" name="file"/>
+            <p>&nbsp;</p>
+            <input type="submit" name="submit" class="btn btn-success"/>
+          </form>
+        </div>
+        <div class="modal-footer">
+          <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
+        </div>
+      </div>
+
+    </div>
+  </div>
+
+  <div id="updates-modal" class="modal fade" role="dialog">
+   <div class="modal-dialog">
+     <!-- Modal content-->
+     <div class="modal-content">
+       <div class="modal-header">
+         <button type="button" class="close" data-dismiss="modal">&times;</button>
+         <h2 class="modal-title">Update</h2>
+       </div>
+       <div class="modal-body">
+         <div id="version-summary"></div>
+
+         <hr />
+
+         <h4>Current Version</h4>
+         <div id="current-version"></div>
+
+         <div id="latest-version">
+           <h4>Latest Version</h4>
+           
+           <div class="status"></div>
+           <div id="latest-version-info">
+             <label>Version</label>
+             <div class="info-key" data-key="tag_name"></div>
+
+             <label>Release Date</label>
+             <div class="info-key" data-key="published_at"></div>
+
+             <label>Release Notes</label>
+             <pre class="info-key" data-key="body"></pre>
+
+             <div>
+               <a class="info-key" data-prop="href" data-key="html_url">View on GitHub</a>
+             </div>
+
+             <div>
+               <a class="info-key" id="firmware-link">Download Firmware</a>
+             </div>
+           </div>
+         </div>
+       </div>
+       <div class="modal-footer">
+         <a href="/download_update/web" class="btn btn-primary">Update Web UI</a>
+         <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
+       </div>
+     </div>
+   </div>
+  </div>
+
+  <div id="restore-settings-modal" class="modal fade" role="dialog">
+    <div class="modal-dialog">
+      <!-- Modal content-->
+      <div class="modal-content">
+        <div class="modal-header">
+          <button type="button" class="close" data-dismiss="modal">&times;</button>
+          <h2 class="modal-title">Restore Settings</h2>
+        </div>
+        <div class="modal-body">
+          <form action="/settings" method="post" enctype="multipart/form-data">
+            <input type="file" name="file"/>
+            <p>&nbsp;</p>
+            <input type="submit" name="submit" class="btn btn-success"/>
+          </form>
+        </div>
+        <div class="modal-footer">
+          <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
+        </div>
+      </div>
+
+    </div>
+  </div>
+
+  <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" id="device-id-label">
+          Device Id
+          <span class="error-info"></span>
+        </label>
+        <select id="deviceId" placeholder="Enter hub ID">
+				</select>
+      </div>
+
+      <div class="col-sm-3">
+        <div class="mode-option" id="group-option" data-for="cct,rgbw,rgb_cct">
+          <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>
+            <label class="btn btn-secondary">
+              <input type="radio" name="options" autocomplete="off" data-value="0"> All
+            </label>
+          </div>
+        </div>
+      </div>
+
+      <div class="col-sm-4">
+        <label for="groupId">Mode</label>
+
+        <div class="btn-group" id="mode" data-toggle="buttons">
+          <label class="btn btn-secondary active">
+            <input type="radio" name="mode" autocomplete="off" data-value="rgbw" checked> RGBW
+          </label>
+          <label class="btn btn-secondary">
+            <input type="radio" name="mode" autocomplete="off" data-value="cct"> CCT
+          </label>
+          <label class="btn btn-secondary">
+            <input type="radio" name="mode" autocomplete="off" data-value="rgb_cct"> RGB+CCT
+          </label>
+          <label class="btn btn-secondary">
+            <input type="radio" name="mode" autocomplete="off" data-value="rgb"> RGB
+          </label>
+        </div>
+      </div>
+    </div>
+
+    <div class="row"><div class="col-sm-12">
+    <div class="mode-option" data-for="rgbw,rgb_cct,rgb">
+      <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>
+    </div></div>
+
+    <div class="mode-option" data-for="rgb_cct">
+      <div class="row">
+        <div class="col-sm-12">
+          <h5>Saturation</h5>
+        </div>
+      </div>
+      <div class="row">
+        <div class="col-sm-6">
+          <input class="slider raw-update" name="saturation"
+              data-slider-min="0"
+              data-slider-max="100"
+              data-slider-value="100"
+          />
+        </div>
+      </div>
+    </div>
+
+    <div class="mode-option" data-for="cct,rgb_cct">
+      <div class="row">
+        <div class="col-sm-12">
+          <h5>Color Temperature</h5>
+        </div>
+      </div>
+      <div class="row">
+        <div class="col-sm-6">
+          <div class="white-temp-picker">
+            <input class="slider raw-update" name="temperature"
+                data-slider-min="0"
+                data-slider-max="100"
+                data-slider-value="100"
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="row">
+      <div class="col-sm-12">
+        <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>
+          <div class="mode-option inline" data-for="rgbw,rgb_cct">
+            <li>
+              <button type="button" class="btn btn-secondary command-btn" data-command="set_white">White</button>
+            </li>
+          </div>
+          <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>
+        <p></p>
+        <ul class="command-buttons">
+          <div class="mode-option inline" data-for="rgb,rgbw,rgb_cct">
+            <li>
+              <div class="plus-minus-group">
+                <button type="button" class="btn btn-default btn-number command-btn" data-command="previous_mode">
+                  <span class="glyphicon glyphicon-minus"></span>
+                </button>
+                <span class="title">Mode</span>
+                <button type="button" class="btn btn-default btn-number command-btn clearfix" data-command="next_mode">
+                  <span class="glyphicon glyphicon-plus"></span>
+                </button>
+                <div class="clearfix"></div>
+              </div>
+            </li>
+          </div>
+          <div class="mode-option inline" data-for="rgb,rgbw,rgb_cct">
+            <li>
+              <div class="plus-minus-group">
+                <button type="button" class="btn btn-default btn-number command-btn" data-command="mode_speed_down">
+                  <span class="glyphicon glyphicon-minus"></span>
+                </button>
+                <span class="title">Speed</span>
+                <button type="button" class="btn btn-default btn-number command-btn" data-command="mode_speed_up">
+                  <span class="glyphicon glyphicon-plus"></span>
+                </button>
+                <div class="clearfix"></div>
+              </div>
+            </li>
+          </div>
+        </ul>
+      </div>
+    </div>
+
+    <div class="row header-row">
+      <div class="col col-sm-10">
+        <h1>Gateway Servers</h1>
+      </div>
+
+      <div class="col col-sm-2">
+        <button class="btn btn-success header-btn" id="add-server-btn">
+          <i class="glyphicon glyphicon-plus"></i>
+          Add Server
+        </button>
+      </div>
+    </div>
+
+    <div class="row">
+      <div class="col col-sm-12">
+        <form id="gateway-server-form">
+          <table class="table">
+            <thead>
+              <tr>
+                <th>Device ID</th>
+                <th>UDP Port</th>
+                <th>Protocol Version</th>
+              </tr>
+            </thead>
+            <tbody id="gateway-server-configs">
+            </tbody>
+          </table>
+          <input type="submit" class="btn btn-success" value="Save" />
+        </form>
+      </div>
+    </div>
+
+    <div>&nbsp;</div>
+
+    <div class="row header-row">
+      <div class="col-sm-12">
+        <h1>Settings</h1>
+      </div>
+    </div>
+
+    <div>&nbsp;</div>
+
+    <div class="row">
+      <div class="col-sm-12">
+        <form action="#" id="settings">
+          <input type="submit" class="btn btn-success" value="Submit" />
+        </form>
+      </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>
+
+        <div> &nbsp; </div>
+
+        <div id="sniffed-traffic"></div>
+      </div>
+    </div>
+
+    <div class="row header-row">
+      <div class="col-sm-12">
+        <h1>Admin</h1>
+      </div>
+    </div>
+
+    <div>&nbsp;</div>
+
+    <div class="row">
+      <div class="col-sm-12">
+        <button type="button" class="btn btn-danger system-btn" data-command="restart">
+          Restart
+        </button>
+
+        <button type="button" class="btn btn-danger system-btn" data-command="clear_wifi_config">
+          Clear Wifi Config
+        </button>
+
+        <button type="button" id="updates-btn" class="btn btn-primary">
+          Check for Updates
+        </button>
+
+        <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#update-firmware-modal">
+          Update Firmware
+        </button>
+
+        <a href="/settings" download="settings.json" class="btn btn-primary">Backup Settings</a>
+
+        <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#restore-settings-modal">
+          Restore Settings
+        </button>
+      </div>
+    </div>
+  </div>
+</body>
+</html>

+ 514 - 0
web/src/js/script.js

@@ -0,0 +1,514 @@
+var FORM_SETTINGS = [
+  "admin_username", "admin_password", "ce_pin", "csn_pin", "reset_pin","packet_repeats",
+  "http_repeat_factor", "auto_restart_period", "discovery_port", "mqtt_server",
+  "mqtt_topic_pattern", "mqtt_update_topic_pattern", "mqtt_username", "mqtt_password",
+  "radio_interface_type", "listen_repeats"
+];
+
+var FORM_SETTINGS_HELP = {
+  ce_pin : "'CE' for NRF24L01 interface, and 'PKT' for 'PL1167/LT8900' interface",
+  packet_repeats : "The number of times to repeat RF packets sent to bulbs",
+  http_repeat_factor : "Multiplicative factor on packet_repeats for " +
+    "requests initiated by the HTTP API. UDP API typically receives " +
+    "duplicate packets, so more repeats should be used for HTTP.",
+  auto_restart_period : "Automatically restart the device every number of " +
+    "minutes specified. Use 0 for disabled.",
+  radio_interface_type : "2.4 GHz radio model. Only change this if you know " +
+    "You're not using an NRF24L01!",
+  mqtt_server : "Domain or IP address of MQTT broker. Optionally specify a port " +
+    "with (example) mymqqtbroker.com:1884.",
+  mqtt_topic_pattern : "Pattern for MQTT topics to listen on. Example: " +
+    "lights/:device_id/:device_type/:group_id. See README for further details.",
+  mqtt_update_topic_pattern : "Pattern to publish MQTT updates. Packets that " +
+    "are received from other devices, and packets that are sent from this device will " +
+    "result in updates being sent.",
+  discovery_port : "UDP port to listen for discovery packets on. Defaults to " +
+    "the same port used by MiLight devices, 48899. Use 0 to disable.",
+  listen_repeats : "Increasing this increases the amount of time spent listening for " +
+    "packets. Set to 0 to disable listening. Default is 3."
+}
+
+var UDP_PROTOCOL_VERSIONS = [ 5, 6 ];
+var DEFAULT_UDP_PROTOCL_VERSION = 5;
+
+var selectize;
+var sniffing = false;
+
+var webSocket = new WebSocket("ws://" + location.hostname + ":81");
+webSocket.onmessage = function(e) {
+  if (sniffing) {
+    var message = e.data;
+    $('#sniffed-traffic').prepend('<pre>' + message + '</pre>');
+  }
+}
+
+var toHex = function(v) {
+  return "0x" + (v).toString(16).toUpperCase();
+}
+
+var activeUrl = function() {
+  var deviceId = $('#deviceId option:selected').val()
+    , groupId = $('#groupId .active input').data('value')
+    , mode = getCurrentMode();
+
+  if (deviceId == "") {
+    alert("Please enter a device ID.");
+    throw "Must enter device ID";
+  }
+
+  if (! $('#group-option').data('for').split(',').includes(mode)) {
+    groupId = 0;
+  }
+
+  return "/gateways/" + deviceId + "/" + mode + "/" + groupId;
+}
+
+var getCurrentMode = function() {
+  return $('input[name="mode"]:checked').data('value');
+};
+
+var updateGroup = _.throttle(
+  function(params) {
+    $.ajax(
+      activeUrl(),
+      {
+        method: 'PUT',
+        data: JSON.stringify(params),
+        contentType: 'application/json'
+      }
+    );
+  },
+  1000
+);
+
+var sendCommand = _.throttle(
+  function(params) {
+    $.ajax(
+      '/system',
+      {
+        method: 'POST',
+        data: JSON.stringify(params),
+        contentType: 'application/json'
+      }
+    );
+  },
+  1000
+)
+
+var gatewayServerRow = function(deviceId, port, version) {
+  var elmt = '<tr>';
+  elmt += '<td>';
+  elmt += '<input name="deviceIds[]" class="form-control" value="' + deviceId + '"/>';
+  elmt += '</td>';
+  elmt += '<td>'
+  elmt += '<input name="ports[]" class="form-control" value="' + port + '"/>';;
+  elmt += '</td>';
+  elmt += '<td>';
+  elmt += '<div class="btn-group" data-toggle="buttons">';
+
+  for (var i = 0; i < UDP_PROTOCOL_VERSIONS.length; i++) {
+    var val = UDP_PROTOCOL_VERSIONS[i]
+      , selected = (version == val || (val == DEFAULT_UDP_PROTOCL_VERSION && !UDP_PROTOCOL_VERSIONS.includes(version)));
+
+    elmt += '<label class="btn btn-secondary' + (selected ? ' active' : '') + '">';
+    elmt += '<input type="radio" name="versions[]" autocomplete="off" data-value="' + val + '" '
+      + (selected ? 'checked' : '') +'> ' + val;
+    elmt += '</label>';
+  }
+
+  elmt += '</div></td>';
+  elmt += '<td>';
+  elmt += '<button class="btn btn-danger remove-gateway-server">';
+  elmt += '<i class="glyphicon glyphicon-remove"></i>';
+  elmt += '</button>';
+  elmt += '</td>';
+  elmt += '</tr>';
+  return elmt;
+}
+
+var loadSettings = function() {
+  $.getJSON('/settings', function(val) {
+    Object.keys(val).forEach(function(k) {
+      var field = $('#settings input[name="' + k + '"]');
+
+      if (field.length > 0) {
+        if (field.attr('type') === 'radio') {
+          field.filter('[value="' + val[k] + '"]').click();
+        } else {
+          field.val(val[k]);
+        }
+      }
+    });
+
+    if (val.device_ids) {
+      selectize.clearOptions();
+      val.device_ids.forEach(function(v) {
+        selectize.addOption({text: toHex(v), value: v});
+      });
+      selectize.refreshOptions();
+    }
+
+    var gatewayForm = $('#gateway-server-configs').html('');
+    if (val.gateway_configs) {
+      val.gateway_configs.forEach(function(v) {
+        gatewayForm.append(gatewayServerRow(toHex(v[0]), v[1], v[2]));
+      });
+    }
+  });
+};
+
+var saveGatewayConfigs = function() {
+  var form = $('#gateway-server-form')
+    , errors = false;
+
+  $('input', form).removeClass('error');
+
+  var deviceIds = $('input[name="deviceIds[]"]', form).map(function(i, v) {
+    var val = $(v).val();
+
+    if (isNaN(val)) {
+      errors = true;
+      $(v).addClass('error');
+      return null;
+    } else {
+      return val;
+    }
+  });
+
+  var ports = $('input[name="ports[]"]', form).map(function(i, v) {
+    var val = $(v).val();
+
+    if (isNaN(val)) {
+      errors = true;
+      $(v).addClass('error');
+      return null;
+    } else {
+      return val;
+    }
+  });
+
+  var versions = $('.active input[name="versions[]"]', form).map(function(i, v) {
+    return $(v).data('value');
+  });
+
+  if (!errors) {
+    var data = [];
+    for (var i = 0; i < deviceIds.length; i++) {
+      data[i] = [deviceIds[i], ports[i], versions[i]];
+    }
+    $.ajax(
+      '/settings',
+      {
+        method: 'put',
+        contentType: 'application/json',
+        data: JSON.stringify({gateway_configs: data})
+      }
+    )
+  }
+};
+
+var deviceIdError = function(v) {
+  if (!v) {
+    $('#device-id-label').removeClass('error');
+  } else {
+    $('#device-id-label').addClass('error');
+    $('#device-id-label .error-info').html(v);
+  }
+};
+
+var updateModeOptions = function() {
+  var currentMode = getCurrentMode();
+
+  $('.mode-option').map(function() {
+    if ($(this).data('for').split(',').includes(currentMode)) {
+      $(this).show();
+    } else {
+      $(this).hide();
+    }
+  });
+};
+
+var parseVersion = function(v) {
+  var matches = v.match(/(\d+)\.(\d+)\.(\d+)(-(.*))?/);
+
+  return {
+    major: matches[1],
+    minor: matches[2],
+    patch: matches[3],
+    revision: matches[5],
+    parts: [matches[1], matches[2], matches[3], matches[5]]
+  };
+};
+
+var isNewerVersion = function(a, b) {
+  var va = parseVersion(a)
+    , vb = parseVersion(b);
+
+  return va.parts > vb.parts;
+};
+
+var handleCheckForUpdates = function() {
+  var currentVersion = null
+    , latestRelease = null;
+
+  var handleReceiveData = function() {
+    if (currentVersion != null) {
+      $('#current-version').html(currentVersion.version + " (" + currentVersion.variant + ")");
+    }
+
+    if (latestRelease != null) {
+      $('#latest-version .info-key').each(function() {
+        var value = latestRelease[$(this).data('key')];
+        var prop = $(this).data('prop');
+
+        if (prop) {
+          $(this).prop(prop, value);
+        } else {
+          $(this).html(value);
+        }
+      });
+    }
+
+    if (currentVersion != null && latestRelease != null) {
+      $('#latest-version .status').html('');
+      $('#latest-version-info').show();
+
+      var summary;
+      if (isNewerVersion(latestRelease.tag_name, currentVersion.version)) {
+        summary = "New version available!";
+      } else {
+        summary = "You're on the most recent version.";
+      }
+      $('#version-summary').html(summary);
+
+      var releaseAsset = latestRelease.assets.filter(function(x) {
+        return x.name.indexOf(currentVersion.variant) != -1;
+      });
+
+      if (releaseAsset.length > 0) {
+        $('#firmware-link').prop('href', releaseAsset[0].browser_download_url);
+      }
+    }
+  }
+
+  var handleError = function(e, d) {
+    console.log(e);
+    console.log(d);
+  }
+
+  $('#current-version,#latest-version .status').html('<i class="spinning glyphicon glyphicon-refresh"></i>');
+  $('#version-summary').html('');
+  $('#latest-version-info').hide();
+  $('#updates-modal').modal();
+
+  $.ajax(
+    '/about',
+    {
+      success: function(data) {
+        currentVersion = data;
+        handleReceiveData();
+      },
+      failure: handleError
+    }
+  );
+
+  $.ajax(
+    'https://api.github.com/repos/sidoh/esp8266_milight_hub/releases/latest',
+    {
+      success: function(data) {
+        latestRelease = data;
+        handleReceiveData();
+      },
+      failure: handleError
+    }
+  );
+};
+
+$(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')});
+  });
+
+  $('.system-btn').click(function() {
+    sendCommand({command: $(this).data('command')});
+  });
+
+  $('#sniff').click(function() {
+    if (sniffing) {
+      sniffing = false;
+      $(this).html('Start Sniffing');
+    } else {
+      sniffing = true;
+      $(this).html('Stop Sniffing');
+    }
+  });
+
+  $('#add-server-btn').click(function() {
+    $('#gateway-server-configs').append(gatewayServerRow('', ''));
+  });
+
+  $('#mode').change(updateModeOptions);
+
+  $('body').on('click', '.remove-gateway-server', function() {
+    $(this).closest('tr').remove();
+  });
+
+  selectize = $('#deviceId').selectize({
+    create: true,
+    sortField: 'text',
+    onOptionAdd: function(v, item) {
+      item.value = parseInt(item.value);
+    },
+    createFilter: function(v) {
+      if (! v.match(/^(0x[a-fA-F0-9]{1,4}|[0-9]{1,5})$/)) {
+        deviceIdError("Must be an integer between 0x0000 and 0xFFFF");
+        return false;
+      }
+
+      var value = parseInt(v);
+
+      if (! (0 <= v && v <= 0xFFFF)) {
+        deviceIdError("Must be an integer between 0x0000 and 0xFFFF");
+        return false;
+      }
+
+      deviceIdError(false);
+
+      return true;
+    }
+  });
+  selectize = selectize[0].selectize;
+
+  var settings = "";
+
+  FORM_SETTINGS.forEach(function(k) {
+    var elmt = '<div class="form-entry">';
+    elmt += '<div>';
+    elmt += '<label for="' + k + '">' + k + '</label>';
+
+    if (FORM_SETTINGS_HELP[k]) {
+      elmt += '<div class="field-help" data-help-text="' + FORM_SETTINGS_HELP[k] + '"></div>';
+    }
+
+    elmt += '</div>';
+
+    if(k === "radio_interface_type") {
+      elmt += '<div class="btn-group" id="radio_interface_type" data-toggle="buttons">' +
+        '<label class="btn btn-secondary active">' +
+          '<input type="radio" id="nrf24" name="radio_interface_type" autocomplete="off" value="nRF24" /> nRF24' +
+        '</label>'+
+        '<label class="btn btn-secondary">' +
+          '<input type="radio" id="lt8900" name="radio_interface_type" autocomplete="off" value="LT8900" /> PL1167/LT8900' +
+        '</label>' +
+      '</div>';
+    } else {
+      elmt += '<input type="text" class="form-control" name="' + k + '"/>';
+      elmt += '</div>';
+    }
+
+    settings += elmt;
+  });
+
+  $('#settings').prepend(settings);
+  $('#settings').submit(function(e) {
+    var obj = {};
+
+    FORM_SETTINGS.forEach(function(k) {
+      var elmt = $('#settings input[name="' + k + '"]');
+
+      if (elmt.attr('type') === 'radio') {
+        obj[k] = elmt.filter(':checked').val();
+      } else {
+        obj[k] = elmt.val();
+      }
+    });
+
+    // pretty hacky. whatever.
+    obj.device_ids = _.map(
+      $('.selectize-control .option'),
+      function(x) {
+        return $(x).data('value')
+      }
+    );
+
+    $.ajax(
+      "/settings",
+      {
+        method: 'put',
+        contentType: 'application/json',
+        data: JSON.stringify(obj)
+      }
+    );
+
+    e.preventDefault();
+    return false;
+  });
+
+  $('#gateway-server-form').submit(function(e) {
+    saveGatewayConfigs();
+    e.preventDefault();
+    return false;
+  });
+
+  $('.field-help').each(function() {
+    var elmt = $('<i></i>')
+      .addClass('glyphicon glyphicon-question-sign')
+      .tooltip({
+        placement: 'top',
+        title: $(this).data('help-text'),
+        container: 'body'
+      });
+    $(this).append(elmt);
+  });
+
+  $('#updates-btn').click(handleCheckForUpdates);
+
+  loadSettings();
+  updateModeOptions();
+});