瀏覽代碼

Merge pull request #29 from sidoh/v1.1.0

v1.1.0 Release
Chris Mullins 8 年之前
父節點
當前提交
e878e5bb06
共有 44 個文件被更改,包括 1683 次插入297 次删除
  1. 1 0
      .gitignore
  2. 25 0
      .prepare_release
  3. 18 9
      .travis.yml
  4. 27 12
      README.md
  5. 334 2
      data/web/index.html
  6. 二進制
      dist/firmware-d1-mini.bin
  7. 二進制
      dist/firmware-esp07.bin
  8. 二進制
      dist/firmware-esp12.bin
  9. 二進制
      dist/firmware-nodemcuv2.bin
  10. 114 0
      lib/GithubClient/GithubClient.cpp
  11. 40 0
      lib/GithubClient/GithubClient.h
  12. 6 0
      lib/Helpers/Size.h
  13. 17 0
      lib/MiLight/CctPacketFormatter.h
  14. 1 61
      lib/MiLight/MiLightButtons.h
  15. 25 1
      lib/MiLight/MiLightClient.cpp
  16. 5 0
      lib/MiLight/MiLightClient.h
  17. 14 22
      lib/MiLight/PacketFormatter.cpp
  18. 15 11
      lib/MiLight/PacketFormatter.h
  19. 42 21
      lib/MiLight/RgbCctPacketFormatter.cpp
  20. 29 2
      lib/MiLight/RgbCctPacketFormatter.h
  21. 22 6
      lib/MiLight/RgbPacketFormatter.cpp
  22. 4 0
      lib/MiLight/RgbPacketFormatter.h
  23. 18 1
      lib/MiLight/RgbwPacketFormatter.cpp
  24. 35 0
      lib/MiLight/RgbwPacketFormatter.h
  25. 27 2
      lib/Settings/Settings.cpp
  26. 22 4
      lib/Settings/Settings.h
  27. 1 1
      lib/Udp/MiLightUdpServer.cpp
  28. 2 2
      lib/Udp/V5MiLightUdpServer.cpp
  29. 49 0
      lib/Udp/V6CctCommandHandler.cpp
  30. 34 0
      lib/Udp/V6CctCommandHandler.h
  31. 63 0
      lib/Udp/V6ComamndHandler.cpp
  32. 76 0
      lib/Udp/V6CommandHandler.h
  33. 95 62
      lib/Udp/V6MiLightUdpServer.cpp
  34. 20 28
      lib/Udp/V6MiLightUdpServer.h
  35. 83 0
      lib/Udp/V6RgbCctCommandHandler.cpp
  36. 41 0
      lib/Udp/V6RgbCctCommandHandler.h
  37. 60 0
      lib/Udp/V6RgbCommandHandler.cpp
  38. 35 0
      lib/Udp/V6RgbCommandHandler.h
  39. 59 0
      lib/Udp/V6RgbwCommandHandler.cpp
  40. 36 0
      lib/Udp/V6RgbwCommandHandler.h
  41. 157 46
      lib/WebServer/MiLightHttpServer.cpp
  42. 9 1
      lib/WebServer/MiLightHttpServer.h
  43. 6 2
      platformio.ini
  44. 16 1
      src/main.cpp

+ 1 - 0
.gitignore

@@ -2,3 +2,4 @@
 .piolibdeps
 .clang_complete
 .gcc-flags.json
+/dist

+ 25 - 0
.prepare_release

@@ -0,0 +1,25 @@
+#!/bin/bash
+
+set -eo pipefail
+
+prepare_log() {
+  echo "[prepare release] -- $@"
+}
+
+if [ -z "$(git tag -l --points-at HEAD)" ]; then
+  prepare_log "Skipping non-tagged commit."
+  exit 0
+fi
+
+VERSION=$(git describe)
+
+prepare_log "Preparing release for tagged version: $VERSION"
+
+mkdir -p dist
+
+for file in $(ls .pioenvs/**/firmware.bin); do
+  env_dir=$(dirname "$file")
+  env=$(basename "$env_dir")
+
+  cp "$file" "dist/esp8266_milight_hub_${env}-${VERSION}.bin"
+done

File diff suppressed because it is too large
+ 18 - 9
.travis.yml


+ 27 - 12
README.md

@@ -36,6 +36,8 @@ platformio run -e $ESP_BOARD --target uploadfs
 
 Of course make sure to substitute `nodemcuv2` with the board that you're using.
 
+You can find pre-compiled firmware images on the [releases](https://github.com/sidoh/esp8266_milight_hub/releases).
+
 #### Configure WiFi
 
 This project uses [WiFiManager](https://github.com/tzapu/WiFiManager) to avoid the need to hardcode AP credentials in the firmware.
@@ -51,31 +53,44 @@ The HTTP endpoints (shown below) will be fully functional at this point. You sho
 ## REST endpoints
 
 1. `GET /`. Opens web UI. You'll need to upload it first.
+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).
 1. `GET /gateway_traffic/:device_type`. Starts an HTTP long poll. Returns any Milight traffic it hears. Useful if you need to know what your Milight gateway/remote ID is. Since protocols for RGBW/CCT are different, specify one of `rgbw`, `cct`, or `rgb_cct` as `:device_type. Accepts a JSON blob.
 1. `PUT /gateways/:device_id/:device_type/:group_id`. Controls or sends commands to `:group_id` from `:device_id`. 
-1. `PUT /gateways/:device_id/:device_type`. A few commands have support for being sent to all groups. You can send those here.
-1. `POST /firmware`. OTA firmware update.
-1. `POST /web`. Update web UI.
+1. `POST /raw_commands/:device_type`. Sends a raw RF packet with radio configs associated with `:device_type`. Example body:
+    ```
+    {"packet": "01 02 03 04 05 06 07 08 09", "num_repeats": 10}
+    ```
 
 #### Bulb commands
 
-Route (5) supports these commands:
+Route (5) supports these commands. Note that each bulb type has support for a different subset of these commands:
 
 1. `status`. Toggles on/off. Can be "on", "off", "true", or "false".
-2. `hue`. (RGBW only) This is the only way to control color with these bulbs. Should be in the range `[0, 359]`.
-3. `level`. (RGBW only) Controls brightness. Should be in the range `[0, 100]`.
-4. `temperature`. (CCT only) Controls white temperature. Should be in the range `[0, 100]`.
-5. `saturation`. (new RGB+CCT only) Controls saturation.
-6. `command`. Sends a command to the group. Can be one of:
-   * `set_white`. (RGBW only) Turns off RGB and enters WW/CW mode.
+1. `hue`. Sets color. Should be in the range `[0, 359]`.
+1. `level`. Controls brightness. Should be in the range `[0, 100]`.
+1. `temperature`. Controls white temperature. Should be in the range `[0, 100]`.
+1. `saturation`. Controls saturation.
+1. `mode`. Sets "disco mode" setting to the specified value. Note that not all bulbs that have modes support this command. Some will only allow you to cycle through next/previous modes using commands.
+1. `command`. Sends a command to the group. Can be one of:
+   * `set_white`. Turns off RGB and enters WW/CW mode.
    * `pair`. Emulates the pairing process. Send this command right as you connect an unpaired bulb and it will pair with the device ID being used.
    * `unpair`. Emulates the unpairing process. Send as you connect a paired bulb to have it disassociate with the device ID being used.
+   * `next_mode`. Cycles to the next "disco mode".
+   * `previous_mode`. Cycles to the previous disco mode.
+   * `mode_speed_up`. 
+   * `mode_speed_down`.
+   * `level_down`. Turns down the brightness. Not all dimmable bulbs support this command.
+   * `level_up`. Turns down the brightness. Not all dimmable bulbs support this command.
+   * `temperature_down`. Turns down the white temperature. Not all bulbs with adjustable white temperature support this command.
+   * `temperature_up`. Turns up the white temperature. Not all bulbs with adjustable white temperature support this command.
    
-Route (6) suports the `command`s `all_on` and `all_off`, which do as you'd expect.
-
+If you'd like to control bulbs in all groups paired with a particular device ID, set `:group_id` to 0.
 
 #### Examples
 

+ 334 - 2
data/web/index.html

@@ -25,6 +25,7 @@
     .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: '('; }
@@ -62,6 +63,53 @@
       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); }
+    }
   </style>
 </head>
 
@@ -76,9 +124,18 @@
   <script lang="text/javascript">
     var FORM_SETTINGS = [
       "admin_username", "admin_password", "ce_pin", "csn_pin", "packet_repeats",
-      "http_repeat_factor"
+      "http_repeat_factor", "auto_restart_period"
     ];
     
+    var FORM_SETTINGS_HELP = {
+      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."
+    }
+    
     var UDP_PROTOCOL_VERSIONS = [ 5, 6 ];
     var DEFAULT_UDP_PROTOCL_VERSION = 5;
     
@@ -90,7 +147,7 @@
   
     var activeUrl = function() {
       var deviceId = $('#deviceId option:selected').val()
-        , groupId = $('#groupId input:checked').data('value')
+        , groupId = $('#groupId .active input').data('value')
         , mode = getCurrentMode();
         
       if (deviceId == "") {
@@ -123,6 +180,20 @@
       1000
     );
     
+    var sendCommand = _.throttle(
+      function(params) {
+        $.ajax(
+          '/system',
+          {
+            method: 'POST',
+            data: JSON.stringify(params),
+            contentType: 'application/json'
+          }
+        );
+      },
+      1000
+    )
+    
     var sniffRequest;
     var sniffing = false;
     var getTraffic = function() {
@@ -263,6 +334,105 @@
       });
     };
     
+    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) {
+            console.log(releaseAsset[0].url);
+            $('#firmware-link').prop('href', releaseAsset[0].browser_download_url);
+          }
+        }
+        
+        console.log(latestRelease);
+      }
+      
+      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 = JSON.parse(data);
+            handleReceiveData();
+          },
+          failure: handleError
+        }
+      );
+      
+      $.ajax(
+        '/latest_release',
+        {
+          success: function(data) {
+            latestRelease = data;
+            handleReceiveData();
+          },
+          failure: handleError
+        }
+      );
+    };
+    
     $(function() {
       $('.radio-option').click(function() {
         $(this).prev().prop('checked', true);
@@ -314,6 +484,10 @@
         updateGroup({command: $(this).data('command')});
       });
       
+      $('.system-btn').click(function() {
+        sendCommand({command: $(this).data('command')});
+      });
+      
       $('#sniff').click(function() {
         if (sniffing) {
           sniffRequest.abort();
@@ -367,8 +541,14 @@
       FORM_SETTINGS.forEach(function(k) {
         var elmt = '<div class="form-entry">';
         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 += '<input type="text" class="form-control" name="' + k + '"/>';
         elmt += '</div>';
+        
         settings += elmt;
       });
         
@@ -407,11 +587,108 @@
         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();
     });
   </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 class="container">
     <div class="row header-row">
       <div class="col-sm-12">
@@ -576,6 +853,37 @@
             </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>
     
@@ -661,6 +969,30 @@
         <pre id="sniffed-traffic"></pre>
       </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" 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>
+      </div>
+    </div>
   </div>
 </body>
 </html>

二進制
dist/firmware-d1-mini.bin


二進制
dist/firmware-esp07.bin


二進制
dist/firmware-esp12.bin


二進制
dist/firmware-nodemcuv2.bin


+ 114 - 0
lib/GithubClient/GithubClient.cpp

@@ -0,0 +1,114 @@
+#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);
+}

+ 40 - 0
lib/GithubClient/GithubClient.h

@@ -0,0 +1,40 @@
+#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

+ 6 - 0
lib/Helpers/Size.h

@@ -0,0 +1,6 @@
+#include <Arduino.h>
+
+template<typename T, size_t sz>
+size_t size(T(&)[sz]) {
+    return sz;
+}

+ 17 - 0
lib/MiLight/CctPacketFormatter.h

@@ -6,6 +6,23 @@
 #define CCT_COMMAND_INDEX 4
 #define CCT_INTERVALS 10
 
+enum MiLightCctButton {
+  CCT_ALL_ON            = 0x05,
+  CCT_ALL_OFF           = 0x09,
+  CCT_GROUP_1_ON        = 0x08,
+  CCT_GROUP_1_OFF       = 0x0B,
+  CCT_GROUP_2_ON        = 0x0D,
+  CCT_GROUP_2_OFF       = 0x03,
+  CCT_GROUP_3_ON        = 0x07,
+  CCT_GROUP_3_OFF       = 0x0A,
+  CCT_GROUP_4_ON        = 0x02,
+  CCT_GROUP_4_OFF       = 0x06,
+  CCT_BRIGHTNESS_DOWN   = 0x04,
+  CCT_BRIGHTNESS_UP     = 0x0C,
+  CCT_TEMPERATURE_UP    = 0x0E,
+  CCT_TEMPERATURE_DOWN  = 0x0F
+};
+
 class CctPacketFormatter : public PacketFormatter {
 public:
   CctPacketFormatter()

+ 1 - 61
lib/MiLight/MiLightButtons.h

@@ -3,75 +3,15 @@
 
 enum MiLightRadioType {
   UNKNOWN = 0,
-  RGBW  = 0xB8,
+  RGBW  = 0xB0,
   CCT   = 0x5A,
   RGB_CCT = 0x20,
   RGB = 0xA4
 };
 
-enum MiLightRgbCctCommand {
-  RGB_CCT_ON = 0x01,
-  RGB_CCT_OFF = 0x01,
-  RGB_CCT_MODE_SPEED_UP = 0x01,
-  RGB_CCT_MODE_SPEED_DOWN = 0x01,
-  RGB_CCT_COLOR = 0x02,
-  RGB_CCT_KELVIN = 0x03,
-  RGB_CCT_BRIGHTNESS = 0x04,
-  RGB_CCT_SATURATION = 0x04,
-  RGB_CCT_MODE = 0x05,
-};
-
 enum MiLightStatus { 
   ON = 0, 
   OFF = 1 
 };
 
-enum MiLightRgbwButton {
-  RGBW_ALL_ON            = 0x01,
-  RGBW_ALL_OFF           = 0x02,
-  RGBW_GROUP_1_ON        = 0x03,
-  RGBW_GROUP_1_OFF       = 0x04,
-  RGBW_GROUP_2_ON        = 0x05,
-  RGBW_GROUP_2_OFF       = 0x06,
-  RGBW_GROUP_3_ON        = 0x07,
-  RGBW_GROUP_3_OFF       = 0x08,
-  RGBW_GROUP_4_ON        = 0x09,
-  RGBW_GROUP_4_OFF       = 0x0A,
-  RGBW_SPEED_UP          = 0x0B, 
-  RGBW_SPEED_DOWN        = 0x0C, 
-  RGBW_DISCO_MODE        = 0x0D,
-  RGBW_BRIGHTNESS        = 0x0E,
-  RGBW_COLOR             = 0x0F,
-  RGBW_ALL_MAX_LEVEL     = 0x11,
-  RGBW_ALL_MIN_LEVEL     = 0x12,
-  
-  // These are the only mechanism (that I know of) to disable RGB and set the
-  // color to white.
-  RGBW_GROUP_1_MAX_LEVEL = 0x13,
-  RGBW_GROUP_1_MIN_LEVEL = 0x14,
-  RGBW_GROUP_2_MAX_LEVEL = 0x15,
-  RGBW_GROUP_2_MIN_LEVEL = 0x16,
-  RGBW_GROUP_3_MAX_LEVEL = 0x17,
-  RGBW_GROUP_3_MIN_LEVEL = 0x18,
-  RGBW_GROUP_4_MAX_LEVEL = 0x19,
-  RGBW_GROUP_4_MIN_LEVEL = 0x1A,
-};
-
-enum MiLightCctButton {
-  CCT_ALL_ON            = 0x05,
-  CCT_ALL_OFF           = 0x09,
-  CCT_GROUP_1_ON        = 0x08,
-  CCT_GROUP_1_OFF       = 0x0B,
-  CCT_GROUP_2_ON        = 0x0D,
-  CCT_GROUP_2_OFF       = 0x03,
-  CCT_GROUP_3_ON        = 0x07,
-  CCT_GROUP_3_OFF       = 0x0A,
-  CCT_GROUP_4_ON        = 0x02,
-  CCT_GROUP_4_OFF       = 0x06,
-  CCT_BRIGHTNESS_DOWN   = 0x04,
-  CCT_BRIGHTNESS_UP     = 0x0C,
-  CCT_TEMPERATURE_UP    = 0x0E,
-  CCT_TEMPERATURE_DOWN  = 0x0F
-};
-
 #endif

+ 25 - 1
lib/MiLight/MiLightClient.cpp

@@ -23,7 +23,7 @@ MiLightRadio* MiLightClient::switchRadio(const MiLightRadioType type) {
     formatter = stack->config.packetFormatter;
     return radio;
   } else {
-    Serial.print("MiLightClient - tried to get radio for unknown type: ");
+    Serial.print(F("MiLightClient - tried to get radio for unknown type: "));
     Serial.println(type);
   }
   
@@ -95,6 +95,30 @@ void MiLightClient::updateBrightness(const uint8_t brightness) {
   flushPacket();
 }
     
+void MiLightClient::updateMode(uint8_t mode) {
+  formatter->updateMode(mode);
+  flushPacket();
+}
+
+void MiLightClient::nextMode() {
+  formatter->nextMode();
+  flushPacket();
+}
+
+void MiLightClient::previousMode() {
+  formatter->previousMode();
+  flushPacket();
+}
+
+void MiLightClient::modeSpeedDown() {
+  formatter->modeSpeedDown();
+  flushPacket();
+}
+void MiLightClient::modeSpeedUp() {
+  formatter->modeSpeedUp();
+  flushPacket();
+}
+    
 void MiLightClient::updateStatus(MiLightStatus status, uint8_t groupId) {
   formatter->updateStatus(status, groupId);
   flushPacket();

+ 5 - 0
lib/MiLight/MiLightClient.h

@@ -52,6 +52,11 @@ class MiLightClient {
     void pair();
     void unpair();
     void command(uint8_t command, uint8_t arg);
+    void updateMode(uint8_t mode);
+    void nextMode();
+    void previousMode();
+    void modeSpeedDown();
+    void modeSpeedUp();
     
     // RGBW methods
     void updateHue(const uint16_t hue);

+ 14 - 22
lib/MiLight/PacketFormatter.cpp

@@ -1,5 +1,7 @@
 #include <PacketFormatter.h>
 
+uint8_t* PacketFormatter::PACKET_BUFFER = new uint8_t[PACKET_FORMATTER_BUFFER_SIZE];
+
 PacketStream::PacketStream()
     : packetStream(NULL),
       numPackets(0),
@@ -19,12 +21,11 @@ uint8_t* PacketStream::next() {
 
 PacketFormatter::PacketFormatter(const size_t packetLength, const size_t maxPackets)
   : packetLength(packetLength),
-    packetBuffer(new uint8_t[packetLength * maxPackets]),
     numPackets(0),
     currentPacket(NULL)
 { 
   packetStream.packetLength = packetLength;
-  packetStream.packetStream = packetBuffer;
+  packetStream.packetStream = PACKET_BUFFER;
 }
   
 void PacketFormatter::finalizePacket(uint8_t* packet) { }
@@ -38,6 +39,8 @@ void PacketFormatter::updateBrightness(uint8_t value) { }
 void PacketFormatter::updateMode(uint8_t value) { }
 void PacketFormatter::modeSpeedDown() { }
 void PacketFormatter::modeSpeedUp() { }
+void PacketFormatter::nextMode() { }
+void PacketFormatter::previousMode() { }
 void PacketFormatter::command(uint8_t command, uint8_t arg) { }
 
 void PacketFormatter::updateHue(uint16_t value) { }
@@ -99,37 +102,26 @@ void PacketFormatter::pushPacket() {
     finalizePacket(currentPacket);
   }
   
-  currentPacket = packetBuffer + (numPackets * packetLength);
+  currentPacket = PACKET_BUFFER + (numPackets * packetLength);
   numPackets++;
   initializePacket(currentPacket);
 }
 
 void PacketFormatter::format(uint8_t const* packet, char* buffer) {
   for (int i = 0; i < packetLength; i++) {
-    sprintf(buffer, "%02X ", packet[i]);
+    sprintf_P(buffer, "%02X ", packet[i]);
     buffer += 3;
   }
-  sprintf(buffer, "\n\n");
+  sprintf_P(buffer, "\n\n");
 }
 
 void PacketFormatter::formatV1Packet(uint8_t const* packet, char* buffer) {
-  String format = String("Request type  : %02X\n") 
-    + "Device ID     : %02X%02X\n"
-    + "b1            : %02X\n"
-    + "b2            : %02X\n"
-    + "b3            : %02X\n"
-    + "Sequence Num. : %02X";
-    
-  sprintf(
-    buffer,
-    format.c_str(),
-    packet[0],
-    packet[1], packet[2],
-    packet[3],
-    packet[4],
-    packet[5],
-    packet[6]
-  );
+  buffer += sprintf_P(buffer, "Request type  : %02X\n", packet[0]) ;
+  buffer += sprintf_P(buffer, "Device ID     : %02X%02X\n", packet[1], packet[2]);
+  buffer += sprintf_P(buffer, "b1            : %02X\n", packet[3]);
+  buffer += sprintf_P(buffer, "b2            : %02X\n", packet[4]);
+  buffer += sprintf_P(buffer, "b3            : %02X\n", packet[5]);
+  buffer += sprintf_P(buffer, "Sequence Num. : %02X", packet[6]);
 }
   
 size_t PacketFormatter::getPacketLength() const {

+ 15 - 11
lib/MiLight/PacketFormatter.h

@@ -3,6 +3,8 @@
 #include <functional>
 #include <MiLightButtons.h>
 
+#define PACKET_FORMATTER_BUFFER_SIZE 48
+
 #ifndef _PACKET_FORMATTER_H
 #define _PACKET_FORMATTER_H 
 
@@ -22,36 +24,37 @@ class PacketFormatter {
 public:
   PacketFormatter(const size_t packetLength, const size_t maxPackets = 1);
   
-  ~PacketFormatter() {
-    delete this->packetBuffer;
-  }
-  
   typedef void (PacketFormatter::*StepFunction)();
   
-  // all
   void updateStatus(MiLightStatus status);
   virtual void updateStatus(MiLightStatus status, uint8_t groupId);
-  virtual void updateBrightness(uint8_t value);
   virtual void command(uint8_t command, uint8_t arg);
+  
+  // Mode
   virtual void updateMode(uint8_t value);
   virtual void modeSpeedDown();
   virtual void modeSpeedUp();
+  virtual void nextMode();
+  virtual void previousMode();
+  
   virtual void pair();
   virtual void unpair();
   
-  // rgbw, rgb+cct
+  // Color
   virtual void updateHue(uint16_t value);
   virtual void updateColorRaw(uint8_t value);
   virtual void updateColorWhite();
   
-  // cct 
+  // White temperature
   virtual void increaseTemperature();
   virtual void decreaseTemperature();
+  virtual void updateTemperature(uint8_t value);
+  
+  // Brightness
+  virtual void updateBrightness(uint8_t value);
   virtual void increaseBrightness();
   virtual void decreaseBrightness();
   
-  // rgb+cct
-  virtual void updateTemperature(uint8_t value);
   virtual void updateSaturation(uint8_t value);
   
   virtual void reset();
@@ -70,7 +73,8 @@ public:
   size_t getPacketLength() const;
   
 protected:
-  uint8_t* packetBuffer;
+  static uint8_t* PACKET_BUFFER;
+  
   uint8_t* currentPacket;
   size_t packetLength;
   uint16_t deviceId;

+ 42 - 21
lib/MiLight/RgbCctPacketFormatter.cpp

@@ -1,20 +1,20 @@
 #include <RgbCctPacketFormatter.h>
 
 #define V2_OFFSET(byte, key, jumpStart) ( \
-  V2_OFFSETS[byte-1][key%4] \
+  pgm_read_byte(&V2_OFFSETS[byte-1][key%4]) \
     + \
   ((jumpStart > 0 && key >= jumpStart && key <= jumpStart+0x80) ? 0x80 : 0) \
 )
 
 uint8_t const RgbCctPacketFormatter::V2_OFFSETS[][4] = {
-  { 0x45, 0x1F, 0x14, 0x5C },
-  { 0x2B, 0xC9, 0xE3, 0x11 },
-  { 0xEE, 0xDE, 0x0B, 0xAA },
-  { 0xAF, 0x03, 0x1D, 0xF3 },
-  { 0x1A, 0xE2, 0xF0, 0xD1 },
-  { 0x04, 0xD8, 0x71, 0x42 },
-  { 0xAF, 0x04, 0xDD, 0x07 },
-  { 0xE1, 0x93, 0xB8, 0xE4 }
+  { 0x45, 0x1F, 0x14, 0x5C }, // request type
+  { 0x2B, 0xC9, 0xE3, 0x11 }, // id 1
+  { 0x6D, 0x5F, 0x8A, 0x2B }, // id 2
+  { 0xAF, 0x03, 0x1D, 0xF3 }, // command
+  { 0x1A, 0xE2, 0xF0, 0xD1 }, // argument
+  { 0x04, 0xD8, 0x71, 0x42 }, // sequence
+  { 0xAF, 0x04, 0xDD, 0x07 }, // group
+  { 0x61, 0x13, 0x38, 0x64 }  // checksum
 };
 
 void RgbCctPacketFormatter::initializePacket(uint8_t* packet) {
@@ -48,6 +48,27 @@ void RgbCctPacketFormatter::command(uint8_t command, uint8_t arg) {
 void RgbCctPacketFormatter::updateStatus(MiLightStatus status, uint8_t groupId) {
   command(RGB_CCT_ON, groupId + (status == OFF ? 5 : 0));
 }
+  
+void RgbCctPacketFormatter::modeSpeedDown() {
+  command(RGB_CCT_ON, RGB_CCT_MODE_SPEED_DOWN);
+}
+
+void RgbCctPacketFormatter::modeSpeedUp() {
+  command(RGB_CCT_ON, RGB_CCT_MODE_SPEED_UP);
+}
+
+void RgbCctPacketFormatter::updateMode(uint8_t mode) {
+  lastMode = mode;
+  command(RGB_CCT_MODE, mode);
+}
+
+void RgbCctPacketFormatter::nextMode() {
+  updateMode((lastMode+1)%RGB_CCT_NUM_MODES);
+}
+
+void RgbCctPacketFormatter::previousMode() {
+  updateMode((lastMode-1)%RGB_CCT_NUM_MODES);
+}
 
 void RgbCctPacketFormatter::updateBrightness(uint8_t brightness) {
   command(RGB_CCT_BRIGHTNESS, 0x8F + brightness);
@@ -124,13 +145,13 @@ void RgbCctPacketFormatter::encodeV2Packet(uint8_t *packet) {
     packet[i] = encodeByte(packet[i], 0, key, V2_OFFSET(i, packet[0], V2_OFFSET_JUMP_START));
   }
   
-  packet[8] = encodeByte(sum, 3, key, V2_OFFSET(8, packet[0], 0));
+  packet[8] = encodeByte(sum, 2, key, V2_OFFSET(8, packet[0], 0));
 }
 
 void RgbCctPacketFormatter::format(uint8_t const* packet, char* buffer) {
-  buffer += sprintf(buffer, "Raw packet: ");
+  buffer += sprintf_P(buffer, PSTR("Raw packet: "));
   for (int i = 0; i < packetLength; i++) {
-    buffer += sprintf(buffer, "%02X ", packet[i]);
+    buffer += sprintf_P(buffer, PSTR("%02X "), packet[i]);
   }
   
   uint8_t decodedPacket[packetLength];
@@ -138,13 +159,13 @@ void RgbCctPacketFormatter::format(uint8_t const* packet, char* buffer) {
   
   decodeV2Packet(decodedPacket);
   
-  buffer += sprintf(buffer, "\n\nDecoded:\n");
-  buffer += sprintf(buffer, "Key      : %02X\n", decodedPacket[0]);
-  buffer += sprintf(buffer, "b1       : %02X\n", decodedPacket[1]);
-  buffer += sprintf(buffer, "ID       : %02X%02X\n", decodedPacket[2], decodedPacket[3]);
-  buffer += sprintf(buffer, "Command  : %02X\n", decodedPacket[4]);
-  buffer += sprintf(buffer, "Argument : %02X\n", decodedPacket[5]);
-  buffer += sprintf(buffer, "Sequence : %02X\n", decodedPacket[6]);
-  buffer += sprintf(buffer, "Group    : %02X\n", decodedPacket[7]);
-  buffer += sprintf(buffer, "Checksum : %02X", decodedPacket[8]);
+  buffer += sprintf_P(buffer, PSTR("\n\nDecoded:\n"));
+  buffer += sprintf_P(buffer, PSTR("Key      : %02X\n"), decodedPacket[0]);
+  buffer += sprintf_P(buffer, PSTR("b1       : %02X\n"), decodedPacket[1]);
+  buffer += sprintf_P(buffer, PSTR("ID       : %02X%02X\n"), decodedPacket[2], decodedPacket[3]);
+  buffer += sprintf_P(buffer, PSTR("Command  : %02X\n"), decodedPacket[4]);
+  buffer += sprintf_P(buffer, PSTR("Argument : %02X\n"), decodedPacket[5]);
+  buffer += sprintf_P(buffer, PSTR("Sequence : %02X\n"), decodedPacket[6]);
+  buffer += sprintf_P(buffer, PSTR("Group    : %02X\n"), decodedPacket[7]);
+  buffer += sprintf_P(buffer, PSTR("Checksum : %02X"), decodedPacket[8]);
 }

+ 29 - 2
lib/MiLight/RgbCctPacketFormatter.h

@@ -2,17 +2,34 @@
 
 #define RGB_CCT_COMMAND_INDEX 4
 #define RGB_CCT_ARGUMENT_INDEX 5
+#define RGB_CCT_NUM_MODES 9
 #define V2_OFFSET_JUMP_START 0x54
 
 #ifndef _RGB_CCT_PACKET_FORMATTER_H
 #define _RGB_CCT_PACKET_FORMATTER_H 
 
+enum MiLightRgbCctCommand {
+  RGB_CCT_ON = 0x01,
+  RGB_CCT_OFF = 0x01,
+  RGB_CCT_COLOR = 0x02,
+  RGB_CCT_KELVIN = 0x03,
+  RGB_CCT_BRIGHTNESS = 0x04,
+  RGB_CCT_SATURATION = 0x04,
+  RGB_CCT_MODE = 0x05
+};
+
+enum MiLightRgbCctArguments {
+  RGB_CCT_MODE_SPEED_UP   = 0x0A,
+  RGB_CCT_MODE_SPEED_DOWN = 0x0B
+};
+
 class RgbCctPacketFormatter : public PacketFormatter {
 public:
-  static uint8_t const V2_OFFSETS[][4];
+  static uint8_t const V2_OFFSETS[][4] PROGMEM;
     
   RgbCctPacketFormatter()
-    : PacketFormatter(9)
+    : PacketFormatter(9),
+      lastMode(0)
   { }
   
   virtual void initializePacket(uint8_t* packet);
@@ -28,6 +45,12 @@ public:
   virtual void format(uint8_t const* packet, char* buffer);
   virtual void unpair();
   
+  virtual void modeSpeedDown();
+  virtual void modeSpeedUp();
+  virtual void updateMode(uint8_t mode);
+  virtual void nextMode();
+  virtual void previousMode();
+  
   virtual void finalizePacket(uint8_t* packet);
     
   static void encodeV2Packet(uint8_t* packet);
@@ -35,6 +58,10 @@ public:
   static uint8_t xorKey(uint8_t key);
   static uint8_t encodeByte(uint8_t byte, uint8_t s1, uint8_t xorKey, uint8_t s2);
   static uint8_t decodeByte(uint8_t byte, uint8_t s1, uint8_t xorKey, uint8_t s2);
+
+protected:
+  
+  uint8_t lastMode;
 };
 
 #endif

+ 22 - 6
lib/MiLight/RgbPacketFormatter.cpp

@@ -38,8 +38,8 @@ void RgbPacketFormatter::updateHue(uint16_t value) {
 }
 
 void RgbPacketFormatter::updateColorRaw(uint8_t value) {
-  currentPacket[RGB_COLOR_INDEX] = value;
   command(0, 0);
+  currentPacket[RGB_COLOR_INDEX] = value;
 }
 
 void RgbPacketFormatter::updateBrightness(uint8_t value) {
@@ -58,11 +58,27 @@ void RgbPacketFormatter::increaseBrightness() {
 void RgbPacketFormatter::decreaseBrightness() {
   command(RGB_BRIGHTNESS_DOWN, 0);
 }
+  
+void RgbPacketFormatter::modeSpeedDown() {
+  command(RGB_SPEED_DOWN, 0);
+}
+ 
+void RgbPacketFormatter::modeSpeedUp() {
+  command(RGB_SPEED_UP, 0);
+}
+
+void RgbPacketFormatter::nextMode() {
+  command(RGB_MODE_UP, 0);
+}
+
+void RgbPacketFormatter::previousMode() {
+  command(RGB_MODE_DOWN, 0);
+}
 
 void RgbPacketFormatter::format(uint8_t const* packet, char* buffer) {
-  buffer += sprintf(buffer, "b0       : %02X\n", packet[0]);
-  buffer += sprintf(buffer, "ID       : %02X%02X\n", packet[1], packet[2]);
-  buffer += sprintf(buffer, "Color    : %02X\n", packet[3]);
-  buffer += sprintf(buffer, "Command  : %02X\n", packet[4]);
-  buffer += sprintf(buffer, "Sequence : %02X\n", packet[5]);
+  buffer += sprintf_P(buffer, "b0       : %02X\n", packet[0]);
+  buffer += sprintf_P(buffer, "ID       : %02X%02X\n", packet[1], packet[2]);
+  buffer += sprintf_P(buffer, "Color    : %02X\n", packet[3]);
+  buffer += sprintf_P(buffer, "Command  : %02X\n", packet[4]);
+  buffer += sprintf_P(buffer, "Sequence : %02X\n", packet[5]);
 }

+ 4 - 0
lib/MiLight/RgbPacketFormatter.h

@@ -35,6 +35,10 @@ public:
   virtual void format(uint8_t const* packet, char* buffer);
   virtual void pair();
   virtual void unpair();
+  virtual void modeSpeedDown();
+  virtual void modeSpeedUp();
+  virtual void nextMode();
+  virtual void previousMode();
   
   virtual void initializePacket(uint8_t* packet);
 };

+ 18 - 1
lib/MiLight/RgbwPacketFormatter.cpp

@@ -17,6 +17,23 @@ void RgbwPacketFormatter::unpair() {
   PacketFormatter::updateStatus(ON);
   updateColorWhite();
 }
+  
+void RgbwPacketFormatter::modeSpeedDown() {
+  command(RGBW_SPEED_DOWN, 0);
+}
+
+void RgbwPacketFormatter::modeSpeedUp() {
+  command(RGBW_SPEED_UP, 0);
+}
+
+void RgbwPacketFormatter::nextMode() {
+  command(RGBW_DISCO_MODE, 0);
+}
+
+void RgbwPacketFormatter::updateMode(uint8_t mode) {
+  command(RGBW_DISCO_MODE, 0);
+  currentPacket[0] = RGBW | mode;
+}
 
 void RgbwPacketFormatter::updateStatus(MiLightStatus status, uint8_t groupId) {
   uint8_t button = RGBW_GROUP_1_ON + ((groupId - 1)*2) + status;
@@ -48,8 +65,8 @@ void RgbwPacketFormatter::updateHue(uint16_t value) {
 }
 
 void RgbwPacketFormatter::updateColorRaw(uint8_t value) {
-  currentPacket[RGBW_COLOR_INDEX] = value;
   command(RGBW_COLOR, 0);
+  currentPacket[RGBW_COLOR_INDEX] = value;
 }
 
 void RgbwPacketFormatter::updateColorWhite() {

+ 35 - 0
lib/MiLight/RgbwPacketFormatter.h

@@ -3,6 +3,37 @@
 #ifndef _RGBW_PACKET_FORMATTER_H
 #define _RGBW_PACKET_FORMATTER_H 
 
+enum MiLightRgbwButton {
+  RGBW_ALL_ON            = 0x01,
+  RGBW_ALL_OFF           = 0x02,
+  RGBW_GROUP_1_ON        = 0x03,
+  RGBW_GROUP_1_OFF       = 0x04,
+  RGBW_GROUP_2_ON        = 0x05,
+  RGBW_GROUP_2_OFF       = 0x06,
+  RGBW_GROUP_3_ON        = 0x07,
+  RGBW_GROUP_3_OFF       = 0x08,
+  RGBW_GROUP_4_ON        = 0x09,
+  RGBW_GROUP_4_OFF       = 0x0A,
+  RGBW_SPEED_UP          = 0x0B, 
+  RGBW_SPEED_DOWN        = 0x0C, 
+  RGBW_DISCO_MODE        = 0x0D,
+  RGBW_BRIGHTNESS        = 0x0E,
+  RGBW_COLOR             = 0x0F,
+  RGBW_ALL_MAX_LEVEL     = 0x11,
+  RGBW_ALL_MIN_LEVEL     = 0x12,
+  
+  // These are the only mechanism (that I know of) to disable RGB and set the
+  // color to white.
+  RGBW_GROUP_1_MAX_LEVEL = 0x13,
+  RGBW_GROUP_1_MIN_LEVEL = 0x14,
+  RGBW_GROUP_2_MAX_LEVEL = 0x15,
+  RGBW_GROUP_2_MIN_LEVEL = 0x16,
+  RGBW_GROUP_3_MAX_LEVEL = 0x17,
+  RGBW_GROUP_3_MIN_LEVEL = 0x18,
+  RGBW_GROUP_4_MAX_LEVEL = 0x19,
+  RGBW_GROUP_4_MIN_LEVEL = 0x1A,
+};
+
 #define RGBW_COMMAND_INDEX 5
 #define RGBW_BRIGHTNESS_GROUP_INDEX 4
 #define RGBW_COLOR_INDEX 3
@@ -21,6 +52,10 @@ public:
   virtual void updateColorWhite();
   virtual void format(uint8_t const* packet, char* buffer);
   virtual void unpair();
+  virtual void modeSpeedDown();
+  virtual void modeSpeedUp();
+  virtual void nextMode();
+  virtual void updateMode(uint8_t mode);
   
   virtual void initializePacket(uint8_t* packet);
 };

+ 27 - 2
lib/Settings/Settings.cpp

@@ -2,6 +2,23 @@
 #include <ArduinoJson.h>
 #include <FS.h>
 #include <IntParsing.h>
+#include <algorithm>
+  
+bool Settings::hasAuthSettings() {
+  return adminUsername.length() > 0 && adminPassword.length() > 0;
+}
+
+bool Settings::isAutoRestartEnabled() {
+  return _autoRestartPeriod > 0;
+}
+
+size_t Settings::getAutoRestartPeriod() {
+  if (_autoRestartPeriod == 0) {
+    return 0;
+  }
+  
+  return std::max(_autoRestartPeriod, static_cast<size_t>(MINIMUM_RESTART_PERIOD));
+}
 
 void Settings::deserialize(Settings& settings, String json) {
   DynamicJsonBuffer jsonBuffer;
@@ -35,6 +52,10 @@ void Settings::deserialize(Settings& settings, JsonObject& parsedSettings) {
       settings.httpRepeatFactor = parsedSettings["http_repeat_factor"];
     }
     
+    if (parsedSettings.containsKey("auto_restart_period")) {
+      settings._autoRestartPeriod = parsedSettings["auto_restart_period"];
+    }
+    
     JsonArray& arr = parsedSettings["device_ids"];
     settings.updateDeviceIds(arr);
     
@@ -70,7 +91,7 @@ void Settings::updateGatewayConfigs(JsonArray& arr) {
       if (params.success() && params.size() == 3) {
         this->gatewayConfigs[i] = new GatewayConfig(parseInt<uint16_t>(params[0]), params[1], params[2]);
       } else {
-        Serial.print("Settings - skipped parsing gateway ports settings for element #");
+        Serial.print(F("Settings - skipped parsing gateway ports settings for element #"));
         Serial.println(i);
       }
     }
@@ -97,6 +118,9 @@ void Settings::patch(JsonObject& parsedSettings) {
     if (parsedSettings.containsKey("http_repeat_factor")) {
       this->httpRepeatFactor = parsedSettings["http_repeat_factor"];
     }
+    if (parsedSettings.containsKey("auto_restart_period")) {
+      this->_autoRestartPeriod = parsedSettings["auto_restart_period"];
+    }
     if (parsedSettings.containsKey("device_ids")) {
       JsonArray& arr = parsedSettings["device_ids"];
       updateDeviceIds(arr);
@@ -131,7 +155,7 @@ void Settings::save() {
   File f = SPIFFS.open(SETTINGS_FILE, "w");
   
   if (!f) {
-    Serial.println("Opening settings file failed");
+    Serial.println(F("Opening settings file failed"));
   } else {
     serialize(f);
     f.close();
@@ -148,6 +172,7 @@ void Settings::serialize(Stream& stream, const bool prettyPrint) {
   root["csn_pin"] = this->csnPin;
   root["packet_repeats"] = this->packetRepeats;
   root["http_repeat_factor"] = this->httpRepeatFactor;
+  root["auto_restart_period"] = this->_autoRestartPeriod;
   
   if (this->deviceIds) {
     JsonArray& arr = jsonBuffer.createArray();

+ 22 - 4
lib/Settings/Settings.h

@@ -5,11 +5,25 @@
 #ifndef _SETTINGS_H_INCLUDED
 #define _SETTINGS_H_INCLUDED
 
+#ifndef FIRMWARE_VARIANT
+#define FIRMWARE_VARIANT "unknown"
+#endif
+
+#ifndef MILIGHT_HUB_VERSION
+#define MILIGHT_HUB_VERSION "unknown"
+#endif
+
 #define SETTINGS_FILE  "/config.json"
 #define SETTINGS_TERMINATOR '\0'
 
 #define WEB_INDEX_FILENAME "/web/index.html"
 
+#define MILIGHT_GITHUB_USER "sidoh"
+#define MILIGHT_GITHUB_REPO "esp8266_milight_hub"
+#define MILIGHT_REPO_WEB_PATH "/data/web/index.html"
+
+#define MINIMUM_RESTART_PERIOD 1
+
 class GatewayConfig {
 public:
   GatewayConfig(uint16_t deviceId, uint16_t port, uint8_t protocolVersion) 
@@ -36,7 +50,8 @@ public:
     numDeviceIds(0),
     numGatewayConfigs(0),
     packetRepeats(10),
-    httpRepeatFactor(5)
+    httpRepeatFactor(5),
+    _autoRestartPeriod(0)
   { }
   
   ~Settings() {
@@ -45,9 +60,9 @@ public:
     }
   }
   
-  bool hasAuthSettings() {
-    return adminUsername.length() > 0 && adminPassword.length() > 0;
-  }
+  bool hasAuthSettings();
+  bool isAutoRestartEnabled();
+  size_t getAutoRestartPeriod();
 
   static void deserialize(Settings& settings, String json);
   static void deserialize(Settings& settings, JsonObject& json);
@@ -70,6 +85,9 @@ public:
   size_t numDeviceIds;
   size_t packetRepeats;
   size_t httpRepeatFactor;
+  
+protected:
+  size_t _autoRestartPeriod;
 };
 
 #endif 

+ 1 - 1
lib/Udp/MiLightUdpServer.cpp

@@ -29,7 +29,7 @@ void MiLightUdpServer::handleClient() {
     socket.read(packetBuffer, packetSize);
     
 #ifdef MILIGHT_UDP_DEBUG
-    printf("Handling packet: ");
+    printf("[MiLightUdpServer port %d] - Handling packet: ", port);
     for (size_t i = 0; i < packetSize; i++) {
       printf("%02X ", packetBuffer[i]);
     }

+ 2 - 2
lib/Udp/V5MiLightUdpServer.cpp

@@ -4,7 +4,7 @@ void V5MiLightUdpServer::handlePacket(uint8_t* packet, size_t packetSize) {
   if (packetSize == 2 || packetSize == 3) {
     handleCommand(packet[0], packet[1]);
   } else {
-    Serial.print("V5MilightUdpServer: unexpected packet length. Should always be 2-3, was: ");
+    Serial.print(F("V5MilightUdpServer: unexpected packet length. Should always be 2-3, was: "));
     Serial.println(packetSize);  
   }
 }
@@ -91,7 +91,7 @@ void V5MiLightUdpServer::handleCommand(uint8_t command, uint8_t commandArg) {
         
       default:
         if (!handled) {
-          Serial.print("V5MiLightUdpServer - Unhandled command: ");
+          Serial.print(F("V5MiLightUdpServer - Unhandled command: "));
           Serial.println(command);
         }
     }

+ 49 - 0
lib/Udp/V6CctCommandHandler.cpp

@@ -0,0 +1,49 @@
+#include <V6CctCommandHandler.h>
+
+bool V6CctCommandHandler::handleCommand(
+    MiLightClient* client, 
+    uint16_t deviceId,
+    uint8_t group,
+    uint32_t command,
+    uint32_t commandArg)
+{
+  const uint8_t cmd = command & 0xFF;
+  const uint8_t arg = commandArg >> 24;
+  
+  client->prepare(MilightCctConfig, deviceId, group);
+  
+  if (cmd == V2_CCT_COMMAND_PREFIX) {
+    switch (arg) {
+      case V2_CCT_ON:
+        client->updateStatus(ON);
+        break;
+        
+      case V2_CCT_OFF:
+        client->updateStatus(OFF);
+        break;
+        
+      case V2_CCT_BRIGHTNESS_DOWN:
+        client->decreaseBrightness();
+        break;
+        
+      case V2_CCT_BRIGHTNESS_UP:
+        client->increaseBrightness();
+        break;
+        
+      case V2_CCT_TEMPERATURE_DOWN:
+        client->decreaseTemperature();
+        break;
+        
+      case V2_CCT_TEMPERATURE_UP:
+        client->increaseTemperature();
+        break;
+        
+      default:
+        return false;
+    }
+    
+    return true;
+  }
+  
+  return false;
+}

+ 34 - 0
lib/Udp/V6CctCommandHandler.h

@@ -0,0 +1,34 @@
+#include <V6CommandHandler.h>
+
+#ifndef _V6_CCT_COMMAND_HANDLER_H
+#define _V6_CCT_COMMAND_HANDLER_H 
+
+enum CctCommandIds {
+  V2_CCT_COMMAND_PREFIX   = 0x01,
+  
+  V2_CCT_BRIGHTNESS_UP    = 0x01,
+  V2_CCT_BRIGHTNESS_DOWN  = 0x02,
+  V2_CCT_TEMPERATURE_UP   = 0x03,
+  V2_CCT_TEMPERATURE_DOWN = 0x04,
+  V2_CCT_NIGHT_LIGHT      = 0x06,
+  V2_CCT_ON               = 0x07,
+  V2_CCT_OFF              = 0x08
+};
+
+class V6CctCommandHandler : public V6CommandHandler {
+public:
+  V6CctCommandHandler()
+    : V6CommandHandler(0x0100, MilightCctConfig)
+  { }
+  
+  virtual bool handleCommand(
+    MiLightClient* client, 
+    uint16_t deviceId,
+    uint8_t group,
+    uint32_t command,
+    uint32_t commandArg
+  );
+  
+};
+
+#endif

+ 63 - 0
lib/Udp/V6ComamndHandler.cpp

@@ -0,0 +1,63 @@
+#include <V6CommandHandler.h>
+#include <V6RgbCctCommandHandler.h>
+#include <V6RgbwCommandHandler.h>
+#include <V6RgbCommandHandler.h>
+#include <V6CctCommandHandler.h>
+#include <Size.h>
+
+V6CommandHandler* V6CommandHandler::ALL_HANDLERS[] = {
+  new V6RgbCctCommandHandler() PROGMEM,
+  new V6RgbwCommandHandler() PROGMEM,
+  new V6RgbCommandHandler() PROGMEM,
+  new V6CctCommandHandler() PROGMEM,
+};
+
+const size_t V6CommandHandler::NUM_HANDLERS = size(ALL_HANDLERS);
+  
+bool V6CommandHandler::handleCommand(MiLightClient* client, 
+  uint16_t deviceId,
+  uint8_t group,
+  uint8_t commandType,
+  uint32_t command,
+  uint32_t commandArg) 
+{
+  client->prepare(radioConfig, deviceId, group);
+  
+  if (commandType == V6_PAIR) {
+    client->pair();
+  } else if (commandType == V6_UNPAIR) {
+    client->unpair();
+  } else if (commandType == V6_COMMAND) {
+    return this->handleCommand(client, deviceId, group, command, commandArg);
+  } else {
+    return false;
+  }
+  
+  return true;
+}
+
+bool V6CommandDemuxer::handleCommand(MiLightClient* client, 
+  uint16_t deviceId,
+  uint8_t group,
+  uint8_t commandType,
+  uint32_t command,
+  uint32_t commandArg)
+{
+  for (size_t i = 0; i < numHandlers; i++) {
+    if (((handlers[i]->commandId & command) == handlers[i]->commandId)
+      && handlers[i]->handleCommand(client, deviceId, group, commandType, command, commandArg)) {
+      return true;
+    }
+  }
+  
+  return false;
+}
+
+bool V6CommandDemuxer::handleCommand(MiLightClient* client, 
+  uint16_t deviceId,
+  uint8_t group,
+  uint32_t command,
+  uint32_t commandArg)
+{
+  return false;
+}

+ 76 - 0
lib/Udp/V6CommandHandler.h

@@ -0,0 +1,76 @@
+#include <MiLightClient.h>
+#include <MiLightRadioConfig.h>
+
+#ifndef _V6_COMMAND_HANDLER_H
+#define _V6_COMMAND_HANDLER_H 
+
+enum V6CommandTypes {
+  V6_PAIR = 0x3D,
+  V6_UNPAIR = 0x3E,
+  V6_COMMAND = 0x31
+};
+
+class V6CommandHandler {
+public:
+  static V6CommandHandler* ALL_HANDLERS[] PROGMEM;
+  static const size_t NUM_HANDLERS;
+  
+  V6CommandHandler(uint16_t commandId, MiLightRadioConfig& radioConfig)
+    : commandId(commandId),
+      radioConfig(radioConfig)
+  { }
+  
+  virtual bool handleCommand(
+    MiLightClient* client, 
+    uint16_t deviceId,
+    uint8_t group,
+    uint8_t commandType,
+    uint32_t command,
+    uint32_t commandArg
+  );
+  
+  const uint16_t commandId;
+  MiLightRadioConfig& radioConfig;
+  
+protected:
+  
+  virtual bool handleCommand(
+    MiLightClient* client, 
+    uint16_t deviceId,
+    uint8_t group,
+    uint32_t command,
+    uint32_t commandArg
+  ) = 0;
+};
+
+class V6CommandDemuxer : public V6CommandHandler {
+public:
+  V6CommandDemuxer(V6CommandHandler* handlers[], size_t numHandlers)
+    : V6CommandHandler(0, MilightRgbwConfig),  
+      handlers(handlers),
+      numHandlers(numHandlers)
+  { }
+  
+  virtual bool handleCommand(
+    MiLightClient* client, 
+    uint16_t deviceId,
+    uint8_t group,
+    uint8_t commandType,
+    uint32_t command,
+    uint32_t commandArg
+  );
+  
+protected:
+  V6CommandHandler** handlers;
+  size_t numHandlers;
+  
+  virtual bool handleCommand(
+    MiLightClient* client, 
+    uint16_t deviceId,
+    uint8_t group,
+    uint32_t command,
+    uint32_t commandArg
+  );
+};
+
+#endif

+ 95 - 62
lib/Udp/V6MiLightUdpServer.cpp

@@ -1,6 +1,17 @@
 #include <V6MiLightUdpServer.h>
 #include <ESP8266WiFi.h>
 #include <Arduino.h>
+#include <Size.h>
+#include <V6CommandHandler.h>
+
+#define MATCHES_PACKET(packet1) ( \
+  matchesPacket(packet1, size(packet1), packet, packetSize) \
+)
+
+V6CommandDemuxer V6MiLightUdpServer::COMMAND_DEMUXER = V6CommandDemuxer(
+  V6CommandHandler::ALL_HANDLERS,
+  V6CommandHandler::NUM_HANDLERS
+);
   
 uint8_t V6MiLightUdpServer::START_SESSION_COMMAND[] = {
   0x20, 0x00, 0x00, 0x00, 0x16, 0x02, 0x62, 0x3A, 0xD5, 0xED, 0xA3, 0x01, 0xAE, 
@@ -23,14 +34,45 @@ uint8_t V6MiLightUdpServer::HEARTBEAT_HEADER[] = {
   0xD0, 0x00, 0x00, 0x00, 0x02
 };
 
+uint8_t V6MiLightUdpServer::HEARTBEAT_HEADER2[] = {
+  0x30, 0x00, 0x00, 0x00, 0x03
+};
+
 uint8_t V6MiLightUdpServer::COMMAND_RESPONSE[] = {
   0x88, 0x00, 0x00, 0x00, 0x03, 0x00, 0xFF, 0x00
 };
 
-template<typename T, size_t sz>
-size_t size(T(&)[sz]) {
-    return sz;
-}
+uint8_t V6MiLightUdpServer::SEARCH_COMMAND[] = {
+  0x10, 0x00, 0x00, 0x00
+  //, 0x24, 0x02
+  //, 0xAE, 0x65, 0x02, 0x39, 0x38, 0x35, 0x62
+};
+
+uint8_t V6MiLightUdpServer::SEARCH_RESPONSE[] = {
+  0x18, 0x00, 0x00, 0x00, 0x40, 0x02, 
+  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // mac address
+  0x00, 0x20, 0x39, 0x38, 0x35, 0x62, 
+  0x31, 0x35, 0x37, 0x62, 0x66, 0x36, 
+  0x66, 0x63, 0x34, 0x33, 0x33, 0x36, 
+  0x38, 0x61, 0x36, 0x33, 0x34, 0x36, 
+  0x37, 0x65, 0x61, 0x33, 0x62, 0x31, 
+  0x39, 0x64, 0x30, 0x64, 0x01, 0x00, 
+  0x01, 
+  0x17, 0x63,  // this is 5987 in hex. specifying a different value seems to 
+               // cause client to connect on a different port for some commands
+  0x00, 0xFF,
+  0x00, 0x00, 0x05, 0x00, 0x09, 0x78, 
+  0x6C, 0x69, 0x6E, 0x6B, 0x5F, 0x64, 
+  0x65, 0x76, 0x07, 0x5B, 0xCD, 0x15
+};
+
+uint8_t V6MiLightUdpServer::OPEN_COMMAND_RESPONSE[] = {
+  0x80, 0x00, 0x00, 0x00, 0x15,
+  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // mac address
+  0x05, 0x02, 0x00, 0x34, 0x00, 0x00,
+  0x00, 0x00 ,0x00 ,0x00, 0x00, 0x00, 
+  0x00, 0x00, 0x34
+};
 
 V6MiLightUdpServer::~V6MiLightUdpServer() {
   V6Session* cur = firstSession;
@@ -88,6 +130,17 @@ uint16_t V6MiLightUdpServer::beginSession() {
   return id;
 }
 
+void V6MiLightUdpServer::handleSearch() {
+  const size_t packetLen = size(SEARCH_RESPONSE);
+  uint8_t response[packetLen];
+  memcpy(response, SEARCH_RESPONSE, packetLen);
+  WiFi.macAddress(response + 6);
+  
+  socket.beginPacket(socket.remoteIP(), socket.remotePort());
+  socket.write(response, packetLen);
+  socket.endPacket();
+}
+
 void V6MiLightUdpServer::handleStartSession() {
   size_t len = size(START_SESSION_RESPONSE);
   uint8_t response[len];
@@ -101,7 +154,7 @@ void V6MiLightUdpServer::handleStartSession() {
   sendResponse(sessionId, response, len);
 }
   
-void V6MiLightUdpServer::sendResponse(uint16_t sessionId, uint8_t* responseBuffer, size_t responseSize) {
+bool V6MiLightUdpServer::sendResponse(uint16_t sessionId, uint8_t* responseBuffer, size_t responseSize) {
   V6Session* session = firstSession;
   
   while (session != NULL) {
@@ -114,60 +167,27 @@ void V6MiLightUdpServer::sendResponse(uint16_t sessionId, uint8_t* responseBuffe
   if (session == NULL || session->sessionId != sessionId) {
     Serial.print("Received request with untracked session ID: ");
     Serial.println(sessionId);
-    return;
+    return false;
   }
   
 #ifdef MILIGHT_UDP_DEBUG
-  printf("Sending response to %s:%d\n", session->ipAddr.toString().c_str(), session->port);
+  printf_P("Sending response to %s:%d\n", session->ipAddr.toString().c_str(), session->port);
 #endif
   
   socket.beginPacket(session->ipAddr, session->port);
   socket.write(responseBuffer, responseSize);
   socket.endPacket();
+  
+  return true;
 }
 
-bool V6MiLightUdpServer::handleV1BulbCommand(uint8_t group, uint32_t _cmd, uint32_t _arg) {
-  // Makes more sense to use V5 protocol for now.
-}
-
-bool V6MiLightUdpServer::handleV2BulbCommand(uint8_t group, uint32_t _cmd, uint32_t _arg) {
-  const uint8_t cmd = _cmd & 0xFF;
-  const uint8_t arg = _arg >> 24;
-  
-  client->prepare(MilightRgbCctConfig, deviceId, group);
-  
-  switch (cmd) {
-    case V2_STATUS:
-      if (arg == 0x01) {
-        client->updateStatus(ON);
-      } else if (arg == 0x02) {
-        client->updateStatus(OFF);
-      } else if (arg == 0x05) {
-        client->updateBrightness(0);
-      }
-      break;
-      
-    case V2_COLOR:
-      client->updateColorRaw(arg);
-      break;
-      
-    case V2_KELVIN:
-      client->updateTemperature(arg);
-      break;
-      
-    case V2_BRIGHTNESS:
-      client->updateBrightness(arg);
-      break;
-      
-    case V2_SATURATION:
-      client->updateSaturation(100 - arg);
-      break;
-      
-    default:
-      return false;
-  }
+bool V6MiLightUdpServer::handleOpenCommand(uint16_t sessionId) {
+  size_t len = size(OPEN_COMMAND_RESPONSE);
+  uint8_t response[len];
+  memcpy(response, OPEN_COMMAND_RESPONSE, len);
+  WiFi.macAddress(response + 5);
   
-  return true;
+  return sendResponse(sessionId, response, len);
 }
   
 void V6MiLightUdpServer::handleCommand(
@@ -183,15 +203,22 @@ void V6MiLightUdpServer::handleCommand(
   uint32_t cmdArg = readInt<uint32_t>(cmd+5);
   
 #ifdef MILIGHT_UDP_DEBUG
-  printf("Command type: %02X, command: %08X, arg: %08X\n", cmdType, cmdHeader, cmdArg);
+  printf_P("Command cmdType: %02X, cmdHeader: %08X, cmdArg: %08X\n", cmdType, cmdHeader, cmdArg);
 #endif
   
   bool handled = false;
   
-  if ((cmdHeader & 0x0800) == 0x0800) {
-    handled = handleV2BulbCommand(group, cmdHeader, cmdArg);
-  } else if ((cmdHeader & 0x0700) == 0x0700) {
-    handled = handleV1BulbCommand(group, cmdHeader, cmdArg);
+  if (cmdHeader == 0) {
+    handled = handleOpenCommand(sessionId);
+  } else {
+    handled = COMMAND_DEMUXER.handleCommand(
+      client,
+      deviceId,
+      group,
+      cmdType,
+      cmdHeader,
+      cmdArg
+    );
   }
   
   if (handled) {
@@ -205,11 +232,11 @@ void V6MiLightUdpServer::handleCommand(
   }
   
 #ifdef MILIGHT_UDP_DEBUG
-  printf("V6MiLightUdpServer - Unhandled command: ");
+  printf_P("V6MiLightUdpServer - Unhandled command: ");
   for (size_t i = 0; i < V6_COMMAND_LEN; i++) {
-    printf("%02X ", cmd[i]);
+    printf_P("%02X ", cmd[i]);
   }
-  printf("\n");
+  printf_P("\n");
 #endif
 }
 
@@ -221,18 +248,24 @@ void V6MiLightUdpServer::handleHeartbeat(uint16_t sessionId) {
   
   sendResponse(sessionId, responseBuffer, 12);
 }
+  
+bool V6MiLightUdpServer::matchesPacket(uint8_t* packet1, size_t packet1Len, uint8_t* packet2, size_t packet2Len) {
+  return packet2Len >= packet1Len && memcmp(packet1, packet2, packet1Len) == 0;
+}
 
 void V6MiLightUdpServer::handlePacket(uint8_t* packet, size_t packetSize) {
 #ifdef MILIGHT_UDP_DEBUG
-  printf("Packet size: %d\n", packetSize);
+  printf_P("Packet size: %d\n", packetSize);
 #endif
   
-  if (packetSize >= size(START_SESSION_COMMAND) && memcmp(START_SESSION_COMMAND, packet, size(START_SESSION_COMMAND)) == 0) {
+  if (MATCHES_PACKET(START_SESSION_COMMAND)) {
     handleStartSession();
-  } else if (packetSize >= size(HEARTBEAT_HEADER) && memcmp(HEARTBEAT_HEADER, packet, size(HEARTBEAT_HEADER)) == 0) {
+  } else if (MATCHES_PACKET(HEARTBEAT_HEADER) || MATCHES_PACKET(HEARTBEAT_HEADER2)) {
     uint16_t sessionId = readInt<uint16_t>(packet+5);
     handleHeartbeat(sessionId);
-  } else if (packetSize == 22 && memcmp(COMMAND_HEADER, packet, size(COMMAND_HEADER)) == 0) {
+  } else if (MATCHES_PACKET(SEARCH_COMMAND)) {
+    handleSearch();
+  } else if (packetSize == 22 && MATCHES_PACKET(COMMAND_HEADER)) {
     uint16_t sessionId = readInt<uint16_t>(packet+5);
     uint8_t sequenceNum = packet[8];
     uint8_t* cmd = packet+10;
@@ -240,11 +273,11 @@ void V6MiLightUdpServer::handlePacket(uint8_t* packet, size_t packetSize) {
     uint8_t checksum = packet[21];
     
 #ifdef MILIGHT_UDP_DEBUG
-    printf("session: %04X, sequence: %d, group: %d, checksum: %d\n", sessionId, sequenceNum, group, checksum);
+    printf_P("session: %04X, sequence: %d, group: %d, checksum: %d\n", sessionId, sequenceNum, group, checksum);
 #endif
     
     handleCommand(sessionId, sequenceNum, cmd, group, checksum);
   } else {
-    Serial.println("Unhandled V6 packet");
+    Serial.println(F("Unhandled V6 packet"));
   }
 }

+ 20 - 28
lib/Udp/V6MiLightUdpServer.h

@@ -6,6 +6,7 @@
 #include <WiFiUdp.h>
 #include <MiLightUdpServer.h>
 #include <Vector.h>
+#include <V6CommandHandler.h>
 
 #define V6_COMMAND_LEN 8
 #define V6_MAX_SESSIONS 10
@@ -13,14 +14,6 @@
 #ifndef _V6_MILIGHT_UDP_SERVER
 #define _V6_MILIGHT_UDP_SERVER 
 
-enum V2CommandIds {
-  V2_COLOR = 0x01,
-  V2_SATURATION = 0x02,
-  V2_BRIGHTNESS = 0x03,
-  V2_STATUS = 0x04,
-  V2_KELVIN = 0x05
-};
-
 struct V6Session {
   V6Session(IPAddress ipAddr, uint16_t port, uint16_t sessionId)
     : ipAddr(ipAddr),
@@ -56,22 +49,33 @@ public:
   static uint8_t* writeInt(const T& value, uint8_t* packet);
     
 protected:
-  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 SEARCH_COMMAND[];
-  static uint8_t LOCAL_SEARCH_COMMAND[];
-  static uint8_t HEARTBEAT_HEADER[];
+  static V6CommandDemuxer COMMAND_DEMUXER PROGMEM;
+  
+  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 SEARCH_COMMAND[] PROGMEM;
+  static uint8_t SEARCH_RESPONSE[] PROGMEM;
+  
+  static uint8_t OPEN_COMMAND_RESPONSE[] PROGMEM;
   
   V6Session* firstSession;
   size_t numSessions;
   uint16_t sessionId;
   
   uint16_t beginSession();
-  void sendResponse(uint16_t sessionId, uint8_t* responseBuffer, size_t responseSize);
+  bool sendResponse(uint16_t sessionId, uint8_t* responseBuffer, size_t responseSize);
+  
+  bool matchesPacket(uint8_t* packet1, size_t packet1Len, uint8_t* packet2, size_t packet2Len);
   
+  void handleSearch();
   void handleStartSession();
+  bool handleOpenCommand(uint16_t sessionId);
   void handleHeartbeat(uint16_t sessionId);
   void handleCommand(
     uint16_t sessionId,
@@ -80,18 +84,6 @@ protected:
     uint8_t group,
     uint8_t checksum
   );
-  
-  bool handleV1BulbCommand(
-    uint8_t group,
-    uint32_t cmd,
-    uint32_t cmdArg
-  );
-  
-  bool handleV2BulbCommand(
-    uint8_t group,
-    uint32_t cmd,
-    uint32_t cmdArg
-  );
 };
 
 #endif

+ 83 - 0
lib/Udp/V6RgbCctCommandHandler.cpp

@@ -0,0 +1,83 @@
+#include <V6RgbCctCommandHandler.h>
+
+bool V6RgbCctCommandHandler::handleCommand(
+    MiLightClient* client, 
+    uint16_t deviceId,
+    uint8_t group,
+    uint32_t command,
+    uint32_t commandArg)
+{
+  const uint8_t cmd = command & 0xFF;
+  const uint8_t arg = commandArg >> 24;
+  
+  client->prepare(MilightRgbCctConfig, deviceId, group);
+  
+  if (cmd == V2_STATUS) {
+    switch (arg) {
+      case V2_RGB_CCT_ON:
+      case V2_RGB_CCT_OFF:
+        client->updateStatus(arg == V2_RGB_CCT_ON ? ON : OFF);
+        break;
+        
+      case V2_RGB_NIGHT_MODE:
+        client->updateBrightness(0);
+        break;
+        
+      case V2_RGB_CCT_SPEED_DOWN:
+        client->modeSpeedDown();
+        break;
+        
+      case V2_RGB_CCT_SPEED_UP:
+        client->modeSpeedUp();
+        break;
+        
+      default: 
+        return false;
+    }
+    
+    return true;
+  }
+  
+  switch (cmd) {
+    case V2_COLOR:
+      handleUpdateColor(client, commandArg);
+      break;
+      
+    case V2_KELVIN:
+      client->updateTemperature(100 - arg);
+      break;
+      
+    case V2_BRIGHTNESS:
+      client->updateBrightness(arg);
+      break;
+      
+    case V2_SATURATION:
+      client->updateSaturation(100 - arg);
+      break;
+      
+    case V2_MODE:
+      client->updateMode(arg-1);
+      break;
+      
+    default:
+      return false;
+  }
+  
+  return true;
+}
+
+/* 
+ * Arguments are 32 bits. Most commands use the first byte, but color arguments
+ * can use all four. Triggered in app when quickly transitioning through colors.
+ */
+void V6RgbCctCommandHandler::handleUpdateColor(MiLightClient *client, uint32_t color) {
+  for (int i = 3; i >= 0; i--) {
+    const uint8_t argValue = (color >> (i*8)) & 0xFF;
+    
+    if (argValue == 0) {
+      return;
+    }
+    
+    client->updateColorRaw(argValue);
+  }
+}

+ 41 - 0
lib/Udp/V6RgbCctCommandHandler.h

@@ -0,0 +1,41 @@
+#include <V6CommandHandler.h>
+
+#ifndef _V6_RGB_CCT_COMMAND_HANDLER_H
+#define _V6_RGB_CCT_COMMAND_HANDLER_H 
+
+enum V2CommandIds {
+  V2_COLOR = 0x01,
+  V2_SATURATION = 0x02,
+  V2_BRIGHTNESS = 0x03,
+  V2_STATUS = 0x04,
+  V2_KELVIN = 0x05,
+  V2_MODE = 0x06
+};
+
+enum V2CommandArgIds {
+  V2_RGB_CCT_ON = 0x01,
+  V2_RGB_CCT_OFF = 0x02,
+  V2_RGB_CCT_SPEED_UP = 0x03,
+  V2_RGB_CCT_SPEED_DOWN = 0x04,
+  V2_RGB_NIGHT_MODE = 0x05
+};
+
+class V6RgbCctCommandHandler : public V6CommandHandler {
+public:
+  V6RgbCctCommandHandler()
+    : V6CommandHandler(0x0800, MilightRgbCctConfig)
+  { }
+  
+  virtual bool handleCommand(
+    MiLightClient* client, 
+    uint16_t deviceId,
+    uint8_t group,
+    uint32_t command,
+    uint32_t commandArg
+  );
+  
+  void handleUpdateColor(MiLightClient* client, uint32_t color);
+  
+};
+
+#endif

+ 60 - 0
lib/Udp/V6RgbCommandHandler.cpp

@@ -0,0 +1,60 @@
+#include <V6RgbCommandHandler.h>
+
+bool V6RgbCommandHandler::handleCommand(
+    MiLightClient* client, 
+    uint16_t deviceId,
+    uint8_t group,
+    uint32_t command,
+    uint32_t commandArg)
+{
+  const uint8_t cmd = command & 0xFF;
+  const uint8_t arg = commandArg >> 24;
+  
+  client->prepare(MilightRgbConfig, deviceId, 0);
+  
+  if (cmd == V2_RGB_COMMAND_PREFIX) {
+    switch (arg) {
+      case V2_RGB_ON:
+        client->updateStatus(ON);
+        break;
+        
+      case V2_RGB_OFF:
+        client->updateStatus(OFF);
+        break;
+        
+      case V2_RGB_BRIGHTNESS_DOWN:
+        client->decreaseBrightness();
+        break;
+        
+      case V2_RGB_BRIGHTNESS_UP:
+        client->increaseBrightness();
+        break;
+        
+      case V2_RGB_MODE_DOWN:
+        client->previousMode();
+        break;
+        
+      case V2_RGB_MODE_UP:
+        client->nextMode();
+        break;
+        
+      case V2_RGB_SPEED_DOWN:
+        client->modeSpeedDown();
+        break;
+        
+      case V2_RGB_SPEED_UP:
+        client->modeSpeedUp();
+        break;
+        
+      default:
+        return false;
+    }
+    
+    return true;
+  } else if (cmd == V2_RGB_COLOR_PREFIX) {
+    client->updateColorRaw(arg);
+    return true;
+  }
+  
+  return false;
+}

+ 35 - 0
lib/Udp/V6RgbCommandHandler.h

@@ -0,0 +1,35 @@
+#include <V6CommandHandler.h>
+
+#ifndef _V6_RGB_COMMAND_HANDLER_H
+#define _V6_RGB_COMMAND_HANDLER_H 
+
+enum RgbCommandIds {
+  V2_RGB_COMMAND_PREFIX  = 0x02,
+  V2_RGB_COLOR_PREFIX    = 0x01,
+  V2_RGB_BRIGHTNESS_DOWN = 0x01,
+  V2_RGB_BRIGHTNESS_UP   = 0x02,
+  V2_RGB_SPEED_DOWN      = 0x03,
+  V2_RGB_SPEED_UP        = 0x04,
+  V2_RGB_MODE_DOWN       = 0x05,
+  V2_RGB_MODE_UP         = 0x06,
+  V2_RGB_ON              = 0x09,
+  V2_RGB_OFF             = 0x0A
+};
+
+class V6RgbCommandHandler : public V6CommandHandler {
+public:
+  V6RgbCommandHandler()
+    : V6CommandHandler(0x0500, MilightRgbConfig)
+  { }
+  
+  virtual bool handleCommand(
+    MiLightClient* client, 
+    uint16_t deviceId,
+    uint8_t group,
+    uint32_t command,
+    uint32_t commandArg
+  );
+  
+};
+
+#endif

+ 59 - 0
lib/Udp/V6RgbwCommandHandler.cpp

@@ -0,0 +1,59 @@
+#include <V6RgbwCommandHandler.h>
+
+bool V6RgbwCommandHandler::handleCommand(
+    MiLightClient* client, 
+    uint16_t deviceId,
+    uint8_t group,
+    uint32_t command,
+    uint32_t commandArg)
+{
+  const uint8_t cmd = command & 0xFF;
+  const uint8_t arg = commandArg >> 24;
+  
+  client->prepare(MilightRgbwConfig, deviceId, 0);
+  
+  if (cmd == V2_RGBW_COMMAND_PREFIX) {
+    switch (arg) {
+      case V2_RGBW_ON:
+        client->updateStatus(ON);
+        break;
+        
+      case V2_RGBW_OFF:
+        client->updateStatus(OFF);
+        break;
+        
+      case V2_RGBW_WHITE_ON:
+        client->updateColorWhite();
+        break;
+        
+      case V2_RGBW_NIGHT_LIGHT:
+        client->updateColorWhite();
+        client->updateBrightness(0);
+        break;
+        
+      case V2_RGBW_SPEED_DOWN:
+        client->modeSpeedDown();
+        break;
+        
+      case V2_RGBW_SPEED_UP:
+        client->modeSpeedUp();
+        break;
+        
+      default:
+        return false;
+    }
+    
+    return true;
+  } else if (cmd == V2_RGBW_COLOR_PREFIX) {
+    client->updateColorRaw(arg);
+    return true;
+  } else if (cmd == V2_RGBW_BRIGHTNESS_PREFIX) {
+    client->updateBrightness(arg);
+    return true;
+  } else if (cmd == V2_RGBW_MODE_PREFIX) {
+    client->updateMode(arg);
+    return true;
+  }
+  
+  return false;
+}

+ 36 - 0
lib/Udp/V6RgbwCommandHandler.h

@@ -0,0 +1,36 @@
+#include <V6CommandHandler.h>
+
+#ifndef _V6_RGBW_COMMAND_HANDLER_H
+#define _V6_RGBW_COMMAND_HANDLER_H 
+
+enum RgbwCommandIds {
+  V2_RGBW_COLOR_PREFIX      = 0x01,
+  V2_RGBW_BRIGHTNESS_PREFIX = 0x02,
+  V2_RGBW_COMMAND_PREFIX    = 0x03,
+  V2_RGBW_MODE_PREFIX       = 0x04,
+  
+  V2_RGBW_ON                = 0x01,
+  V2_RGBW_OFF               = 0x02,
+  V2_RGBW_SPEED_DOWN        = 0x03,
+  V2_RGBW_SPEED_UP          = 0x04,
+  V2_RGBW_WHITE_ON          = 0x05,
+  V2_RGBW_NIGHT_LIGHT       = 0x06
+};
+
+class V6RgbwCommandHandler : public V6CommandHandler {
+public:
+  V6RgbwCommandHandler()
+    : V6CommandHandler(0x0700, MilightRgbwConfig)
+  { }
+  
+  virtual bool handleCommand(
+    MiLightClient* client, 
+    uint16_t deviceId,
+    uint8_t group,
+    uint32_t command,
+    uint32_t commandArg
+  );
+  
+};
+
+#endif

+ 157 - 46
lib/WebServer/MiLightHttpServer.cpp

@@ -4,25 +4,43 @@
 #include <Settings.h>
 #include <MiLightHttpServer.h>
 #include <MiLightRadioConfig.h>
+#include <GithubClient.h>
 
 void MiLightHttpServer::begin() {
   applySettings(settings);
   
-  server.on("/", HTTP_GET, handleServeFile(WEB_INDEX_FILENAME, "text/html"));
+  server.on("/", HTTP_GET, handleServeFile(WEB_INDEX_FILENAME, "text/html", DEFAULT_INDEX_PAGE));
   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(200, "text/plain", "success"); }, handleUpdateFile(SETTINGS_FILE));
   server.on("/radio_configs", HTTP_GET, [this]() { handleGetRadioConfigs(); });
   server.onPattern("/gateway_traffic/:type", HTTP_GET, [this](const UrlTokenBindings* b) { handleListenGateway(b); });
   server.onPattern("/gateways/:device_id/:type/:group_id", HTTP_PUT, [this](const UrlTokenBindings* b) { handleUpdateGroup(b); });
-  server.onPattern("/gateways/:device_id/:type", HTTP_PUT, [this](const UrlTokenBindings* b) { handleUpdateGateway(b); });
-  server.onPattern("/send_raw/:type", HTTP_PUT, [this](const UrlTokenBindings* b) { handleSendRaw(b); });
+  server.onPattern("/raw_commands/:type", HTTP_PUT, [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("/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](){
       server.sendHeader("Connection", "close");
       server.sendHeader("Access-Control-Allow-Origin", "*");
-      server.send(200, "text/plain", (Update.hasError())?"FAIL":"OK");
+      
+      if (Update.hasError()) {
+        server.send_P(
+          500,
+          "text/plain",
+          PSTR("Failed updating firmware. Check serial logs for more information. You may need to re-flash the device.")
+        );
+      } else {
+        server.send_P(
+          200,
+          "text/plain",
+          PSTR("Success. Device will now reboot.")
+        );
+      }
+      
       ESP.restart();
     },
     [this](){
@@ -50,15 +68,108 @@ void MiLightHttpServer::begin() {
   server.begin();
 }
 
+void MiLightHttpServer::handleGetLatestRelease() {
+  GithubClient client = GithubClient::apiClient();
+  String path = GithubClient::buildApiRequest(
+    MILIGHT_GITHUB_USER,
+    MILIGHT_GITHUB_REPO,
+    "/releases/latest"
+  );
+  
+  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);
+}
+
 void MiLightHttpServer::handleClient() {
   server.handleClient();
 }
 
-void MiLightHttpServer::applySettings(Settings& settings) {
-  if (server.authenticationRequired() && !settings.hasAuthSettings()) {
-    server.disableAuthentication();
+void MiLightHttpServer::handleSystemPost() {
+  DynamicJsonBuffer buffer;
+  JsonObject& request = buffer.parse(server.arg("plain"));
+  
+  bool handled = false;
+  
+  if (request.containsKey("command")) {
+    if (request["command"] == "restart") {
+      Serial.println(F("Restarting..."));
+      server.send(200, "text/plain", "true");
+      
+      delay(100);
+      
+      ESP.restart();
+    }
+  }
+  
+  if (handled) {
+    server.send(200, "text/plain", "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) {
+      printf("building url\n");
+      String urlPath = GithubClient::buildRepoPath(
+        MILIGHT_GITHUB_USER,
+        MILIGHT_GITHUB_REPO,
+        MILIGHT_REPO_WEB_PATH
+      );
+      
+      printf("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);
+  }
+}
+
+void MiLightHttpServer::applySettings(Settings& settings) {
+  if (settings.hasAuthSettings()) {
     server.requireAuthentication(settings.adminUsername, settings.adminPassword);
+  } else {
+    server.disableAuthentication();
   }
   
   milightClient->setResendCount(settings.packetRepeats);
@@ -68,6 +179,19 @@ void MiLightHttpServer::onSettingsSaved(SettingsSavedHandler handler) {
   this->settingsSavedHandler = handler;
 }
   
+void MiLightHttpServer::handleAbout() {
+  DynamicJsonBuffer buffer;
+  JsonObject& response = buffer.createObject();
+  
+  response["version"] = MILIGHT_HUB_VERSION;
+  response["variant"] = FIRMWARE_VARIANT;
+  
+  String body;
+  response.printTo(body);
+  
+  server.send(200, "application", body); 
+}
+  
 void MiLightHttpServer::handleGetRadioConfigs() {
   DynamicJsonBuffer buffer;
   JsonArray& arr = buffer.createArray();
@@ -102,7 +226,7 @@ ESP8266WebServer::THandlerFunction MiLightHttpServer::handleServeFile(
 bool MiLightHttpServer::serveFile(const char* file, const char* contentType) {
   if (SPIFFS.exists(file)) {
     File f = SPIFFS.open(file, "r");
-    server.send(200, contentType, f.readString());
+    server.streamFile(f, contentType);
     f.close();
     return true;
   }
@@ -118,7 +242,7 @@ ESP8266WebServer::THandlerFunction MiLightHttpServer::handleUpdateFile(const cha
       updateFile = SPIFFS.open(filename, "w");
     } else if(upload.status == UPLOAD_FILE_WRITE){
       if (updateFile.write(upload.buf, upload.currentSize) != upload.currentSize) {
-        Serial.println("Error updating web file");
+        Serial.println(F("Error updating web file"));
       }
     } else if (upload.status == UPLOAD_FILE_END) {
       updateFile.close();
@@ -173,15 +297,11 @@ void MiLightHttpServer::handleListenGateway(const UrlTokenBindings* bindings) {
   uint8_t packet[config->getPacketLength()];
   milightClient->read(packet);
   
-  String response = "Packet received (";
-  response += String(sizeof(packet)) + " bytes)";
-  response += ":\n";
+  char response[200];
+  char* responseBuffer = response;
   
-  char ppBuffer[200];
-  milightClient->formatPacket(packet, ppBuffer);
-  response += String(ppBuffer);
-  
-  response += "\n\n";
+  responseBuffer += sprintf(responseBuffer, "\nPacket received (%d bytes):\n", sizeof(packet));
+  milightClient->formatPacket(packet, responseBuffer);
   
   server.send(200, "text/plain", response);
 }
@@ -191,7 +311,7 @@ void MiLightHttpServer::handleUpdateGroup(const UrlTokenBindings* urlBindings) {
   JsonObject& request = buffer.parse(server.arg("plain"));
   
   if (!request.success()) {
-    server.send(400, "text/plain", "Invalid JSON");
+    server.send(400, "text/plain", F("Invalid JSON"));
     return;
   }
   
@@ -202,7 +322,6 @@ void MiLightHttpServer::handleUpdateGroup(const UrlTokenBindings* urlBindings) {
   if (config == NULL) {
     String body = "Unknown device type: ";
     body += urlBindings->get("type");
-    
     server.send(400, "text/plain", body);
     return;
   }
@@ -246,6 +365,22 @@ void MiLightHttpServer::handleUpdateGroup(const UrlTokenBindings* urlBindings) {
     if (request["command"] == "temperature_down") {
       milightClient->decreaseTemperature();
     }
+    
+    if (request["command"] == "next_mode") {
+      milightClient->nextMode();
+    }
+    
+    if (request["command"] == "previous_mode") {
+      milightClient->previousMode();
+    }
+    
+    if (request["command"] == "mode_speed_down") {
+      milightClient->modeSpeedDown();
+    }
+    
+    if (request["command"] == "mode_speed_up") {
+      milightClient->modeSpeedUp();
+    }
   }
   
   if (request.containsKey("hue")) {
@@ -264,35 +399,11 @@ void MiLightHttpServer::handleUpdateGroup(const UrlTokenBindings* urlBindings) {
     milightClient->updateSaturation(request["saturation"]);
   }
   
-  milightClient->setResendCount(settings.packetRepeats);
-  
-  server.send(200, "application/json", "true");
-}
-
-void MiLightHttpServer::handleUpdateGateway(const UrlTokenBindings* urlBindings) {
-  DynamicJsonBuffer buffer;
-  JsonObject& request = buffer.parse(server.arg("plain"));
-  
-  const uint16_t deviceId = parseInt<uint16_t>(urlBindings->get("device_id"));
-  MiLightRadioConfig* config = MiLightRadioConfig::fromString(urlBindings->get("type"));
-  
-  if (config == NULL) {
-    String body = "Unknown device type: ";
-    body += urlBindings->get("type");
-    
-    server.send(400, "text/plain", body);
-    return;
+  if (request.containsKey("mode")) {
+    milightClient->updateMode(request["mode"]);
   }
   
-  milightClient->prepare(*config, deviceId, 0);
-  
-  if (request.containsKey("status")) {
-    if (request["status"] == "on") {
-      milightClient->updateStatus(ON);
-    } else if (request["status"] == "off") {
-      milightClient->updateStatus(OFF);
-    }
-  }
+  milightClient->setResendCount(settings.packetRepeats);
   
   server.send(200, "application/json", "true");
 }

+ 9 - 1
lib/WebServer/MiLightHttpServer.h

@@ -5,8 +5,13 @@
 #ifndef _MILIGHT_HTTP_SERVER
 #define _MILIGHT_HTTP_SERVER 
 
+#define MAX_DOWNLOAD_ATTEMPTS 3
+
 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.";
+
 class MiLightHttpServer {
 public:
   MiLightHttpServer(Settings& settings, MiLightClient*& milightClient)
@@ -33,10 +38,13 @@ protected:
   
   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 handleUpdateGateway(const UrlTokenBindings* urlBindings);
+  void handleDownloadUpdate(const UrlTokenBindings* urlBindings);
   
   File updateFile;
   

+ 6 - 2
platformio.ini

@@ -16,6 +16,8 @@ lib_deps_external =
   RF24
   WiFiManager
   ArduinoJson
+build_flags = !echo -D MILIGHT_HUB_VERSION=\\\"$(git describe --always)\\\"
+# -D MILIGHT_UDP_DEBUG
 
 [env:nodemcuv2]
 platform = espressif8266
@@ -24,6 +26,7 @@ board = nodemcuv2
 lib_deps =
   ${common.lib_deps_builtin}
   ${common.lib_deps_external}
+build_flags = ${common.build_flags} -D FIRMWARE_VARIANT=\\\"nodemcuv2\\\"
 
 [env:d1_mini]
 platform = espressif8266
@@ -32,12 +35,13 @@ board = d1_mini
 lib_deps =
   ${common.lib_deps_builtin}
   ${common.lib_deps_external}
+build_flags = ${common.build_flags} -D FIRMWARE_VARIANT=\\\"d1_mini\\\"
 
 [env:esp12]
 platform = espressif8266
 board = esp12e
 framework = arduino
-build_flags = -Wl,-Tesp8266.flash.4m1m.ld
+build_flags = ${common.build_flags} -Wl,-Tesp8266.flash.4m1m.ld -D FIRMWARE_VARIANT=\\\"esp12\\\"
 lib_deps =
   ${common.lib_deps_builtin}
   ${common.lib_deps_external}
@@ -46,7 +50,7 @@ lib_deps =
 platform = espressif8266
 board = esp07
 framework = arduino
-build_flags = -Wl,-Tesp8266.flash.1m64.ld
+build_flags = ${common.build_flags} -Wl,-Tesp8266.flash.1m64.ld -D FIRMWARE_VARIANT=\\\"esp07\\\"
 lib_deps =
   ${common.lib_deps_builtin}
   ${common.lib_deps_external}

+ 16 - 1
src/main.cpp

@@ -3,7 +3,9 @@
 #include <ArduinoJson.h>
 #include <stdlib.h>
 #include <FS.h>
+#include <GithubClient.h>
 #include <IntParsing.h>
+#include <Size.h>
 #include <MiLightClient.h>
 #include <MiLightRadioConfig.h>
 #include <MiLightHttpServer.h>
@@ -44,7 +46,7 @@ void initMilightUdpServers() {
     );
     
     if (server == NULL) {
-      Serial.print("Error creating UDP server with protocol version: ");
+      Serial.print(F("Error creating UDP server with protocol version: "));
       Serial.println(config->protocolVersion);
     } else {
       udpServers[i] = server;
@@ -67,6 +69,14 @@ void applySettings() {
   initMilightUdpServers();
 }
 
+bool shouldRestart() {
+  if (! settings.isAutoRestartEnabled()) {
+    return false;
+  }
+  
+  return settings.getAutoRestartPeriod()*60*1000 < millis();
+}
+
 void setup() {
   Serial.begin(9600);
   wifiManager.autoConnect();
@@ -87,4 +97,9 @@ void loop() {
       udpServers[i]->handleClient();
     }
   }
+  
+  if (shouldRestart()) {
+    Serial.println(F("Auto-restart triggered. Restarting..."));
+    ESP.restart();
+  }
 }