Procházet zdrojové kódy

Merge pull request #61 from sidoh/v1.2.0

1.2.0 - LT8900, MQTT, mDNS/SSDP
Chris Mullins před 8 roky
rodič
revize
217ca8fd9b
76 změnil soubory, kde provedl 6214 přidání a 1163 odebrání
  1. 78 8
      README.md
  2. 188 156
      data/web/index.html
  3. 145 0
      lib/ESP8266WebServer/examples/AdvancedWebServer/AdvancedWebServer.ino
  4. 238 0
      lib/ESP8266WebServer/examples/FSBrowser/FSBrowser.ino
  5. binární
      lib/ESP8266WebServer/examples/FSBrowser/data/edit.htm.gz
  6. binární
      lib/ESP8266WebServer/examples/FSBrowser/data/favicon.ico
  7. binární
      lib/ESP8266WebServer/examples/FSBrowser/data/graphs.js.gz
  8. 97 0
      lib/ESP8266WebServer/examples/FSBrowser/data/index.htm
  9. 72 0
      lib/ESP8266WebServer/examples/HelloServer/HelloServer.ino
  10. 40 0
      lib/ESP8266WebServer/examples/HttpBasicAuth/HttpBasicAuth.ino
  11. 269 0
      lib/ESP8266WebServer/examples/SDWebServer/SDWebServer.ino
  12. 674 0
      lib/ESP8266WebServer/examples/SDWebServer/SdRoot/edit/index.htm
  13. 22 0
      lib/ESP8266WebServer/examples/SDWebServer/SdRoot/index.htm
  14. binární
      lib/ESP8266WebServer/examples/SDWebServer/SdRoot/pins.png
  15. 126 0
      lib/ESP8266WebServer/examples/SimpleAuthentification/SimpleAuthentification.ino
  16. 71 0
      lib/ESP8266WebServer/examples/WebUpdate/WebUpdate.ino
  17. 36 0
      lib/ESP8266WebServer/keywords.txt
  18. 9 0
      lib/ESP8266WebServer/library.properties
  19. 534 0
      lib/ESP8266WebServer/src/ESP8266WebServer.cpp
  20. 181 0
      lib/ESP8266WebServer/src/ESP8266WebServer.h
  21. 589 0
      lib/ESP8266WebServer/src/Parsing.cpp
  22. 19 0
      lib/ESP8266WebServer/src/detail/RequestHandler.h
  23. 150 0
      lib/ESP8266WebServer/src/detail/RequestHandlersImpl.h
  24. 51 0
      lib/Helpers/TokenIterator.cpp
  25. 21 0
      lib/Helpers/TokenIterator.h
  26. 35 0
      lib/Helpers/UrlTokenBindings.cpp
  27. 18 0
      lib/Helpers/UrlTokenBindings.h
  28. 142 0
      lib/MQTT/MqttClient.cpp
  29. 35 0
      lib/MQTT/MqttClient.h
  30. 0 36
      lib/MiLight/AbstractPL1167.h
  31. 13 6
      lib/MiLight/CctPacketFormatter.cpp
  32. 8 7
      lib/MiLight/CctPacketFormatter.h
  33. 513 0
      lib/MiLight/LT8900MiLightRadio.cpp
  34. 91 0
      lib/MiLight/LT8900MiLightRadio.h
  35. 195 36
      lib/MiLight/MiLightClient.cpp
  36. 66 79
      lib/MiLight/MiLightClient.h
  37. 13 32
      lib/MiLight/MiLightRadio.h
  38. 32 0
      lib/MiLight/MiLightRadioFactory.cpp
  39. 49 0
      lib/MiLight/MiLightRadioFactory.h
  40. 28 36
      lib/MiLight/MiLightRadio.cpp
  41. 43 0
      lib/MiLight/NRF24MiLightRadio.h
  42. 6 5
      lib/MiLight/PL1167_nRF24.cpp
  43. 2 3
      lib/MiLight/PL1167_nRF24.h
  44. 25 18
      lib/MiLight/PacketFormatter.cpp
  45. 24 20
      lib/MiLight/PacketFormatter.h
  46. 0 34
      lib/MiLight/RadioStack.h
  47. 28 18
      lib/MiLight/RgbCctPacketFormatter.cpp
  48. 10 9
      lib/MiLight/RgbCctPacketFormatter.h
  49. 10 7
      lib/MiLight/RgbPacketFormatter.cpp
  50. 4 4
      lib/MiLight/RgbPacketFormatter.h
  51. 20 9
      lib/MiLight/RgbwPacketFormatter.cpp
  52. 8 7
      lib/MiLight/RgbwPacketFormatter.h
  53. 441 0
      lib/SSDP/New_ESP8266SSDP.cpp
  54. 128 0
      lib/SSDP/New_ESP8266SSDP.h
  55. 78 73
      lib/Settings/Settings.cpp
  56. 26 1
      lib/Settings/Settings.h
  57. 26 16
      lib/Udp/V6CctCommandHandler.cpp
  58. 12 8
      lib/Udp/V6CctCommandHandler.h
  59. 20 13
      lib/Udp/V6ComamndHandler.cpp
  60. 28 19
      lib/Udp/V6CommandHandler.h
  61. 63 56
      lib/Udp/V6MiLightUdpServer.cpp
  62. 14 15
      lib/Udp/V6MiLightUdpServer.h
  63. 48 24
      lib/Udp/V6RgbCctCommandHandler.cpp
  64. 12 8
      lib/Udp/V6RgbCctCommandHandler.h
  65. 23 18
      lib/Udp/V6RgbCommandHandler.cpp
  66. 11 7
      lib/Udp/V6RgbCommandHandler.h
  67. 33 18
      lib/Udp/V6RgbwCommandHandler.cpp
  68. 12 8
      lib/Udp/V6RgbwCommandHandler.h
  69. 0 74
      lib/Vector/Vector.h
  70. 72 104
      lib/WebServer/MiLightHttpServer.cpp
  71. 20 13
      lib/WebServer/MiLightHttpServer.h
  72. 46 69
      lib/WebServer/PatternHandler.cpp
  73. 11 71
      lib/WebServer/PatternHandler.h
  74. 2 2
      lib/WebServer/WebServer.cpp
  75. 6 2
      platformio.ini
  76. 54 14
      src/main.cpp

+ 78 - 8
README.md

@@ -12,17 +12,38 @@ This is a replacement for a Milight/LimitlessLED remote/gateway hosted on an ESP
 3. You can secure the ESP8266 with a username/password, which is more than you can say for the Milight gateway! (The 2.4 GHz protocol is still totally insecure, so this doesn't accomplish much :).
 4. Official hubs connect to remote servers to enable WAN access, and this behavior is not disableable.
 
+## Supported bulbs
+
+Support has been added for the following [bulb types](http://futlight.com/productlist.aspx?typeid=101):
+
+1. RGBW bulbs: FUT014, FUT016, FUT103
+1. Dual-White (CCT) bulbs: FUT019
+1. RGB LED strips: FUT025
+1. RGB + Dual White (RGB+CCT) bulbs: FUT015
+
+Other bulb types might work, but have not been tested. It is also relatively easy to add support for new bulb types.
+
 ## What you'll need
 
 1. An ESP8266. I used a NodeMCU.
-2. A NRF24L01+ module (~$3 on ebay).
+2. A NRF24L01+ module (~$3 on ebay). Alternatively, you can use a LT8900.
 3. Some way to connect the two (7 female/female dupont cables is probably easiest).
 
 ## Installing
 
-#### Connect the NRF24L01+
+#### Connect the NRF24L01+ / LT8900
+
+This project is compatible with both NRF24L01 and LT8900 radios. LT8900 is the same model used in the official MiLight devices. NRF24s are a very common 2.4 GHz radio device, but require software emulation of the LT8900's packet structure. As such, the LT8900 is more performant.
+
+Both modules are SPI devices and should be connected to the standard SPI pins on the ESP8266.
+
+##### NRF24L01+
+
+[This guide](https://www.mysensors.org/build/esp8266_gateway) details how to connect an NRF24 to an ESP8266. I used GPIO 16 for CE and GPIO 15 for CSN. These can be configured later.
 
-This module is an SPI device. [This guide](https://www.mysensors.org/build/esp8266_gateway) details how to connect it. I used GPIO 16 for CE and GPIO 15 for CSN. These can be configured later.
+##### LT8900
+
+Connect SPI pins (CS, SCK, MOSI, MISO) to appropriate SPI pins on the ESP8266. With default settings, connect RST to GPIO 0, and PKT to GPIO 16.
 
 #### Setting up the ESP
 
@@ -44,6 +65,14 @@ This project uses [WiFiManager](https://github.com/tzapu/WiFiManager) to avoid t
 
 When the ESP powers on, you should be able to see a network named "ESPXXXXX", with XXXXX being an identifier for your ESP. Connect to this AP and a window should pop up prompting you to enter WiFi credentials.
 
+#### Get IP Address
+
+Both mDNS and SSDP are supported.
+
+* OS X - you should be able to navigate to http://milight-hub.local.
+* Windows - you should see a device called "ESP8266 MiLight Gateway" show up in your network explorer.
+* Linux users can install [avahi](http://www.avahi.org/) (`sudo apt-get install avahi-daemon` on Ubuntu), and should then be able to navigate to http://milight-hub.local.
+
 #### 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:
@@ -60,8 +89,8 @@ The HTTP endpoints (shown below) will be fully functional at this point. You sho
 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. `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.
+1. `PUT /gateways/:device_id/:device_type/:group_id`. Controls or sends commands to `:group_id` from `:device_id`. Accepts a JSON blob. The schema is documented below in the _Bulb commands_ section.
 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}
@@ -73,9 +102,9 @@ Route (5) supports these commands. Note that each bulb type has support for a di
 
 1. `status`. Toggles on/off. Can be "on", "off", "true", or "false".
 1. `hue`. Sets color. Should be in the range `[0, 359]`.
+1. `saturation`. Controls saturation.
 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.
@@ -83,13 +112,22 @@ Route (5) supports these commands. Note that each bulb type has support for a di
    * `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_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.
-   
+   * `night_mode`. Enable "night mode", which is minimum brightness and bulbs only responding to on/off commands.
+1. `commands`. An array containing any number of the above commands (including repeats).
+
+The following redundant commands are supported for the sake of compatibility with HomeAssistant's [`mqtt_json`](https://home-assistant.io/components/light.mqtt_json/) light platform:
+
+1. `color`. Hash containing RGB color. All keys for r, g, and b should be present. For example, `{"r":255,"g":200,"b":255}`.
+1. `color_temp`. Controls white temperature. Value is in [mireds](https://en.wikipedia.org/wiki/Mired). Milight bulbs are in the range 153-370 mireds (2700K-6500K).
+1. `brightness`. Same as `level` with a range of `[0,255]`.
+1. `state`. Same as `status`.
+
 If you'd like to control bulbs in all groups paired with a particular device ID, set `:group_id` to 0.
 
 #### Examples
@@ -108,8 +146,40 @@ $ curl --data-binary '{"command":"set_white"}' -X PUT http://esp8266/gateways/0x
 true%
 ```
 
+## MQTT
+
+To configure your ESP to integrate with MQTT, fill out the following settings:
+
+1. `mqtt_server`- IP or hostname should work. Specify a port with standard syntax (e.g., "mymqttbroker.com:1884").
+1. `mqtt_topic_pattern` - you can control arbitrary configurations of device ID, device type, and group ID with this. A good default choice is something like `milight/:device_id/:device_type/:group_id`. More detail is provided below.
+1. (optionally) `mqtt_username`
+1. (optionally) `mqtt_password`
+
+#### More detail on `mqtt_topic_pattern`
+
+`mqtt_topic_pattern` leverages single-level wildcards (documented [here](https://mosquitto.org/man/mqtt-7.html)). For example, specifying `milight/:device_id/:device_type/:group_id` will cause the ESP to subscribe to the topic `milight/+/+/+`. It will then interpret the second, third, and fourth tokens in topics it receives messages on as `:device_id`, `:device_type`, and `:group_id`, respectively.
+
+Messages should be JSON objects using exactly the same schema that the REST gateway uses for the `/gateways/:device_id/:device_type/:group_id` endpoint. Documented above in the _Bulb commands_ section.
+
+##### Example:
+
+If `mqtt_topic_pattern` is set to `milight/:device_id/:device_type/:group_id`, you could send the following message to it (the below example uses a ruby MQTT client):
+
+```ruby
+irb(main):001:0> require 'mqtt'
+irb(main):002:0> client = MQTT::Client.new('10.133.8.11',1883)
+irb(main):003:0> client.connect
+irb(main):004:0> client.publish('milight/0x118D/rgb_cct/1', '{"status":"ON","color":{"r":255,"g":200,"b":255},"brightness":100}')
+```
+
+This will instruct the ESP to send messages to RGB+CCT bulbs with device ID `0x118D` in group 1 to turn on, set color to RGB(255,200,255), and brightness to 100.
+
 ## UDP Gateways
 
 You can add an arbitrary number of UDP gateways through the REST API or through the web UI. Each gateway server listens on a port and responds to the standard set of commands supported by the Milight protocol. This should allow you to use one of these with standard Milight integrations (SmartThings, Home Assistant, OpenHAB, etc.).
 
 You can select between versions 5 and 6 of the UDP protocol (documented [here](http://www.limitlessled.com/dev/)). Version 6 has support for the newer RGB+CCT bulbs and also includes response packets, which can theoretically improve reliability. Version 5 has much smaller packets and is probably lower latency.
+
+## Acknowledgements
+
+* @WoodsterDK added support for LT8900 radios.

+ 188 - 156
data/web/index.html

@@ -16,7 +16,7 @@
   <!--[if lt IE 9]>
     <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.js"></script>
   <![endif]-->
-  
+
   <style>
     .header-row { border-bottom: 1px solid #ccc; }
     label { display: block; }
@@ -31,8 +31,8 @@
     .error-info:before { content: '('; }
     .error-info:after { content: ')'; }
     .header-btn { margin: 20px; }
-    .btn-secondary { 
-      background-color: #fff; 
+    .btn-secondary {
+      background-color: #fff;
       border: 1px solid #ccc;
     }
     .inline { display: inline-block; }
@@ -46,41 +46,41 @@
       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%, 
+      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 { 
+    .hue-value-display {
       border: 1px solid #000;
       margin-left: 0.5em;
       width: 2em;
       height: 2em;
       display: inline-block;
     }
-    .plus-minus-group { 
+    .plus-minus-group {
       overflow: auto;
       width: 100%;
       clear: both;
       display: block;
     }
-    .plus-minus-group button:first-of-type { 
+    .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 { 
+    .plus-minus-group button:last-of-type {
       border-bottom-left-radius: 0;
       border-top-left-radius: 0;
       display: block;
     }
-    .plus-minus-group .title { 
+    .plus-minus-group .title {
       border-width: 1px 0;
       border-color: #ccc;
       border-style: solid;
@@ -100,12 +100,12 @@
       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); }
@@ -120,52 +120,60 @@
   <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 lang="text/javascript">
     var FORM_SETTINGS = [
-      "admin_username", "admin_password", "ce_pin", "csn_pin", "packet_repeats",
-      "http_repeat_factor", "auto_restart_period"
+      "admin_username", "admin_password", "ce_pin", "csn_pin", "reset_pin","packet_repeats",
+      "http_repeat_factor", "auto_restart_period", "mqtt_server", "mqtt_topic_pattern",
+      "mqtt_username", "mqtt_password", "radio_interface_type"
     ];
-    
+
     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."
+        "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/:type/:group. See README for further details."
     }
-    
+
     var UDP_PROTOCOL_VERSIONS = [ 5, 6 ];
     var DEFAULT_UDP_PROTOCL_VERSION = 5;
-    
+
     var selectize;
-    
+
     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(
@@ -179,7 +187,7 @@
       },
       1000
     );
-    
+
     var sendCommand = _.throttle(
       function(params) {
         $.ajax(
@@ -193,18 +201,18 @@
       },
       1000
     )
-    
+
     var sniffRequest;
     var sniffing = false;
     var getTraffic = function() {
       var sniffType = $('#sniff-type input:checked').data('value');
-      
+
       sniffRequest = $.get('/gateway_traffic/' + sniffType, function(data) {
         $('#sniffed-traffic').html(data + $('#sniffed-traffic').html());
         getTraffic();
       });
     };
-    
+
     var gatewayServerRow = function(deviceId, port, version) {
       var elmt = '<tr>';
       elmt += '<td>';
@@ -215,17 +223,17 @@
       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 + '" ' 
+        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">';
@@ -235,17 +243,21 @@
       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) {
-            field.val(val[k]);
+            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) {
@@ -253,7 +265,7 @@
           });
           selectize.refreshOptions();
         }
-        
+
         var gatewayForm = $('#gateway-server-configs').html('');
         if (val.gateway_configs) {
           val.gateway_configs.forEach(function(v) {
@@ -262,16 +274,16 @@
         }
       });
     };
-    
+
     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');
@@ -280,10 +292,10 @@
           return val;
         }
       });
-      
+
       var ports = $('input[name="ports[]"]', form).map(function(i, v) {
         var val = $(v).val();
-        
+
         if (isNaN(val)) {
           errors = true;
           $(v).addClass('error');
@@ -292,11 +304,11 @@
           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++) {
@@ -312,7 +324,7 @@
         )
       }
     };
-    
+
     var deviceIdError = function(v) {
       if (!v) {
         $('#device-id-label').removeClass('error');
@@ -321,10 +333,10 @@
         $('#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();
@@ -333,10 +345,10 @@
         }
       });
     };
-    
+
     var parseVersion = function(v) {
       var matches = v.match(/(\d+)\.(\d+)\.(\d+)(-(.*))?/);
-      
+
       return {
         major: matches[1],
         minor: matches[2],
@@ -345,28 +357,28 @@
         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 {
@@ -374,11 +386,11 @@
             }
           });
         }
-        
+
         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!";
@@ -386,30 +398,30 @@
             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',
         {
@@ -420,7 +432,7 @@
           failure: handleError
         }
       );
-      
+
       $.ajax(
         '/latest_release',
         {
@@ -432,26 +444,26 @@
         }
       );
     };
-    
+
     $(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;
@@ -468,26 +480,26 @@
             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) {
           sniffRequest.abort();
@@ -499,17 +511,17 @@
           $(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',
@@ -521,53 +533,73 @@
             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 += '<input type="text" class="form-control" name="' + k + '"/>';
+
         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) {
-          obj[k] = $('#settings input[name="' + k + '"]').val();
+          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) { 
+          function(x) {
             return $(x).data('value')
           }
         );
-        
+
         $.ajax(
           "/settings",
           {
@@ -576,17 +608,17 @@
             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')
@@ -597,14 +629,14 @@
           });
         $(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-->
@@ -620,10 +652,10 @@
             <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, 
+              Firmware with incompatible settings could prevent boots. If this happens,
               reflash the board with USB.
             </p>
           </div>
@@ -637,10 +669,10 @@
           <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-->
@@ -651,29 +683,29 @@
         </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>
@@ -685,10 +717,10 @@
           <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">
@@ -697,9 +729,9 @@
         </h1>
       </div>
     </div>
-    
+
     <div>&nbsp;</div>
-    
+
     <div class="row">
       <div class="col-sm-4">
         <label for="deviceId" id="device-id-label">
@@ -709,11 +741,11 @@
         <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
@@ -733,10 +765,10 @@
           </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
@@ -753,7 +785,7 @@
         </div>
       </div>
     </div>
-    
+
     <div class="row"><div class="col-sm-12">
     <div class="mode-option" data-for="rgbw,rgb_cct,rgb">
       <div class="row">
@@ -761,7 +793,7 @@
           <h5>Hue</h5>
         </div>
       </div>
-      
+
       <div class="row">
         <div class="col-sm-6">
           <span class="hue-picker">
@@ -772,7 +804,7 @@
       </div>
     </div>
     </div></div>
-    
+
     <div class="mode-option" data-for="rgb_cct">
       <div class="row">
         <div class="col-sm-12">
@@ -789,7 +821,7 @@
         </div>
       </div>
     </div>
-          
+
     <div class="mode-option" data-for="cct,rgb_cct">
       <div class="row">
         <div class="col-sm-12">
@@ -806,13 +838,13 @@
         </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"
@@ -822,13 +854,13 @@
         />
       </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">
@@ -886,12 +918,12 @@
         </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>
@@ -899,7 +931,7 @@
         </button>
       </div>
     </div>
-    
+
     <div class="row">
       <div class="col col-sm-12">
         <form id="gateway-server-form">
@@ -918,17 +950,17 @@
         </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">
@@ -936,19 +968,19 @@
         </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 class="btn-group" id="sniff-type" data-toggle="buttons">
           <label class="btn btn-secondary active">
             <input type="radio" name="options" autocomplete="off" data-value="rgbw" checked> RGBW
@@ -963,31 +995,31 @@
             <input type="radio" name="options" autocomplete="off" data-value="rgb"> RGB
           </label>
         </div>
-        
+
         <div> &nbsp; </div>
-        
+
         <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>
@@ -995,4 +1027,4 @@
     </div>
   </div>
 </body>
-</html>
+</html>

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

@@ -0,0 +1,145 @@
+/*
+ * 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);
+}

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

@@ -0,0 +1,238 @@
+/* 
+  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ární
lib/ESP8266WebServer/examples/FSBrowser/data/edit.htm.gz


binární
lib/ESP8266WebServer/examples/FSBrowser/data/favicon.ico


binární
lib/ESP8266WebServer/examples/FSBrowser/data/graphs.js.gz


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

@@ -0,0 +1,97 @@
+<!-- 
+  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>

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

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

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

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

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

@@ -0,0 +1,269 @@
+/*
+  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();
+}

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 674 - 0
lib/ESP8266WebServer/examples/SDWebServer/SdRoot/edit/index.htm


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

@@ -0,0 +1,22 @@
+<!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ární
lib/ESP8266WebServer/examples/SDWebServer/SdRoot/pins.png


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

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

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

@@ -0,0 +1,71 @@
+/*
+  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);
+} 

+ 36 - 0
lib/ESP8266WebServer/keywords.txt

@@ -0,0 +1,36 @@
+#######################################
+# Syntax Coloring Map For Ultrasound
+#######################################
+
+#######################################
+# Datatypes (KEYWORD1)
+#######################################
+
+ESP8266WebServer	KEYWORD1
+HTTPMethod	KEYWORD1
+
+#######################################
+# Methods and Functions (KEYWORD2)
+#######################################
+
+begin	KEYWORD2
+handleClient	KEYWORD2
+on	KEYWORD2
+addHandler	KEYWORD2
+uri	KEYWORD2
+method	KEYWORD2
+client	KEYWORD2
+send	KEYWORD2
+arg	KEYWORD2
+argName	KEYWORD2
+args	KEYWORD2
+hasArg	KEYWORD2
+onNotFound	KEYWORD2
+
+#######################################
+# Constants (LITERAL1)
+#######################################
+
+HTTP_GET	LITERAL1
+HTTP_POST	LITERAL1
+HTTP_ANY	LITERAL1

+ 9 - 0
lib/ESP8266WebServer/library.properties

@@ -0,0 +1,9 @@
+name=ESP8266WebServer
+version=1.0
+author=Ivan Grokhotkov
+maintainer=Ivan Grokhtkov <ivan@esp8266.com>
+sentence=Simple web server library
+paragraph=The library supports HTTP GET and POST requests, provides argument parsing, handles one client at a time.
+category=Communication
+url=
+architectures=esp8266

+ 534 - 0
lib/ESP8266WebServer/src/ESP8266WebServer.cpp

@@ -0,0 +1,534 @@
+/*
+  ESP8266WebServer.cpp - Dead simple web-server.
+  Supports only one simultaneous client, knows how to handle GET and POST.
+
+  Copyright (c) 2014 Ivan Grokhotkov. All rights reserved.
+
+  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
+  Modified 8 May 2015 by Hristo Gochkov (proper post and file upload handling)
+*/
+
+
+#include <Arduino.h>
+#include <libb64/cencode.h>
+#include "WiFiServer.h"
+#include "WiFiClient.h"
+#include "ESP8266WebServer.h"
+#include "FS.h"
+#include "detail/RequestHandlersImpl.h"
+
+//#define DEBUG_ESP_HTTP_SERVER
+#ifdef DEBUG_ESP_PORT
+#define DEBUG_OUTPUT DEBUG_ESP_PORT
+#else
+#define DEBUG_OUTPUT Serial
+#endif
+
+const char * AUTHORIZATION_HEADER = "Authorization";
+
+ESP8266WebServer::ESP8266WebServer(IPAddress addr, int port)
+: _server(addr, port)
+, _currentMethod(HTTP_ANY)
+, _currentHandler(0)
+, _firstHandler(0)
+, _lastHandler(0)
+, _currentArgCount(0)
+, _currentArgs(0)
+, _headerKeysCount(0)
+, _currentHeaders(0)
+, _contentLength(0)
+{
+}
+
+ESP8266WebServer::ESP8266WebServer(int port)
+: _server(port)
+, _currentMethod(HTTP_ANY)
+, _currentHandler(0)
+, _firstHandler(0)
+, _lastHandler(0)
+, _currentArgCount(0)
+, _currentArgs(0)
+, _headerKeysCount(0)
+, _currentHeaders(0)
+, _contentLength(0)
+{
+}
+
+ESP8266WebServer::~ESP8266WebServer() {
+  if (_currentHeaders)
+    delete[]_currentHeaders;
+  _headerKeysCount = 0;
+  RequestHandler* handler = _firstHandler;
+  while (handler) {
+    RequestHandler* next = handler->next();
+    delete handler;
+    handler = next;
+  }
+  close();
+}
+
+void ESP8266WebServer::begin() {
+  _currentStatus = HC_NONE;
+  _server.begin();
+  if(!_headerKeysCount)
+    collectHeaders(0, 0);
+}
+
+bool ESP8266WebServer::authenticate(const char * username, const char * password){
+  if(hasHeader(AUTHORIZATION_HEADER)){
+    String authReq = header(AUTHORIZATION_HEADER);
+    if(authReq.startsWith("Basic")){
+      authReq = authReq.substring(6);
+      authReq.trim();
+      char toencodeLen = strlen(username)+strlen(password)+1;
+      char *toencode = new char[toencodeLen + 1];
+      if(toencode == NULL){
+        authReq = String();
+        return false;
+      }
+      char *encoded = new char[base64_encode_expected_len(toencodeLen)+1];
+      if(encoded == NULL){
+        authReq = String();
+        delete[] toencode;
+        return false;
+      }
+      sprintf(toencode, "%s:%s", username, password);
+      if(base64_encode_chars(toencode, toencodeLen, encoded) > 0 && authReq.equals(encoded)){
+        authReq = String();
+        delete[] toencode;
+        delete[] encoded;
+        return true;
+      }
+      delete[] toencode;
+      delete[] encoded;
+    }
+    authReq = String();
+  }
+  return false;
+}
+
+void ESP8266WebServer::requestAuthentication(){
+  sendHeader("WWW-Authenticate", "Basic realm=\"Login Required\"");
+  send(401);
+}
+
+void ESP8266WebServer::on(const char* uri, ESP8266WebServer::THandlerFunction handler) {
+  on(uri, HTTP_ANY, handler);
+}
+
+void ESP8266WebServer::on(const char* uri, HTTPMethod method, ESP8266WebServer::THandlerFunction fn) {
+  on(uri, method, fn, _fileUploadHandler);
+}
+
+void ESP8266WebServer::on(const char* uri, HTTPMethod method, ESP8266WebServer::THandlerFunction fn, ESP8266WebServer::THandlerFunction ufn) {
+  _addRequestHandler(new FunctionRequestHandler(fn, ufn, uri, method));
+}
+
+void ESP8266WebServer::addHandler(RequestHandler* handler) {
+    _addRequestHandler(handler);
+}
+
+void ESP8266WebServer::_addRequestHandler(RequestHandler* handler) {
+    if (!_lastHandler) {
+      _firstHandler = handler;
+      _lastHandler = handler;
+    }
+    else {
+      _lastHandler->next(handler);
+      _lastHandler = handler;
+    }
+}
+
+void ESP8266WebServer::serveStatic(const char* uri, FS& fs, const char* path, const char* cache_header) {
+    _addRequestHandler(new StaticRequestHandler(fs, path, uri, cache_header));
+}
+
+void ESP8266WebServer::handleClient() {
+  if (_currentStatus == HC_NONE) {
+    WiFiClient client = _server.available();
+    if (!client) {
+      return;
+    }
+
+#ifdef DEBUG_ESP_HTTP_SERVER
+    DEBUG_OUTPUT.println("New client");
+#endif
+
+    _currentClient = client;
+    _currentStatus = HC_WAIT_READ;
+    _statusChange = millis();
+  }
+
+  if (!_currentClient.connected()) {
+    _currentClient = WiFiClient();
+    _currentStatus = HC_NONE;
+    return;
+  }
+
+  // Wait for data from client to become available
+  if (_currentStatus == HC_WAIT_READ) {
+    if (!_currentClient.available()) {
+      if (millis() - _statusChange > HTTP_MAX_DATA_WAIT) {
+        _currentClient = WiFiClient();
+        _currentStatus = HC_NONE;
+      }
+      yield();
+      return;
+    }
+
+    if (!_parseRequest(_currentClient)) {
+      _currentClient = WiFiClient();
+      _currentStatus = HC_NONE;
+      return;
+    }
+
+    _contentLength = CONTENT_LENGTH_NOT_SET;
+    _handleRequest();
+
+    if (!_currentClient.connected()) {
+      _currentClient = WiFiClient();
+      _currentStatus = HC_NONE;
+      return;
+    } else {
+      _currentStatus = HC_WAIT_CLOSE;
+      _statusChange = millis();
+      return;
+    }
+  }
+
+  if (_currentStatus == HC_WAIT_CLOSE) {
+    if (millis() - _statusChange > HTTP_MAX_CLOSE_WAIT) {
+      _currentClient = WiFiClient();
+      _currentStatus = HC_NONE;
+    } else {
+      yield();
+      return;
+    }
+  }
+}
+
+void ESP8266WebServer::close() {
+  _server.close();
+}
+
+void ESP8266WebServer::stop() {
+  close();
+}
+
+void ESP8266WebServer::sendHeader(const String& name, const String& value, bool first) {
+  String headerLine = name;
+  headerLine += ": ";
+  headerLine += value;
+  headerLine += "\r\n";
+
+  if (first) {
+    _responseHeaders = headerLine + _responseHeaders;
+  }
+  else {
+    _responseHeaders += headerLine;
+  }
+}
+
+
+void ESP8266WebServer::_prepareHeader(String& response, int code, const char* content_type, size_t contentLength) {
+    response = "HTTP/1.1 ";
+    response += String(code);
+    response += " ";
+    response += _responseCodeToString(code);
+    response += "\r\n";
+
+    if (!content_type)
+        content_type = "text/html";
+
+    sendHeader("Content-Type", content_type, true);
+    if (_contentLength == CONTENT_LENGTH_NOT_SET) {
+        sendHeader("Content-Length", String(contentLength));
+    } else if (_contentLength != CONTENT_LENGTH_UNKNOWN) {
+        sendHeader("Content-Length", String(_contentLength));
+    }
+    sendHeader("Connection", "close");
+    sendHeader("Access-Control-Allow-Origin", "*");
+
+    response += _responseHeaders;
+    response += "\r\n";
+    _responseHeaders = String();
+}
+
+void ESP8266WebServer::send(int code, const char* content_type, const String& content) {
+    String header;
+    _prepareHeader(header, code, content_type, content.length());
+    sendContent(header);
+
+    sendContent(content);
+}
+
+void ESP8266WebServer::send_P(int code, PGM_P content_type, PGM_P content) {
+    size_t contentLength = 0;
+
+    if (content != NULL) {
+        contentLength = strlen_P(content);
+    }
+
+    String header;
+    char type[64];
+    memccpy_P((void*)type, (PGM_VOID_P)content_type, 0, sizeof(type));
+    _prepareHeader(header, code, (const char* )type, contentLength);
+    sendContent(header);
+    sendContent_P(content);
+}
+
+void ESP8266WebServer::send_P(int code, PGM_P content_type, PGM_P content, size_t contentLength) {
+    String header;
+    char type[64];
+    memccpy_P((void*)type, (PGM_VOID_P)content_type, 0, sizeof(type));
+    _prepareHeader(header, code, (const char* )type, contentLength);
+    sendContent(header);
+    sendContent_P(content, contentLength);
+}
+
+void ESP8266WebServer::send(int code, char* content_type, const String& content) {
+  send(code, (const char*)content_type, content);
+}
+
+void ESP8266WebServer::send(int code, const String& content_type, const String& content) {
+  send(code, (const char*)content_type.c_str(), content);
+}
+
+void ESP8266WebServer::sendContent(const String& content) {
+  const size_t unit_size = HTTP_DOWNLOAD_UNIT_SIZE;
+  size_t size_to_send = content.length();
+  const char* send_start = content.c_str();
+
+  while (size_to_send) {
+    size_t will_send = (size_to_send < unit_size) ? size_to_send : unit_size;
+    size_t sent = _currentClient.write(send_start, will_send);
+    if (sent == 0) {
+      break;
+    }
+    size_to_send -= sent;
+    send_start += sent;
+  }
+}
+
+void ESP8266WebServer::sendContent_P(PGM_P content) {
+    char contentUnit[HTTP_DOWNLOAD_UNIT_SIZE + 1];
+
+    contentUnit[HTTP_DOWNLOAD_UNIT_SIZE] = '\0';
+
+    while (content != NULL) {
+        size_t contentUnitLen;
+        PGM_P contentNext;
+
+        // due to the memccpy signature, lots of casts are needed
+        contentNext = (PGM_P)memccpy_P((void*)contentUnit, (PGM_VOID_P)content, 0, HTTP_DOWNLOAD_UNIT_SIZE);
+
+        if (contentNext == NULL) {
+            // no terminator, more data available
+            content += HTTP_DOWNLOAD_UNIT_SIZE;
+            contentUnitLen = HTTP_DOWNLOAD_UNIT_SIZE;
+        }
+        else {
+            // reached terminator. Do not send the terminator
+            contentUnitLen = contentNext - contentUnit - 1;
+            content = NULL;
+        }
+
+        // write is so overloaded, had to use the cast to get it pick the right one
+        _currentClient.write((const char*)contentUnit, contentUnitLen);
+    }
+}
+
+void ESP8266WebServer::sendContent_P(PGM_P content, size_t size) {
+    char contentUnit[HTTP_DOWNLOAD_UNIT_SIZE + 1];
+    contentUnit[HTTP_DOWNLOAD_UNIT_SIZE] = '\0';
+    size_t remaining_size = size;
+
+    while (content != NULL && remaining_size > 0) {
+        size_t contentUnitLen = HTTP_DOWNLOAD_UNIT_SIZE;
+
+        if (remaining_size < HTTP_DOWNLOAD_UNIT_SIZE) contentUnitLen = remaining_size;
+        // due to the memcpy signature, lots of casts are needed
+        memcpy_P((void*)contentUnit, (PGM_VOID_P)content, contentUnitLen);
+
+        content += contentUnitLen;
+        remaining_size -= contentUnitLen;
+
+        // write is so overloaded, had to use the cast to get it pick the right one
+        _currentClient.write((const char*)contentUnit, contentUnitLen);
+    }
+}
+
+
+String ESP8266WebServer::arg(String name) {
+  for (int i = 0; i < _currentArgCount; ++i) {
+    if ( _currentArgs[i].key == name )
+      return _currentArgs[i].value;
+  }
+  return String();
+}
+
+String ESP8266WebServer::arg(int i) {
+  if (i < _currentArgCount)
+    return _currentArgs[i].value;
+  return String();
+}
+
+String ESP8266WebServer::argName(int i) {
+  if (i < _currentArgCount)
+    return _currentArgs[i].key;
+  return String();
+}
+
+int ESP8266WebServer::args() {
+  return _currentArgCount;
+}
+
+bool ESP8266WebServer::hasArg(String  name) {
+  for (int i = 0; i < _currentArgCount; ++i) {
+    if (_currentArgs[i].key == name)
+      return true;
+  }
+  return false;
+}
+
+
+String ESP8266WebServer::header(String name) {
+  for (int i = 0; i < _headerKeysCount; ++i) {
+    if (_currentHeaders[i].key == name)
+      return _currentHeaders[i].value;
+  }
+  return String();
+}
+
+void ESP8266WebServer::collectHeaders(const char* headerKeys[], const size_t headerKeysCount) {
+  _headerKeysCount = headerKeysCount + 1;
+  if (_currentHeaders)
+     delete[]_currentHeaders;
+  _currentHeaders = new RequestArgument[_headerKeysCount];
+  _currentHeaders[0].key = AUTHORIZATION_HEADER;
+  for (int i = 1; i < _headerKeysCount; i++){
+    _currentHeaders[i].key = headerKeys[i-1];
+  }
+}
+
+String ESP8266WebServer::header(int i) {
+  if (i < _headerKeysCount)
+    return _currentHeaders[i].value;
+  return String();
+}
+
+String ESP8266WebServer::headerName(int i) {
+  if (i < _headerKeysCount)
+    return _currentHeaders[i].key;
+  return String();
+}
+
+int ESP8266WebServer::headers() {
+  return _headerKeysCount;
+}
+
+bool ESP8266WebServer::hasHeader(String name) {
+  for (int i = 0; i < _headerKeysCount; ++i) {
+    if ((_currentHeaders[i].key == name) &&  (_currentHeaders[i].value.length() > 0))
+      return true;
+  }
+  return false;
+}
+
+String ESP8266WebServer::hostHeader() {
+  return _hostHeader;
+}
+
+void ESP8266WebServer::onFileUpload(THandlerFunction fn) {
+  _fileUploadHandler = fn;
+}
+
+void ESP8266WebServer::onNotFound(THandlerFunction fn) {
+  _notFoundHandler = fn;
+}
+
+void ESP8266WebServer::_handleRequest() {
+  bool handled = false;
+  if (!_currentHandler){
+#ifdef DEBUG_ESP_HTTP_SERVER
+    DEBUG_OUTPUT.println("request handler not found");
+#endif
+  }
+  else {
+    handled = _currentHandler->handle(*this, _currentMethod, _currentUri);
+#ifdef DEBUG_ESP_HTTP_SERVER
+    if (!handled) {
+      DEBUG_OUTPUT.println("request handler failed to handle request");
+    }
+#endif
+  }
+
+  if (!handled) {
+    if(_notFoundHandler) {
+      _notFoundHandler();
+    }
+    else {
+      send(404, "text/plain", String("Not found: ") + _currentUri);
+    }
+  }
+
+  _currentUri = String();
+}
+
+String ESP8266WebServer::_responseCodeToString(int code) {
+  switch (code) {
+    case 100: return F("Continue");
+    case 101: return F("Switching Protocols");
+    case 200: return F("OK");
+    case 201: return F("Created");
+    case 202: return F("Accepted");
+    case 203: return F("Non-Authoritative Information");
+    case 204: return F("No Content");
+    case 205: return F("Reset Content");
+    case 206: return F("Partial Content");
+    case 300: return F("Multiple Choices");
+    case 301: return F("Moved Permanently");
+    case 302: return F("Found");
+    case 303: return F("See Other");
+    case 304: return F("Not Modified");
+    case 305: return F("Use Proxy");
+    case 307: return F("Temporary Redirect");
+    case 400: return F("Bad Request");
+    case 401: return F("Unauthorized");
+    case 402: return F("Payment Required");
+    case 403: return F("Forbidden");
+    case 404: return F("Not Found");
+    case 405: return F("Method Not Allowed");
+    case 406: return F("Not Acceptable");
+    case 407: return F("Proxy Authentication Required");
+    case 408: return F("Request Time-out");
+    case 409: return F("Conflict");
+    case 410: return F("Gone");
+    case 411: return F("Length Required");
+    case 412: return F("Precondition Failed");
+    case 413: return F("Request Entity Too Large");
+    case 414: return F("Request-URI Too Large");
+    case 415: return F("Unsupported Media Type");
+    case 416: return F("Requested range not satisfiable");
+    case 417: return F("Expectation Failed");
+    case 500: return F("Internal Server Error");
+    case 501: return F("Not Implemented");
+    case 502: return F("Bad Gateway");
+    case 503: return F("Service Unavailable");
+    case 504: return F("Gateway Time-out");
+    case 505: return F("HTTP Version not supported");
+    default:  return "";
+  }
+}

+ 181 - 0
lib/ESP8266WebServer/src/ESP8266WebServer.h

@@ -0,0 +1,181 @@
+/*
+  ESP8266WebServer.h - Dead simple web-server.
+  Supports only one simultaneous client, knows how to handle GET and POST.
+
+  Copyright (c) 2014 Ivan Grokhotkov. All rights reserved.
+
+  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
+  Modified 8 May 2015 by Hristo Gochkov (proper post and file upload handling)
+*/
+
+
+#ifndef ESP8266WEBSERVER_H
+#define ESP8266WEBSERVER_H
+
+#include <functional>
+#include <ESP8266WiFi.h>
+
+enum HTTPMethod { HTTP_ANY, HTTP_GET, HTTP_POST, HTTP_PUT, HTTP_PATCH, HTTP_DELETE, HTTP_OPTIONS };
+enum HTTPUploadStatus { UPLOAD_FILE_START, UPLOAD_FILE_WRITE, UPLOAD_FILE_END,
+                        UPLOAD_FILE_ABORTED };
+enum HTTPClientStatus { HC_NONE, HC_WAIT_READ, HC_WAIT_CLOSE };
+
+#define HTTP_DOWNLOAD_UNIT_SIZE 1460
+#define HTTP_UPLOAD_BUFLEN 20
+#define HTTP_MAX_DATA_WAIT 1000 //ms to wait for the client to send the request
+#define HTTP_MAX_POST_WAIT 1000 //ms to wait for POST data to arrive
+#define HTTP_MAX_CLOSE_WAIT 2000 //ms to wait for the client to close the connection
+
+#define CONTENT_LENGTH_UNKNOWN ((size_t) -1)
+#define CONTENT_LENGTH_NOT_SET ((size_t) -2)
+
+class ESP8266WebServer;
+
+typedef struct {
+  HTTPUploadStatus status;
+  String  filename;
+  String  name;
+  String  type;
+  size_t  totalSize;    // file size
+  size_t  currentSize;  // size of data currently in buf
+  uint8_t buf[HTTP_UPLOAD_BUFLEN];
+} HTTPUpload;
+
+#include "detail/RequestHandler.h"
+
+namespace fs {
+class FS;
+}
+
+class ESP8266WebServer
+{
+public:
+  ESP8266WebServer(IPAddress addr, int port = 80);
+  ESP8266WebServer(int port = 80);
+  ~ESP8266WebServer();
+
+  void begin();
+  void handleClient();
+
+  void close();
+  void stop();
+
+  bool authenticate(const char * username, const char * password);
+  void requestAuthentication();
+
+  typedef std::function<void(void)> THandlerFunction;
+  void on(const char* uri, THandlerFunction handler);
+  void on(const char* uri, HTTPMethod method, THandlerFunction fn);
+  void on(const char* uri, HTTPMethod method, THandlerFunction fn, THandlerFunction ufn);
+  void addHandler(RequestHandler* handler);
+  void serveStatic(const char* uri, fs::FS& fs, const char* path, const char* cache_header = NULL );
+  void onNotFound(THandlerFunction fn);  //called when handler is not assigned
+  void onFileUpload(THandlerFunction fn); //handle file uploads
+
+  String uri() { return _currentUri; }
+  HTTPMethod method() { return _currentMethod; }
+  WiFiClient client() { return _currentClient; }
+  HTTPUpload& upload() { return _currentUpload; }
+
+  String arg(String name);        // get request argument value by name
+  String arg(int i);              // get request argument value by number
+  String argName(int i);          // get request argument name by number
+  int args();                     // get arguments count
+  bool hasArg(String name);       // check if argument exists
+  void collectHeaders(const char* headerKeys[], const size_t headerKeysCount); // set the request headers to collect
+  String header(String name);      // get request header value by name
+  String header(int i);              // get request header value by number
+  String headerName(int i);          // get request header name by number
+  int headers();                     // get header count
+  bool hasHeader(String name);       // check if header exists
+
+  String hostHeader();            // get request host header if available or empty String if not
+
+  // send response to the client
+  // code - HTTP response code, can be 200 or 404
+  // content_type - HTTP content type, like "text/plain" or "image/png"
+  // content - actual content body
+  void send(int code, const char* content_type = NULL, const String& content = String(""));
+  void send(int code, char* content_type, const String& content);
+  void send(int code, const String& content_type, const String& content);
+  void send_P(int code, PGM_P content_type, PGM_P content);
+  void send_P(int code, PGM_P content_type, PGM_P content, size_t contentLength);
+
+  void setContentLength(size_t contentLength) { _contentLength = contentLength; }
+  void sendHeader(const String& name, const String& value, bool first = false);
+  void sendContent(const String& content);
+  void sendContent_P(PGM_P content);
+  void sendContent_P(PGM_P content, size_t size);
+
+  static String urlDecode(const String& text);
+
+template<typename T> size_t streamFile(T &file, const String& contentType){
+  setContentLength(file.size());
+  if (String(file.name()).endsWith(".gz") &&
+      contentType != "application/x-gzip" &&
+      contentType != "application/octet-stream"){
+    sendHeader("Content-Encoding", "gzip");
+  }
+  send(200, contentType, "");
+  return _currentClient.write(file, HTTP_DOWNLOAD_UNIT_SIZE);
+}
+
+protected:
+  void _addRequestHandler(RequestHandler* handler);
+  void _handleRequest();
+  bool _parseRequest(WiFiClient& client);
+  void _parseArguments(String data);
+  static String _responseCodeToString(int code);
+  bool _parseForm(WiFiClient& client, String boundary, uint32_t len);
+  bool _parseFormUploadAborted();
+  void _uploadWriteByte(uint8_t b);
+  uint8_t _uploadReadByte(WiFiClient& client);
+  void _prepareHeader(String& response, int code, const char* content_type, size_t contentLength);
+  bool _collectHeader(const char* headerName, const char* headerValue);
+
+  struct RequestArgument {
+    String key;
+    String value;
+  };
+
+  WiFiServer  _server;
+
+  WiFiClient  _currentClient;
+  HTTPMethod  _currentMethod;
+  String      _currentUri;
+  HTTPClientStatus _currentStatus;
+  unsigned long _statusChange;
+
+  RequestHandler*  _currentHandler;
+  RequestHandler*  _firstHandler;
+  RequestHandler*  _lastHandler;
+  THandlerFunction _notFoundHandler;
+  THandlerFunction _fileUploadHandler;
+
+  int              _currentArgCount;
+  RequestArgument* _currentArgs;
+  HTTPUpload       _currentUpload;
+
+  int              _headerKeysCount;
+  RequestArgument* _currentHeaders;
+  size_t           _contentLength;
+  String           _responseHeaders;
+
+  String           _hostHeader;
+
+};
+
+
+#endif //ESP8266WEBSERVER_H

+ 589 - 0
lib/ESP8266WebServer/src/Parsing.cpp

@@ -0,0 +1,589 @@
+/*
+  Parsing.cpp - HTTP request parsing.
+
+  Copyright (c) 2015 Ivan Grokhotkov. All rights reserved.
+
+  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
+  Modified 8 May 2015 by Hristo Gochkov (proper post and file upload handling)
+*/
+
+#include <Arduino.h>
+#include "WiFiServer.h"
+#include "WiFiClient.h"
+#include "ESP8266WebServer.h"
+
+//#define DEBUG_ESP_HTTP_SERVER
+#ifdef DEBUG_ESP_PORT
+#define DEBUG_OUTPUT DEBUG_ESP_PORT
+#else
+#define DEBUG_OUTPUT Serial
+#endif
+
+static char* readBytesWithTimeout(WiFiClient& client, size_t maxLength, size_t& dataLength, int timeout_ms)
+{
+  char *buf = nullptr;
+  dataLength = 0;
+  while (dataLength < maxLength) {
+    int tries = timeout_ms;
+    size_t newLength;
+    while (!(newLength = client.available()) && tries--) delay(1);
+    if (!newLength) {
+      break;
+    }
+    if (!buf) {
+      buf = (char *) malloc(newLength + 1);
+      if (!buf) {
+        return nullptr;
+      }
+    }
+    else {
+      char* newBuf = (char *) realloc(buf, dataLength + newLength + 1);
+      if (!newBuf) {
+        free(buf);
+        return nullptr;
+      }
+      buf = newBuf;
+    }
+    client.readBytes(buf + dataLength, newLength);
+    dataLength += newLength;
+    buf[dataLength] = '\0';
+  }
+  return buf;
+}
+
+bool ESP8266WebServer::_parseRequest(WiFiClient& client) {
+  // Read the first line of HTTP request
+  String req = client.readStringUntil('\r');
+  client.readStringUntil('\n');
+  //reset header value
+  for (int i = 0; i < _headerKeysCount; ++i) {
+    _currentHeaders[i].value =String();
+   }
+
+  // First line of HTTP request looks like "GET /path HTTP/1.1"
+  // Retrieve the "/path" part by finding the spaces
+  int addr_start = req.indexOf(' ');
+  int addr_end = req.indexOf(' ', addr_start + 1);
+  if (addr_start == -1 || addr_end == -1) {
+#ifdef DEBUG_ESP_HTTP_SERVER
+    DEBUG_OUTPUT.print("Invalid request: ");
+    DEBUG_OUTPUT.println(req);
+#endif
+    return false;
+  }
+
+  String methodStr = req.substring(0, addr_start);
+  String url = req.substring(addr_start + 1, addr_end);
+  String searchStr = "";
+  int hasSearch = url.indexOf('?');
+  if (hasSearch != -1){
+    searchStr = url.substring(hasSearch + 1);
+    url = url.substring(0, hasSearch);
+  }
+  _currentUri = url;
+
+  HTTPMethod method = HTTP_GET;
+  if (methodStr == "POST") {
+    method = HTTP_POST;
+  } else if (methodStr == "DELETE") {
+    method = HTTP_DELETE;
+  } else if (methodStr == "OPTIONS") {
+    method = HTTP_OPTIONS;
+  } else if (methodStr == "PUT") {
+    method = HTTP_PUT;
+  } else if (methodStr == "PATCH") {
+    method = HTTP_PATCH;
+  }
+  _currentMethod = method;
+
+#ifdef DEBUG_ESP_HTTP_SERVER
+  DEBUG_OUTPUT.print("method: ");
+  DEBUG_OUTPUT.print(methodStr);
+  DEBUG_OUTPUT.print(" url: ");
+  DEBUG_OUTPUT.print(url);
+  DEBUG_OUTPUT.print(" search: ");
+  DEBUG_OUTPUT.println(searchStr);
+#endif
+
+  //attach handler
+  RequestHandler* handler;
+  for (handler = _firstHandler; handler; handler = handler->next()) {
+    if (handler->canHandle(_currentMethod, _currentUri))
+      break;
+  }
+  _currentHandler = handler;
+
+  String formData;
+  // below is needed only when POST type request
+  if (method == HTTP_POST || method == HTTP_PUT || method == HTTP_PATCH || method == HTTP_DELETE){
+    String boundaryStr;
+    String headerName;
+    String headerValue;
+    bool isForm = false;
+    uint32_t contentLength = 0;
+    //parse headers
+    while(1){
+      req = client.readStringUntil('\r');
+      client.readStringUntil('\n');
+      if (req == "") break;//no moar headers
+      int headerDiv = req.indexOf(':');
+      if (headerDiv == -1){
+        break;
+      }
+      headerName = req.substring(0, headerDiv);
+      headerValue = req.substring(headerDiv + 1);
+      headerValue.trim();
+       _collectHeader(headerName.c_str(),headerValue.c_str());
+
+	  #ifdef DEBUG_ESP_HTTP_SERVER
+	  DEBUG_OUTPUT.print("headerName: ");
+	  DEBUG_OUTPUT.println(headerName);
+	  DEBUG_OUTPUT.print("headerValue: ");
+	  DEBUG_OUTPUT.println(headerValue);
+	  #endif
+
+      if (headerName == "Content-Type"){
+        if (headerValue.startsWith("text/plain")){
+          isForm = false;
+        } else if (headerValue.startsWith("multipart/form-data")){
+          boundaryStr = headerValue.substring(headerValue.indexOf('=')+1);
+          isForm = true;
+        }
+      } else if (headerName == "Content-Length"){
+        contentLength = headerValue.toInt();
+      } else if (headerName == "Host"){
+        _hostHeader = headerValue;
+      }
+    }
+
+    if (!isForm){
+      size_t plainLength;
+      char* plainBuf = readBytesWithTimeout(client, contentLength, plainLength, HTTP_MAX_POST_WAIT);
+      if (plainLength < contentLength) {
+      	free(plainBuf);
+      	return false;
+      }
+#ifdef DEBUG_ESP_HTTP_SERVER
+      DEBUG_OUTPUT.print("Plain: ");
+      DEBUG_OUTPUT.println(plainBuf);
+#endif
+      if (contentLength > 0) {
+        if (searchStr != "") searchStr += '&';
+        if(plainBuf[0] == '{' || plainBuf[0] == '[' || strstr(plainBuf, "=") == NULL){
+          //plain post json or other data
+          searchStr += "plain=";
+          searchStr += plainBuf;
+        } else {
+          searchStr += plainBuf;
+        }
+        free(plainBuf);
+      }
+    }
+    _parseArguments(searchStr);
+    if (isForm){
+      if (!_parseForm(client, boundaryStr, contentLength)) {
+        return false;
+      }
+    }
+  } else {
+    String headerName;
+    String headerValue;
+    //parse headers
+    while(1){
+      req = client.readStringUntil('\r');
+      client.readStringUntil('\n');
+      if (req == "") break;//no moar headers
+      int headerDiv = req.indexOf(':');
+      if (headerDiv == -1){
+        break;
+      }
+      headerName = req.substring(0, headerDiv);
+      headerValue = req.substring(headerDiv + 2);
+      _collectHeader(headerName.c_str(),headerValue.c_str());
+
+	  #ifdef DEBUG_ESP_HTTP_SERVER
+	  DEBUG_OUTPUT.print("headerName: ");
+	  DEBUG_OUTPUT.println(headerName);
+	  DEBUG_OUTPUT.print("headerValue: ");
+	  DEBUG_OUTPUT.println(headerValue);
+	  #endif
+
+	  if (headerName == "Host"){
+        _hostHeader = headerValue;
+      }
+    }
+    _parseArguments(searchStr);
+  }
+  client.flush();
+
+#ifdef DEBUG_ESP_HTTP_SERVER
+  DEBUG_OUTPUT.print("Request: ");
+  DEBUG_OUTPUT.println(url);
+  DEBUG_OUTPUT.print(" Arguments: ");
+  DEBUG_OUTPUT.println(searchStr);
+#endif
+
+  return true;
+}
+
+bool ESP8266WebServer::_collectHeader(const char* headerName, const char* headerValue) {
+  for (int i = 0; i < _headerKeysCount; i++) {
+    if (_currentHeaders[i].key==headerName) {
+            _currentHeaders[i].value=headerValue;
+            return true;
+        }
+  }
+  return false;
+}
+
+void ESP8266WebServer::_parseArguments(String data) {
+#ifdef DEBUG_ESP_HTTP_SERVER
+  DEBUG_OUTPUT.print("args: ");
+  DEBUG_OUTPUT.println(data);
+#endif
+  if (_currentArgs)
+    delete[] _currentArgs;
+  _currentArgs = 0;
+  if (data.length() == 0) {
+    _currentArgCount = 0;
+    return;
+  }
+  _currentArgCount = 1;
+
+  for (int i = 0; i < (int)data.length(); ) {
+    i = data.indexOf('&', i);
+    if (i == -1)
+      break;
+    ++i;
+    ++_currentArgCount;
+  }
+#ifdef DEBUG_ESP_HTTP_SERVER
+  DEBUG_OUTPUT.print("args count: ");
+  DEBUG_OUTPUT.println(_currentArgCount);
+#endif
+
+  _currentArgs = new RequestArgument[_currentArgCount];
+  int pos = 0;
+  int iarg;
+  for (iarg = 0; iarg < _currentArgCount;) {
+    int equal_sign_index = data.indexOf('=', pos);
+    int next_arg_index = data.indexOf('&', pos);
+#ifdef DEBUG_ESP_HTTP_SERVER
+    DEBUG_OUTPUT.print("pos ");
+    DEBUG_OUTPUT.print(pos);
+    DEBUG_OUTPUT.print("=@ ");
+    DEBUG_OUTPUT.print(equal_sign_index);
+    DEBUG_OUTPUT.print(" &@ ");
+    DEBUG_OUTPUT.println(next_arg_index);
+#endif
+    if ((equal_sign_index == -1) || ((equal_sign_index > next_arg_index) && (next_arg_index != -1))) {
+#ifdef DEBUG_ESP_HTTP_SERVER
+      DEBUG_OUTPUT.print("arg missing value: ");
+      DEBUG_OUTPUT.println(iarg);
+#endif
+      if (next_arg_index == -1)
+        break;
+      pos = next_arg_index + 1;
+      continue;
+    }
+    RequestArgument& arg = _currentArgs[iarg];
+    arg.key = data.substring(pos, equal_sign_index);
+	arg.value = urlDecode(data.substring(equal_sign_index + 1, next_arg_index));
+#ifdef DEBUG_ESP_HTTP_SERVER
+    DEBUG_OUTPUT.print("arg ");
+    DEBUG_OUTPUT.print(iarg);
+    DEBUG_OUTPUT.print(" key: ");
+    DEBUG_OUTPUT.print(arg.key);
+    DEBUG_OUTPUT.print(" value: ");
+    DEBUG_OUTPUT.println(arg.value);
+#endif
+    ++iarg;
+    if (next_arg_index == -1)
+      break;
+    pos = next_arg_index + 1;
+  }
+  _currentArgCount = iarg;
+#ifdef DEBUG_ESP_HTTP_SERVER
+  DEBUG_OUTPUT.print("args count: ");
+  DEBUG_OUTPUT.println(_currentArgCount);
+#endif
+
+}
+
+void ESP8266WebServer::_uploadWriteByte(uint8_t b){
+  if (_currentUpload.currentSize == HTTP_UPLOAD_BUFLEN){
+    if(_currentHandler && _currentHandler->canUpload(_currentUri))
+      _currentHandler->upload(*this, _currentUri, _currentUpload);
+    _currentUpload.totalSize += _currentUpload.currentSize;
+    _currentUpload.currentSize = 0;
+  }
+  _currentUpload.buf[_currentUpload.currentSize++] = b;
+}
+
+uint8_t ESP8266WebServer::_uploadReadByte(WiFiClient& client){
+  int res = client.read();
+  if(res == -1){
+    while(!client.available() && client.connected())
+      yield();
+    res = client.read();
+  }
+  return (uint8_t)res;
+}
+
+bool ESP8266WebServer::_parseForm(WiFiClient& client, String boundary, uint32_t len){
+
+#ifdef DEBUG_ESP_HTTP_SERVER
+  DEBUG_OUTPUT.print("Parse Form: Boundary: ");
+  DEBUG_OUTPUT.print(boundary);
+  DEBUG_OUTPUT.print(" Length: ");
+  DEBUG_OUTPUT.println(len);
+#endif
+  String line;
+  int retry = 0;
+  do {
+    line = client.readStringUntil('\r');
+    ++retry;
+  } while (line.length() == 0 && retry < 3);
+
+  client.readStringUntil('\n');
+  //start reading the form
+  if (line == ("--"+boundary)){
+    RequestArgument* postArgs = new RequestArgument[32];
+    int postArgsLen = 0;
+    while(1){
+      String argName;
+      String argValue;
+      String argType;
+      String argFilename;
+      bool argIsFile = false;
+
+      line = client.readStringUntil('\r');
+      client.readStringUntil('\n');
+      if (line.startsWith("Content-Disposition")){
+        int nameStart = line.indexOf('=');
+        if (nameStart != -1){
+          argName = line.substring(nameStart+2);
+          nameStart = argName.indexOf('=');
+          if (nameStart == -1){
+            argName = argName.substring(0, argName.length() - 1);
+          } else {
+            argFilename = argName.substring(nameStart+2, argName.length() - 1);
+            argName = argName.substring(0, argName.indexOf('"'));
+            argIsFile = true;
+#ifdef DEBUG_ESP_HTTP_SERVER
+            DEBUG_OUTPUT.print("PostArg FileName: ");
+            DEBUG_OUTPUT.println(argFilename);
+#endif
+            //use GET to set the filename if uploading using blob
+            if (argFilename == "blob" && hasArg("filename")) argFilename = arg("filename");
+          }
+#ifdef DEBUG_ESP_HTTP_SERVER
+          DEBUG_OUTPUT.print("PostArg Name: ");
+          DEBUG_OUTPUT.println(argName);
+#endif
+          argType = "text/plain";
+          line = client.readStringUntil('\r');
+          client.readStringUntil('\n');
+          if (line.startsWith("Content-Type")){
+            argType = line.substring(line.indexOf(':')+2);
+            //skip next line
+            client.readStringUntil('\r');
+            client.readStringUntil('\n');
+          }
+#ifdef DEBUG_ESP_HTTP_SERVER
+          DEBUG_OUTPUT.print("PostArg Type: ");
+          DEBUG_OUTPUT.println(argType);
+#endif
+          if (!argIsFile){
+            while(1){
+              line = client.readStringUntil('\r');
+              client.readStringUntil('\n');
+              if (line.startsWith("--"+boundary)) break;
+              if (argValue.length() > 0) argValue += "\n";
+              argValue += line;
+            }
+#ifdef DEBUG_ESP_HTTP_SERVER
+            DEBUG_OUTPUT.print("PostArg Value: ");
+            DEBUG_OUTPUT.println(argValue);
+            DEBUG_OUTPUT.println();
+#endif
+
+            RequestArgument& arg = postArgs[postArgsLen++];
+            arg.key = argName;
+            arg.value = argValue;
+
+            if (line == ("--"+boundary+"--")){
+#ifdef DEBUG_ESP_HTTP_SERVER
+              DEBUG_OUTPUT.println("Done Parsing POST");
+#endif
+              break;
+            }
+          } else {
+            _currentUpload.status = UPLOAD_FILE_START;
+            _currentUpload.name = argName;
+            _currentUpload.filename = argFilename;
+            _currentUpload.type = argType;
+            _currentUpload.totalSize = 0;
+            _currentUpload.currentSize = 0;
+#ifdef DEBUG_ESP_HTTP_SERVER
+            DEBUG_OUTPUT.print("Start File: ");
+            DEBUG_OUTPUT.print(_currentUpload.filename);
+            DEBUG_OUTPUT.print(" Type: ");
+            DEBUG_OUTPUT.println(_currentUpload.type);
+#endif
+            if(_currentHandler && _currentHandler->canUpload(_currentUri))
+              _currentHandler->upload(*this, _currentUri, _currentUpload);
+            _currentUpload.status = UPLOAD_FILE_WRITE;
+            uint8_t argByte = _uploadReadByte(client);
+readfile:
+            while(argByte != 0x0D){
+              if (!client.connected()) return _parseFormUploadAborted();
+              _uploadWriteByte(argByte);
+              argByte = _uploadReadByte(client);
+            }
+
+            argByte = _uploadReadByte(client);
+            if (!client.connected()) return _parseFormUploadAborted();
+            if (argByte == 0x0A){
+              argByte = _uploadReadByte(client);
+              if (!client.connected()) return _parseFormUploadAborted();
+              if ((char)argByte != '-'){
+                //continue reading the file
+                _uploadWriteByte(0x0D);
+                _uploadWriteByte(0x0A);
+                goto readfile;
+              } else {
+                argByte = _uploadReadByte(client);
+                if (!client.connected()) return _parseFormUploadAborted();
+                if ((char)argByte != '-'){
+                  //continue reading the file
+                  _uploadWriteByte(0x0D);
+                  _uploadWriteByte(0x0A);
+                  _uploadWriteByte((uint8_t)('-'));
+                  goto readfile;
+                }
+              }
+
+              uint8_t endBuf[boundary.length()];
+              client.readBytes(endBuf, boundary.length());
+
+              if (strstr((const char*)endBuf, boundary.c_str()) != NULL){
+                if(_currentHandler && _currentHandler->canUpload(_currentUri))
+                  _currentHandler->upload(*this, _currentUri, _currentUpload);
+                _currentUpload.totalSize += _currentUpload.currentSize;
+                _currentUpload.status = UPLOAD_FILE_END;
+                if(_currentHandler && _currentHandler->canUpload(_currentUri))
+                  _currentHandler->upload(*this, _currentUri, _currentUpload);
+#ifdef DEBUG_ESP_HTTP_SERVER
+                DEBUG_OUTPUT.print("End File: ");
+                DEBUG_OUTPUT.print(_currentUpload.filename);
+                DEBUG_OUTPUT.print(" Type: ");
+                DEBUG_OUTPUT.print(_currentUpload.type);
+                DEBUG_OUTPUT.print(" Size: ");
+                DEBUG_OUTPUT.println(_currentUpload.totalSize);
+#endif
+                line = client.readStringUntil(0x0D);
+                client.readStringUntil(0x0A);
+                if (line == "--"){
+#ifdef DEBUG_ESP_HTTP_SERVER
+                  DEBUG_OUTPUT.println("Done Parsing POST");
+#endif
+                  break;
+                }
+                continue;
+              } else {
+                _uploadWriteByte(0x0D);
+                _uploadWriteByte(0x0A);
+                _uploadWriteByte((uint8_t)('-'));
+                _uploadWriteByte((uint8_t)('-'));
+                uint32_t i = 0;
+                while(i < boundary.length()){
+                  _uploadWriteByte(endBuf[i++]);
+                }
+                argByte = _uploadReadByte(client);
+                goto readfile;
+              }
+            } else {
+              _uploadWriteByte(0x0D);
+              goto readfile;
+            }
+            break;
+          }
+        }
+      }
+    }
+
+    int iarg;
+    int totalArgs = ((32 - postArgsLen) < _currentArgCount)?(32 - postArgsLen):_currentArgCount;
+    for (iarg = 0; iarg < totalArgs; iarg++){
+      RequestArgument& arg = postArgs[postArgsLen++];
+      arg.key = _currentArgs[iarg].key;
+      arg.value = _currentArgs[iarg].value;
+    }
+    if (_currentArgs) delete[] _currentArgs;
+    _currentArgs = new RequestArgument[postArgsLen];
+    for (iarg = 0; iarg < postArgsLen; iarg++){
+      RequestArgument& arg = _currentArgs[iarg];
+      arg.key = postArgs[iarg].key;
+      arg.value = postArgs[iarg].value;
+    }
+    _currentArgCount = iarg;
+    if (postArgs) delete[] postArgs;
+    return true;
+  }
+#ifdef DEBUG_ESP_HTTP_SERVER
+  DEBUG_OUTPUT.print("Error: line: ");
+  DEBUG_OUTPUT.println(line);
+#endif
+  return false;
+}
+
+String ESP8266WebServer::urlDecode(const String& text)
+{
+	String decoded = "";
+	char temp[] = "0x00";
+	unsigned int len = text.length();
+	unsigned int i = 0;
+	while (i < len)
+	{
+		char decodedChar;
+		char encodedChar = text.charAt(i++);
+		if ((encodedChar == '%') && (i + 1 < len))
+		{
+			temp[2] = text.charAt(i++);
+			temp[3] = text.charAt(i++);
+
+			decodedChar = strtol(temp, NULL, 16);
+		}
+		else {
+			if (encodedChar == '+')
+			{
+				decodedChar = ' ';
+			}
+			else {
+				decodedChar = encodedChar;  // normal ascii char
+			}
+		}
+		decoded += decodedChar;
+	}
+	return decoded;
+}
+
+bool ESP8266WebServer::_parseFormUploadAborted(){
+  _currentUpload.status = UPLOAD_FILE_ABORTED;
+  if(_currentHandler && _currentHandler->canUpload(_currentUri))
+    _currentHandler->upload(*this, _currentUri, _currentUpload);
+  return false;
+}

+ 19 - 0
lib/ESP8266WebServer/src/detail/RequestHandler.h

@@ -0,0 +1,19 @@
+#ifndef REQUESTHANDLER_H
+#define REQUESTHANDLER_H
+
+class RequestHandler {
+public:
+    virtual ~RequestHandler() { }
+    virtual bool canHandle(HTTPMethod method, String uri) { return false; }
+    virtual bool canUpload(String uri) { return false; }
+    virtual bool handle(ESP8266WebServer& server, HTTPMethod requestMethod, String requestUri) { return false; }
+    virtual void upload(ESP8266WebServer& server, String requestUri, HTTPUpload& upload) {}
+
+    RequestHandler* next() { return _next; }
+    void next(RequestHandler* r) { _next = r; }
+
+private:
+    RequestHandler* _next = nullptr;
+};
+
+#endif //REQUESTHANDLER_H

+ 150 - 0
lib/ESP8266WebServer/src/detail/RequestHandlersImpl.h

@@ -0,0 +1,150 @@
+#ifndef REQUESTHANDLERSIMPL_H
+#define REQUESTHANDLERSIMPL_H
+
+#include "RequestHandler.h"
+
+class FunctionRequestHandler : public RequestHandler {
+public:
+    FunctionRequestHandler(ESP8266WebServer::THandlerFunction fn, ESP8266WebServer::THandlerFunction ufn, const char* uri, HTTPMethod method)
+    : _fn(fn)
+    , _ufn(ufn)
+    , _uri(uri)
+    , _method(method)
+    {
+    }
+
+    bool canHandle(HTTPMethod requestMethod, String requestUri) override  {
+        if (_method != HTTP_ANY && _method != requestMethod)
+            return false;
+
+        if (requestUri != _uri)
+            return false;
+
+        return true;
+    }
+
+    bool canUpload(String requestUri) override  {
+        if (!_ufn || !canHandle(HTTP_POST, requestUri))
+            return false;
+
+        return true;
+    }
+
+    bool handle(ESP8266WebServer& server, HTTPMethod requestMethod, String requestUri) override {
+        if (!canHandle(requestMethod, requestUri))
+            return false;
+
+        _fn();
+        return true;
+    }
+
+    void upload(ESP8266WebServer& server, String requestUri, HTTPUpload& upload) override {
+        if (canUpload(requestUri))
+            _ufn();
+    }
+
+protected:
+    ESP8266WebServer::THandlerFunction _fn;
+    ESP8266WebServer::THandlerFunction _ufn;
+    String _uri;
+    HTTPMethod _method;
+};
+
+class StaticRequestHandler : public RequestHandler {
+public:
+    StaticRequestHandler(FS& fs, const char* path, const char* uri, const char* cache_header)
+    : _fs(fs)
+    , _uri(uri)
+    , _path(path)
+    , _cache_header(cache_header)
+    {
+        _isFile = fs.exists(path);
+        DEBUGV("StaticRequestHandler: path=%s uri=%s isFile=%d, cache_header=%s\r\n", path, uri, _isFile, cache_header);
+        _baseUriLength = _uri.length();
+    }
+
+    bool canHandle(HTTPMethod requestMethod, String requestUri) override  {
+        if (requestMethod != HTTP_GET)
+            return false;
+
+        if ((_isFile && requestUri != _uri) || !requestUri.startsWith(_uri))
+            return false;
+
+        return true;
+    }
+
+    bool handle(ESP8266WebServer& server, HTTPMethod requestMethod, String requestUri) override {
+        if (!canHandle(requestMethod, requestUri))
+            return false;
+
+        DEBUGV("StaticRequestHandler::handle: request=%s _uri=%s\r\n", requestUri.c_str(), _uri.c_str());
+
+        String path(_path);
+
+        if (!_isFile) {
+            // Base URI doesn't point to a file.
+            // If a directory is requested, look for index file.
+            if (requestUri.endsWith("/")) requestUri += "index.htm";
+
+            // Append whatever follows this URI in request to get the file path.
+            path += requestUri.substring(_baseUriLength);
+        }
+        DEBUGV("StaticRequestHandler::handle: path=%s, isFile=%d\r\n", path.c_str(), _isFile);
+
+        String contentType = getContentType(path);
+
+        // look for gz file, only if the original specified path is not a gz.  So part only works to send gzip via content encoding when a non compressed is asked for
+        // if you point the the path to gzip you will serve the gzip as content type "application/x-gzip", not text or javascript etc...
+        if (!path.endsWith(".gz") && !_fs.exists(path))  {
+            String pathWithGz = path + ".gz";
+            if(_fs.exists(pathWithGz))
+                path += ".gz";
+        }
+
+        File f = _fs.open(path, "r");
+        if (!f)
+            return false;
+
+        if (_cache_header.length() != 0)
+            server.sendHeader("Cache-Control", _cache_header);
+
+        server.streamFile(f, contentType);
+        return true;
+    }
+
+    static String getContentType(const String& path) {
+        if (path.endsWith(".html")) return "text/html";
+        else if (path.endsWith(".htm")) return "text/html";
+        else if (path.endsWith(".css")) return "text/css";
+        else if (path.endsWith(".txt")) return "text/plain";
+        else if (path.endsWith(".js")) return "application/javascript";
+        else if (path.endsWith(".png")) return "image/png";
+        else if (path.endsWith(".gif")) return "image/gif";
+        else if (path.endsWith(".jpg")) return "image/jpeg";
+        else if (path.endsWith(".ico")) return "image/x-icon";
+        else if (path.endsWith(".svg")) return "image/svg+xml";
+        else if (path.endsWith(".ttf")) return "application/x-font-ttf";
+        else if (path.endsWith(".otf")) return "application/x-font-opentype";
+        else if (path.endsWith(".woff")) return "application/font-woff";
+        else if (path.endsWith(".woff2")) return "application/font-woff2";
+        else if (path.endsWith(".eot")) return "application/vnd.ms-fontobject";
+        else if (path.endsWith(".sfnt")) return "application/font-sfnt";
+        else if (path.endsWith(".xml")) return "text/xml";
+        else if (path.endsWith(".pdf")) return "application/pdf";
+        else if (path.endsWith(".zip")) return "application/zip";
+        else if(path.endsWith(".gz")) return "application/x-gzip";
+        else if (path.endsWith(".appcache")) return "text/cache-manifest";
+        return "application/octet-stream";
+    }
+
+protected:
+    FS _fs;
+    String _uri;
+    String _path;
+    String _cache_header;
+    bool _isFile;
+    size_t _baseUriLength;
+};
+
+
+#endif //REQUESTHANDLERSIMPL_H

+ 51 - 0
lib/Helpers/TokenIterator.cpp

@@ -0,0 +1,51 @@
+#include <TokenIterator.h>
+
+TokenIterator::TokenIterator(char* data, size_t length, const char sep)
+  : data(data),
+    current(data),
+    length(length),
+    sep(sep),
+    i(0)
+{
+  for (size_t i = 0; i < length; i++) {
+    if (data[i] == sep) {
+      data[i] = 0;
+    }
+  }
+}
+
+const char* TokenIterator::nextToken() {
+  if (i >= length) {
+    return NULL;
+  }
+
+  char* token = current;
+  char* nextToken = current;
+
+  for (; i < length && *nextToken != 0; i++, nextToken++);
+
+  if (i == length) {
+    nextToken = NULL;
+  } else {
+    i = (nextToken - data);
+
+    if (i < length) {
+      nextToken++;
+    } else {
+      nextToken = NULL;
+    }
+  }
+
+  current = nextToken;
+
+  return token;
+}
+
+void TokenIterator::reset() {
+  current = data;
+  i = 0;
+}
+
+bool TokenIterator::hasNext() {
+  return i < length;
+}

+ 21 - 0
lib/Helpers/TokenIterator.h

@@ -0,0 +1,21 @@
+#include <Arduino.h>
+
+#ifndef _TOKEN_ITERATOR_H
+#define _TOKEN_ITERATOR_H
+
+class TokenIterator {
+public:
+  TokenIterator(char* data, size_t length, char sep = ',');
+
+  bool hasNext();
+  const char* nextToken();
+  void reset();
+
+private:
+  char* data;
+  char* current;
+  size_t length;
+  char sep;
+  int i;
+};
+#endif

+ 35 - 0
lib/Helpers/UrlTokenBindings.cpp

@@ -0,0 +1,35 @@
+#include <UrlTokenBindings.h>
+
+UrlTokenBindings::UrlTokenBindings(TokenIterator& patternTokens, TokenIterator& requestTokens)
+  : patternTokens(patternTokens),
+    requestTokens(requestTokens)
+{ }
+
+bool UrlTokenBindings::hasBinding(const char* searchToken) const {
+  patternTokens.reset();
+  while (patternTokens.hasNext()) {
+    const char* token = patternTokens.nextToken();
+
+    if (token[0] == ':' && strcmp(token+1, searchToken) == 0) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+const char* UrlTokenBindings::get(const char* searchToken) const {
+  patternTokens.reset();
+  requestTokens.reset();
+
+  while (patternTokens.hasNext() && requestTokens.hasNext()) {
+    const char* token = patternTokens.nextToken();
+    const char* binding = requestTokens.nextToken();
+
+    if (token[0] == ':' && strcmp(token+1, searchToken) == 0) {
+      return binding;
+    }
+  }
+
+  return NULL;
+}

+ 18 - 0
lib/Helpers/UrlTokenBindings.h

@@ -0,0 +1,18 @@
+#include <TokenIterator.h>
+
+#ifndef _URL_TOKEN_BINDINGS_H
+#define _URL_TOKEN_BINDINGS_H
+
+class UrlTokenBindings {
+public:
+  UrlTokenBindings(TokenIterator& patternTokens, TokenIterator& requestTokens);
+
+  bool hasBinding(const char* key) const;
+  const char* get(const char* key) const;
+
+private:
+  TokenIterator& patternTokens;
+  TokenIterator& requestTokens;
+};
+
+#endif

+ 142 - 0
lib/MQTT/MqttClient.cpp

@@ -0,0 +1,142 @@
+#include <MqttClient.h>
+#include <TokenIterator.h>
+#include <UrlTokenBindings.h>
+#include <IntParsing.h>
+#include <ArduinoJson.h>
+#include <WiFiClient.h>
+
+MqttClient::MqttClient(Settings& settings, MiLightClient*& milightClient)
+  : milightClient(milightClient),
+    settings(settings),
+    lastConnectAttempt(0)
+{
+  String strDomain = settings.mqttServer();
+  this->domain = new char[strDomain.length() + 1];
+  strcpy(this->domain, strDomain.c_str());
+
+  this->mqttClient = new PubSubClient(tcpClient);
+}
+
+MqttClient::~MqttClient() {
+  mqttClient->disconnect();
+  delete this->domain;
+}
+
+void MqttClient::begin() {
+#ifdef MQTT_DEBUG
+  printf_P(
+    PSTR("MqttClient - Connecting to: %s\nparsed:%s:%u\n"),
+    settings._mqttServer.c_str(),
+    settings.mqttServer().c_str(),
+    settings.mqttPort()
+  );
+#endif
+
+  mqttClient->setServer(this->domain, settings.mqttPort());
+  mqttClient->setCallback(
+    [this](char* topic, byte* payload, int length) {
+      this->publishCallback(topic, payload, length);
+    }
+  );
+  reconnect();
+}
+
+bool MqttClient::connect() {
+  char nameBuffer[30];
+  sprintf_P(nameBuffer, PSTR("milight-hub-%u"), ESP.getChipId());
+
+#ifdef MQTT_DEBUG
+    Serial.println(F("MqttClient - connecting"));
+#endif
+
+  if (settings.mqttUsername.length() > 0) {
+    return mqttClient->connect(
+      nameBuffer,
+      settings.mqttUsername.c_str(),
+      settings.mqttPassword.c_str()
+    );
+  } else {
+    return mqttClient->connect(nameBuffer);
+  }
+}
+
+void MqttClient::reconnect() {
+  if (lastConnectAttempt > 0 && (millis() - lastConnectAttempt) < MQTT_CONNECTION_ATTEMPT_FREQUENCY) {
+    return;
+  }
+
+  if (! mqttClient->connected()) {
+    if (connect()) {
+      subscribe();
+
+#ifdef MQTT_DEBUG
+      Serial.println(F("MqttClient - Successfully connected to MQTT server"));
+#endif
+    } else {
+      Serial.println(F("ERROR: Failed to connect to MQTT server"));
+    }
+  }
+
+  lastConnectAttempt = millis();
+}
+
+void MqttClient::handleClient() {
+  reconnect();
+  mqttClient->loop();
+}
+
+void MqttClient::subscribe() {
+  String topic = settings.mqttTopicPattern;
+
+  topic.replace(":device_id", "+");
+  topic.replace(":group_id", "+");
+  topic.replace(":device_type", "+");
+
+#ifdef MQTT_DEBUG
+  printf_P(PSTR("MqttClient - subscribing to topic: %s\n"), topic.c_str());
+#endif
+
+  mqttClient->subscribe(topic.c_str());
+}
+
+void MqttClient::publishCallback(char* topic, byte* payload, int length) {
+  uint16_t deviceId = 0;
+  uint8_t groupId = 0;
+  MiLightRadioConfig* config = &MilightRgbCctConfig;
+  char cstrPayload[length + 1];
+  cstrPayload[length] = 0;
+  memcpy(cstrPayload, payload, sizeof(byte)*length);
+
+#ifdef MQTT_DEBUG
+  printf_P(PSTR("MqttClient - Got message on topic: %s\n%s\n"), topic, cstrPayload);
+#endif
+
+  char topicPattern[settings.mqttTopicPattern.length()];
+  strcpy(topicPattern, settings.mqttTopicPattern.c_str());
+
+  TokenIterator patternIterator(topicPattern, settings.mqttTopicPattern.length(), '/');
+  TokenIterator topicIterator(topic, strlen(topic), '/');
+  UrlTokenBindings tokenBindings(patternIterator, topicIterator);
+
+  if (tokenBindings.hasBinding("device_id")) {
+    deviceId = parseInt<uint16_t>(tokenBindings.get("device_id"));
+  }
+
+  if (tokenBindings.hasBinding("group_id")) {
+    groupId = parseInt<uint16_t>(tokenBindings.get("group_id"));
+  }
+
+  if (tokenBindings.hasBinding("device_type")) {
+    config = MiLightRadioConfig::fromString(tokenBindings.get("device_type"));
+  }
+
+  StaticJsonBuffer<400> buffer;
+  JsonObject& obj = buffer.parseObject(cstrPayload);
+
+#ifdef MQTT_DEBUG
+  printf_P(PSTR("MqttClient - device %04X, group %u\n"), deviceId, groupId);
+#endif
+
+  milightClient->prepare(*config, deviceId, groupId);
+  milightClient->update(obj);
+}

+ 35 - 0
lib/MQTT/MqttClient.h

@@ -0,0 +1,35 @@
+#include <MiLightClient.h>
+#include <Settings.h>
+#include <PubSubClient.h>
+#include <WiFiClient.h>
+
+#ifndef MQTT_CONNECTION_ATTEMPT_FREQUENCY
+#define MQTT_CONNECTION_ATTEMPT_FREQUENCY 5000
+#endif
+
+#ifndef _MQTT_CLIENT_H
+#define _MQTT_CLIENT_H
+
+class MqttClient {
+public:
+  MqttClient(Settings& settings, MiLightClient*& milightClient);
+  ~MqttClient();
+
+  void begin();
+  void handleClient();
+  void reconnect();
+
+private:
+  WiFiClient tcpClient;
+  PubSubClient* mqttClient;
+  MiLightClient*& milightClient;
+  Settings& settings;
+  char* domain;
+  unsigned long lastConnectAttempt;
+
+  bool connect();
+  void subscribe();
+  void publishCallback(char* topic, byte* payload, int length);
+};
+
+#endif

+ 0 - 36
lib/MiLight/AbstractPL1167.h

@@ -1,36 +0,0 @@
-/*
- * AbstractPL1167.h
- *
- *  Created on: 29 May 2015
- *      Author: henryk
- */
-
-#ifdef ARDUINO
-#include "Arduino.h"
-#else
-#include <stdint.h>
-#include <stdlib.h>
-#endif
-
-#ifndef ABSTRACTPL1167_H_
-#define ABSTRACTPL1167_H_
-
-class AbstractPL1167 {
-  public:
-    virtual int open() = 0;
-
-    virtual int setPreambleLength(uint8_t preambleLength) = 0;
-    virtual int setSyncword(uint16_t syncword0, uint16_t syncword3) = 0;
-    virtual int setTrailerLength(uint8_t trailerLength) = 0;
-    virtual int setMaxPacketLength(uint8_t maxPacketLength) = 0;
-    virtual int setCRC(bool crc) = 0;
-    virtual int writeFIFO(const uint8_t data[], size_t data_length) = 0;
-    virtual int transmit(uint8_t channel) = 0;
-    virtual int receive(uint8_t channel) = 0;
-    virtual int readFIFO(uint8_t data[], size_t &data_length) = 0;
-};
-
-
-
-
-#endif /* ABSTRACTPL1167_H_ */

+ 13 - 6
lib/MiLight/CctPacketFormatter.cpp

@@ -3,7 +3,7 @@
 
 void CctPacketFormatter::initializePacket(uint8_t* packet) {
   size_t packetPtr = 0;
-  
+
   packet[packetPtr++] = CCT;
   packet[packetPtr++] = deviceId >> 8;
   packet[packetPtr++] = deviceId & 0xFF;
@@ -30,16 +30,19 @@ void CctPacketFormatter::updateTemperature(uint8_t value) {
     value / CCT_INTERVALS
   );
 }
-  
+
 void CctPacketFormatter::command(uint8_t command, uint8_t arg) {
   pushPacket();
+  if (held) {
+    command |= 0x80;
+  }
   currentPacket[CCT_COMMAND_INDEX] = command;
 }
 
 void CctPacketFormatter::updateStatus(MiLightStatus status, uint8_t groupId) {
   command(getCctStatusButton(groupId, status), 0);
 }
-  
+
 void CctPacketFormatter::increaseTemperature() {
   command(CCT_TEMPERATURE_UP, 0);
 }
@@ -56,9 +59,13 @@ void CctPacketFormatter::decreaseBrightness() {
   command(CCT_BRIGHTNESS_DOWN, 0);
 }
 
+void CctPacketFormatter::enableNightMode() {
+  command(getCctStatusButton(groupId, OFF) | 0x10, 0);
+}
+
 uint8_t CctPacketFormatter::getCctStatusButton(uint8_t groupId, MiLightStatus status) {
   uint8_t button = 0;
-  
+
   if (status == ON) {
     switch(groupId) {
       case 1:
@@ -90,10 +97,10 @@ uint8_t CctPacketFormatter::getCctStatusButton(uint8_t groupId, MiLightStatus st
         break;
     }
   }
-  
+
   return button;
 }
 
 void CctPacketFormatter::format(uint8_t const* packet, char* buffer) {
   PacketFormatter::formatV1Packet(packet, buffer);
-}
+}

+ 8 - 7
lib/MiLight/CctPacketFormatter.h

@@ -1,7 +1,7 @@
 #include <PacketFormatter.h>
 
 #ifndef _CCT_PACKET_FORMATTER_H
-#define _CCT_PACKET_FORMATTER_H 
+#define _CCT_PACKET_FORMATTER_H
 
 #define CCT_COMMAND_INDEX 4
 #define CCT_INTERVALS 10
@@ -28,22 +28,23 @@ public:
   CctPacketFormatter()
     : PacketFormatter(7, 20)
   { }
-  
+
   virtual void updateStatus(MiLightStatus status, uint8_t groupId);
   virtual void command(uint8_t command, uint8_t arg);
-  
+
   virtual void updateTemperature(uint8_t value);
   virtual void increaseTemperature();
   virtual void decreaseTemperature();
-  
+
   virtual void updateBrightness(uint8_t value);
   virtual void increaseBrightness();
   virtual void decreaseBrightness();
-  
+  virtual void enableNightMode();
+
   virtual void format(uint8_t const* packet, char* buffer);
   virtual void initializePacket(uint8_t* packet);
-  
+
   static uint8_t getCctStatusButton(uint8_t groupId, MiLightStatus status);
 };
 
-#endif
+#endif

+ 513 - 0
lib/MiLight/LT8900MiLightRadio.cpp

@@ -0,0 +1,513 @@
+/*
+ * MiLightRadioPL1167_LT89000.cpp
+ *
+ *  Created on: 31 March 2017
+ *      Author: WoodsterDK
+ *
+ *  Very inspired by:
+ *  https://github.com/pmoscetta/authometion-milight/tree/master/Authometion-MiLight
+ *  https://bitbucket.org/robvanderveer/lt8900lib
+ */
+
+#include "LT8900MiLightRadio.h"
+#include <SPI.h>
+
+/**************************************************************************/
+// Constructor
+/**************************************************************************/
+LT8900MiLightRadio::LT8900MiLightRadio(byte byCSPin, byte byResetPin, byte byPktFlag, const MiLightRadioConfig& config)
+  : _config(config),
+    _channel(0),
+    _currentPacketLen(0),
+    _currentPacketPos(0)
+{
+  _csPin = byCSPin;
+	_pin_pktflag = byPktFlag;
+
+  pinMode(_pin_pktflag, INPUT);
+
+	if (byResetPin > 0)					// If zero then bypass hardware reset
+	{
+		pinMode(byResetPin, OUTPUT);
+		digitalWrite(byResetPin, LOW);
+		delay(200);
+		digitalWrite(byResetPin, HIGH);
+		delay(200);
+	}
+
+  pinMode(_csPin, OUTPUT);
+  digitalWrite(_csPin, HIGH);
+
+  SPI.begin();
+
+  SPI.setDataMode(SPI_MODE1);
+  // The following speed settings depends upon the wiring and PCB
+  //SPI.setFrequency(8000000);
+  SPI.setFrequency(4000000);
+  SPI.setBitOrder(MSBFIRST);
+
+  //Initialize transceiver with correct settings
+  vInitRadioModule(config.type);
+  delay(50);
+
+  // Check if HW is connected
+  _bConnected = bCheckRadioConnection();
+
+  //Reset SPI MODE to default
+  SPI.setDataMode(SPI_MODE0);
+  _waiting = false;
+}
+
+
+
+
+/**************************************************************************/
+// Checks the connection to the radio module by verifying a register setting
+/**************************************************************************/
+bool LT8900MiLightRadio::bCheckRadioConnection(void)
+{
+	bool bRetValue = false;
+	uint16_t value_0 = uiReadRegister(0);
+	uint16_t value_1 = uiReadRegister(1);
+
+	if ((value_0 == 0x6fe0) && (value_1 == 0x5681))
+	{
+    #ifdef DEBUG_PRINTF
+		  Serial.println(F("Radio module running correctly..."));
+    #endif
+		bRetValue = true;
+	}
+	else
+	{
+    #ifdef DEBUG_PRINTF
+		  Serial.println(F("Failed initializing the radio module..."));
+    #endif
+	}
+
+	return bRetValue;
+}
+
+/**************************************************************************/
+// Initialize radio module
+/**************************************************************************/
+void LT8900MiLightRadio::vInitRadioModule(MiLightRadioType type)
+{
+	if (type == RGB_CCT)
+	{
+		bool bWriteDefaultDefault = true;  // Is it okay to use the default power up values, without setting them
+
+		regWrite16(0x00, 0x6F, 0xE0, 7);  // Recommended value by PMmicro
+		regWrite16(0x02, 0x66, 0x17, 7);  // Recommended value by PMmicro
+		regWrite16(0x04, 0x9C, 0xC9, 7);  // Recommended value by PMmicro
+
+		regWrite16(0x05, 0x66, 0x37, 7);  // Recommended value by PMmicro
+		regWrite16(0x07, 0x00, 0x4C, 7);  // PL1167's TX/RX Enable and Channel Register, Default channel 76
+		regWrite16(0x08, 0x6C, 0x90, 7);  // Recommended value by PMmicro
+		regWrite16(0x09, 0x48, 0x00, 7);  // PA Control register
+
+		regWrite16(0x0B, 0x00, 0x08, 7);  // Recommended value by PMmicro
+		regWrite16(0x0D, 0x48, 0xBD, 7);  // Recommended value by PMmicro
+		regWrite16(0x16, 0x00, 0xFF, 7);  // Recommended value by PMmicro
+		regWrite16(0x18, 0x00, 0x67, 7);  // Recommended value by PMmicro
+
+		regWrite16(0x1A, 0x19, 0xE0, 7);  // Recommended value by PMmicro
+		regWrite16(0x1B, 0x13, 0x00, 7);  // Recommended value by PMmicro
+
+		regWrite16(0x20, 0x48, 0x00, 7);  // Recommended value by PMmicro
+		regWrite16(0x21, 0x3F, 0xC7, 7);  // Recommended value by PMmicro
+		regWrite16(0x22, 0x20, 0x00, 7);  // Recommended value by PMmicro
+		regWrite16(0x23, 0x03, 0x00, 7);  // Recommended value by PMmicro
+
+		regWrite16(0x24, 0x72, 0x36, 7);  // Sync R0
+		regWrite16(0x27, 0x18, 0x09, 7);  // Sync R3
+		regWrite16(0x28, 0x44, 0x02, 7);  // Recommended value by PMmicro
+		regWrite16(0x29, 0xB0, 0x00, 7);  // Recommended value by PMmicro
+		regWrite16(0x2A, 0xFD, 0xB0, 7);  // Recommended value by PMmicro
+
+		if (bWriteDefaultDefault == true)
+		{
+			regWrite16(0x01, 0x56, 0x81, 7);  // Recommended value by PMmicro
+			regWrite16(0x0A, 0x7F, 0xFD, 7);  // Recommended value by PMmicro
+			regWrite16(0x0C, 0x00, 0x00, 7);  // Recommended value by PMmicro
+			regWrite16(0x17, 0x80, 0x05, 7);  // Recommended value by PMmicro
+			regWrite16(0x19, 0x16, 0x59, 7);  // Recommended value by PMmicro
+			regWrite16(0x1C, 0x18, 0x00, 7);  // Recommended value by PMmicro
+
+			regWrite16(0x25, 0x00, 0x00, 7);  // Recommended value by PMmicro
+			regWrite16(0x26, 0x00, 0x00, 7);  // Recommended value by PMmicro
+			regWrite16(0x2B, 0x00, 0x0F, 7);  // Recommended value by PMmicro
+		}
+	}
+	else if((type == RGBW) || (type == CCT) || (type == RGB) )
+	{
+		regWrite16(0, 111, 224, 7);  // Recommended value by PMmicro
+		regWrite16(1, 86, 129, 7);   // Recommended value by PMmicro
+		regWrite16(2, 102, 23, 7);   // Recommended value by PMmicro
+		regWrite16(4, 156, 201, 7);  // Recommended value by PMmicro
+		regWrite16(5, 102, 55, 7);   // Recommended value by PMmicro
+		regWrite16(7, 0, 76, 7);     // PL1167's TX/RX Enable and Channel Register
+		regWrite16(8, 108, 144, 7);  // Recommended value by PMmicro
+		regWrite16(9, 72, 0, 7);     // PL1167's PA Control Register
+		regWrite16(10, 127, 253, 7); // Recommended value by PMmicro
+		regWrite16(11, 0, 8, 7);     // PL1167's RSSI OFF Control Register -- ???
+		regWrite16(12, 0, 0, 7);     // Recommended value by PMmicro
+		regWrite16(13, 72, 189, 7);  // Recommended value by PMmicro
+		regWrite16(22, 0, 255, 7);   // Recommended value by PMmicro
+		regWrite16(23, 128, 5, 7);   // PL1167's VCO Calibration Enable Register
+		regWrite16(24, 0, 103, 7);   // Recommended value by PMmicro
+		regWrite16(25, 22, 89, 7);   // Recommended value by PMmicro
+		regWrite16(26, 25, 224, 7);  // Recommended value by PMmicro
+		regWrite16(27, 19, 0, 7);    // Recommended value by PMmicro
+		regWrite16(28, 24, 0, 7);    // Recommended value by PMmicro
+		regWrite16(32, 72, 0, 7);    // PL1167's Data Configure Register: LEN_PREAMBLE = 010 -> (0xAAAAAA) 3 bytes, LEN_SYNCWORD = 01 -> 32 bits, LEN_TRAILER = 000 -> (0x05) 4 bits, TYPE_PKT_DAT = 00 -> NRZ law data, TYPE_FEC = 00 -> No FEC
+		regWrite16(33, 63, 199, 7);  // PL1167's Delay Time Control Register 0
+		regWrite16(34, 32, 0, 7);    // PL1167's Delay Time Control Register 1
+		regWrite16(35, 3, 0, 7);     // PL1167's Power Management and Miscellaneous Register
+		regWrite16(40, 68, 2, 7);    // PL1167's FIFO and SYNCWORD Threshold Register
+		regWrite16(41, 176, 0, 7);   // PL1167's Miscellaneous Register: CRC_ON = 1 -> ON, SCR_ON = 0 -> OFF, EN_PACK_LEN = 1 -> ON, FW_TERM_TX = 1 -> ON, AUTO_ACK = 0 -> OFF, PKT_LEVEL = 0 -> PKT active high, CRC_INIT_DAT = 0
+		regWrite16(42, 253, 176, 7); // PL1167's SCAN RSSI Register 0
+		regWrite16(43, 0, 15, 7);    // PL1167's SCAN RSSI Register 1
+		delay(200);
+		regWrite16(128, 0, 0, 7);
+		regWrite16(129, 255, 255, 7);
+		regWrite16(130, 0, 0, 7);
+		regWrite16(132, 0, 0, 7);
+		regWrite16(133, 255, 255, 7);
+		regWrite16(135, 255, 255, 7);
+		regWrite16(136, 0, 0, 7);
+		regWrite16(137, 255, 255, 7);
+		regWrite16(138, 0, 0, 7);
+		regWrite16(139, 255, 255, 7);
+		regWrite16(140, 0, 0, 7);
+		regWrite16(141, 255, 255, 7);
+		regWrite16(150, 0, 0, 7);
+		regWrite16(151, 255, 255, 7);
+		regWrite16(152, 0, 0, 7);
+		regWrite16(153, 255, 255, 7);
+		regWrite16(154, 0, 0, 7);
+		regWrite16(155, 255, 255, 7);
+		regWrite16(156, 0, 0, 7);
+		regWrite16(160, 0, 0, 7);
+		regWrite16(161, 255, 255, 7);
+		regWrite16(162, 0, 0, 7);
+		regWrite16(163, 255, 255, 7);
+		regWrite16(168, 0, 0, 7);
+		regWrite16(169, 255, 255, 7);
+		regWrite16(170, 0, 0, 7);
+		regWrite16(171, 255, 255, 7);
+		regWrite16(7, 0, 0, 7);       // Disable TX/RX and set radio channel to 0
+	}
+}
+
+/**************************************************************************/
+// Set sync word
+/**************************************************************************/
+void LT8900MiLightRadio::vSetSyncWord(uint16_t syncWord3, uint16_t syncWord2, uint16_t syncWord1, uint16_t syncWord0)
+{
+	uiWriteRegister(R_SYNCWORD1, syncWord0);
+	uiWriteRegister(R_SYNCWORD2, syncWord1);
+	uiWriteRegister(R_SYNCWORD3, syncWord1);
+	uiWriteRegister(R_SYNCWORD4, syncWord3);
+}
+
+/**************************************************************************/
+// Low level register write with delay
+/**************************************************************************/
+void LT8900MiLightRadio::regWrite16(byte ADDR, byte V1, byte V2, byte WAIT)
+{
+	digitalWrite(_csPin, LOW);
+	SPI.transfer(ADDR);
+	SPI.transfer(V1);
+	SPI.transfer(V2);
+	digitalWrite(_csPin, HIGH);
+	delayMicroseconds(WAIT);
+}
+
+
+/**************************************************************************/
+// Low level register read
+/**************************************************************************/
+uint16_t LT8900MiLightRadio::uiReadRegister(uint8_t reg)
+{
+	SPI.setDataMode(SPI_MODE1);
+	digitalWrite(_csPin, LOW);
+	SPI.transfer(REGISTER_READ | (REGISTER_MASK & reg));
+	uint8_t high = SPI.transfer(0x00);
+	uint8_t low = SPI.transfer(0x00);
+
+	digitalWrite(_csPin, HIGH);
+
+	SPI.setDataMode(SPI_MODE0);
+	return (high << 8 | low);
+}
+
+
+/**************************************************************************/
+// Low level 16bit register write
+/**************************************************************************/
+uint8_t LT8900MiLightRadio::uiWriteRegister(uint8_t reg, uint16_t data)
+{
+	uint8_t high = data >> 8;
+	uint8_t low = data & 0xFF;
+
+	digitalWrite(_csPin, LOW);
+
+	uint8_t result = SPI.transfer(REGISTER_WRITE | (REGISTER_MASK & reg));
+	SPI.transfer(high);
+	SPI.transfer(low);
+
+	digitalWrite(_csPin, HIGH);
+
+	return result;
+}
+
+/**************************************************************************/
+// Start listening on specified channel and syncword
+/**************************************************************************/
+void LT8900MiLightRadio::vStartListening(uint uiChannelToListenTo)
+{
+  _dupes_received = 0;
+  vSetSyncWord(_config.syncword3, 0,0,_config.syncword0);
+	//vSetChannel(uiChannelToListenTo);
+
+  _channel = uiChannelToListenTo;
+
+  vResumeRX();
+	delay(5);
+}
+
+/**************************************************************************/
+// Resume listening - without changing the channel and syncword
+/**************************************************************************/
+void LT8900MiLightRadio::vResumeRX(void)
+{
+  _dupes_received = 0;
+	uiWriteRegister(R_CHANNEL, _channel & CHANNEL_MASK);   //turn off rx/tx
+	delay(3);
+	uiWriteRegister(R_FIFO_CONTROL, 0x0080);  //flush rx
+	uiWriteRegister(R_CHANNEL, (_channel & CHANNEL_MASK) | _BV(CHANNEL_RX_BIT));   //enable RX
+}
+
+/**************************************************************************/
+// Check if data is available using the hardware pin PKT_FLAG
+/**************************************************************************/
+bool LT8900MiLightRadio::bAvailablePin() {
+  return digitalRead(_pin_pktflag) > 0;
+}
+
+/**************************************************************************/
+// Check if data is available using the PKT_FLAG state in the status register
+/**************************************************************************/
+bool LT8900MiLightRadio::bAvailableRegister() {
+	//read the PKT_FLAG state; this can also be done with a hard wire.
+	uint16_t value = uiReadRegister(R_STATUS);
+
+  if (bitRead(value, STATUS_CRC_BIT) != 0) {
+#ifdef DEBUG_PRINTF
+    Serial.println(F("LT8900: CRC failed"));
+#endif
+    vResumeRX();
+    return false;
+  }
+
+  return (value & STATUS_PKT_BIT_MASK) > 0;
+}
+
+/**************************************************************************/
+// Read the RX buffer
+/**************************************************************************/
+int LT8900MiLightRadio::iReadRXBuffer(uint8_t *buffer, size_t maxBuffer) {
+  size_t bufferIx = 0;
+  uint16_t data;
+
+  if (_currentPacketLen == 0) {
+    if (! available()) {
+      return -1;
+    }
+
+    data = uiReadRegister(R_FIFO);
+
+    _currentPacketLen = (data >> 8);
+    _currentPacketPos = 1;
+
+    buffer[bufferIx++] = (data & 0xFF);
+  }
+
+  while (_currentPacketPos < _currentPacketLen && (bufferIx+1) < maxBuffer) {
+    data = uiReadRegister(R_FIFO);
+    buffer[bufferIx++] = data >> 8;
+    buffer[bufferIx++] = data & 0xFF;
+
+    _currentPacketPos += 2;
+  }
+
+#ifdef DEBUG_PRINTF
+  printf_P(
+    PSTR("Read %d/%d bytes in RX, read %d bytes into buffer\n"),
+    _currentPacketPos,
+    _currentPacketLen,
+    bufferIx
+  );
+#endif
+
+  if (_currentPacketPos >= _currentPacketLen) {
+    _currentPacketPos = 0;
+    _currentPacketLen = 0;
+  }
+
+  return bufferIx;
+}
+
+
+/**************************************************************************/
+// Set the active channel for the radio module
+/**************************************************************************/
+void LT8900MiLightRadio::vSetChannel(uint8_t channel)
+{
+	_channel = channel;
+	uiWriteRegister(R_CHANNEL, (_channel & CHANNEL_MASK));
+}
+
+/**************************************************************************/
+// Startup
+/**************************************************************************/
+int LT8900MiLightRadio::begin()
+{
+  vSetChannel(_config.channels[0]);
+  configure();
+  available();
+  return 0;
+}
+
+/**************************************************************************/
+// Configure the module according to type, and start listening
+/**************************************************************************/
+int LT8900MiLightRadio::configure()
+{
+  vInitRadioModule(_config.type);
+  vSetSyncWord(_config.syncword3, 0,0,_config.syncword0);
+  vStartListening(_config.channels[0]);
+  return 0;
+}
+
+/**************************************************************************/
+// Check if data is available
+/**************************************************************************/
+bool LT8900MiLightRadio::available()
+{
+  if (_currentPacketPos < _currentPacketLen) {
+    return true;
+  }
+
+  return bAvailablePin() && bAvailableRegister();
+}
+
+/**************************************************************************/
+// Read received data from buffer to upper layer
+/**************************************************************************/
+int LT8900MiLightRadio::read(uint8_t frame[], size_t &frame_length)
+{
+  if (!available()) {
+    frame_length = 0;
+    return -1;
+  }
+
+  #ifdef DEBUG_PRINTF
+  Serial.println(F("LT8900: Radio was available, reading packet..."));
+  #endif
+
+  uint8_t buf[_config.getPacketLength()];
+  int packetSize = iReadRXBuffer(buf, _config.getPacketLength());
+
+  if (packetSize > 0) {
+    frame_length = packetSize;
+    memcpy(frame, buf, packetSize);
+  }
+
+  vResumeRX();
+
+  return packetSize;
+}
+
+/**************************************************************************/
+// Write data
+/**************************************************************************/
+int LT8900MiLightRadio::write(uint8_t frame[], size_t frame_length)
+{
+  if (frame_length > sizeof(_out_packet) - 1) {
+    return -1;
+  }
+
+  memcpy(_out_packet + 1, frame, frame_length);
+  _out_packet[0] = frame_length;
+
+  SPI.setDataMode(SPI_MODE1);
+
+  int retval = resend();
+  yield();
+
+  SPI.setDataMode(SPI_MODE0);
+
+  if (retval < 0) {
+    return retval;
+  }
+  return frame_length;
+}
+
+/**************************************************************************/
+// Handle the transmission to regarding to freq diversity and repeats
+/**************************************************************************/
+int LT8900MiLightRadio::resend()
+{
+  byte Length =  _out_packet[0];
+
+  for (size_t i = 0; i < MiLightRadioConfig::NUM_CHANNELS; i++)
+  {
+    sendPacket(_out_packet, Length, _config.channels[i]);
+    delayMicroseconds(DEFAULT_TIME_BETWEEN_RETRANSMISSIONS_uS);
+  }
+
+  return 0;
+}
+
+/**************************************************************************/
+// The actual transmit happens here
+/**************************************************************************/
+bool LT8900MiLightRadio::sendPacket(uint8_t *data, size_t packetSize, byte byChannel)
+{
+  if(_bConnected) // Must be connected to module otherwise it might lookup waiting for _pin_pktflag
+  {
+    if (packetSize < 1 || packetSize > 255)
+    {
+      return false;
+    }
+
+    uiWriteRegister(R_CHANNEL, 0x0000);
+    uiWriteRegister(R_FIFO_CONTROL, 0x8080);  //flush tx and RX
+
+    digitalWrite(_csPin, LOW);        // Enable PL1167 SPI transmission
+    SPI.transfer(R_FIFO);             // Start writing PL1167's FIFO Data register
+    SPI.transfer(packetSize);         // Length of data buffer: x bytes
+
+    for (byte iCounter = 0; iCounter < packetSize; iCounter++)
+    {
+      SPI.transfer((data[1+iCounter]));
+    }
+    digitalWrite(_csPin, HIGH);  // Disable PL1167 SPI transmission
+    delayMicroseconds(10);
+
+    uiWriteRegister(R_CHANNEL,  (byChannel & CHANNEL_MASK) | _BV(CHANNEL_TX_BIT));   //enable RX
+
+    //Wait until the packet is sent.
+    while (digitalRead(_pin_pktflag) == 0)
+    {
+        //do nothing.
+    }
+
+    return true;
+  }
+}
+
+const MiLightRadioConfig& LT8900MiLightRadio::config() {
+  return _config;
+}

+ 91 - 0
lib/MiLight/LT8900MiLightRadio.h

@@ -0,0 +1,91 @@
+#ifdef ARDUINO
+#include "Arduino.h"
+#else
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#endif
+
+#include <MiLightRadioConfig.h>
+#include <MiLightButtons.h>
+#include <MiLightRadio.h>
+
+//#define DEBUG_PRINTF
+
+// Register defines
+#define REGISTER_READ       0b10000000  //bin
+#define REGISTER_WRITE      0b00000000  //bin
+#define REGISTER_MASK       0b01111111  //bin
+
+#define R_CHANNEL           7
+#define CHANNEL_RX_BIT      7
+#define CHANNEL_TX_BIT      8
+#define CHANNEL_MASK        0b01111111  ///bin
+
+#define STATUS_PKT_BIT_MASK	0x40
+
+#define R_STATUS            48
+#define STATUS_CRC_BIT      15
+
+#define R_FIFO              50
+#define R_FIFO_CONTROL      52
+
+#define R_SYNCWORD1         36
+#define R_SYNCWORD2         37
+#define R_SYNCWORD3         38
+#define R_SYNCWORD4         39
+
+//#define DEFAULT_TIME_BETWEEN_RETRANSMISSIONS_uS	350
+#define DEFAULT_TIME_BETWEEN_RETRANSMISSIONS_uS	0
+
+#ifndef MILIGHTRADIOPL1167_LT8900_H_
+#define MILIGHTRADIOPL1167_LT8900_H_
+
+class LT8900MiLightRadio : public MiLightRadio {
+  public:
+    LT8900MiLightRadio(byte byCSPin, byte byResetPin, byte byPktFlag, const MiLightRadioConfig& config);
+
+    virtual int begin();
+    virtual bool available();
+    virtual int read(uint8_t frame[], size_t &frame_length);
+    virtual int write(uint8_t frame[], size_t frame_length);
+    virtual int resend();
+    virtual int configure();
+    virtual const MiLightRadioConfig& config();
+
+  private:
+
+    void vInitRadioModule(MiLightRadioType type);
+    void vSetSyncWord(uint16_t syncWord3, uint16_t syncWord2, uint16_t syncWord1, uint16_t syncWord0);
+    uint16_t uiReadRegister(uint8_t reg);
+    void regWrite16(byte ADDR, byte V1, byte V2, byte WAIT);
+    uint8_t uiWriteRegister(uint8_t reg, uint16_t data);
+
+    bool bAvailablePin(void);
+    bool bAvailableRegister(void);
+    void vStartListening(uint uiChannelToListenTo);
+    void vResumeRX(void);
+    int iReadRXBuffer(uint8_t *buffer, size_t maxBuffer);
+    void vSetChannel(uint8_t channel);
+    void vGenericSendPacket(int iMode, int iLength, byte *pbyFrame, byte byChannel );
+    bool bCheckRadioConnection(void);
+    bool sendPacket(uint8_t *data, size_t packetSize,byte byChannel);
+
+    byte _pin_pktflag;
+    byte _csPin;
+    bool _bConnected;
+
+    const MiLightRadioConfig& _config;
+
+    uint8_t _channel;
+    uint8_t _packet[10];
+    uint8_t _out_packet[10];
+    bool _waiting;
+    int _dupes_received;
+    size_t _currentPacketLen;
+    size_t _currentPacketPos;
+};
+
+
+
+#endif

+ 195 - 36
lib/MiLight/MiLightClient.cpp

@@ -1,41 +1,70 @@
 #include <MiLightClient.h>
 #include <MiLightRadioConfig.h>
 #include <Arduino.h>
+#include <RGBConverter.h>
+
+#define COLOR_TEMP_MAX_MIREDS 370
+#define COLOR_TEMP_MIN_MIREDS 153
+
+MiLightClient::MiLightClient(MiLightRadioFactory* radioFactory)
+  : resendCount(MILIGHT_DEFAULT_RESEND_COUNT),
+    currentRadio(NULL),
+    numRadios(MiLightRadioConfig::NUM_CONFIGS)
+{
+  radios = new MiLightRadio*[numRadios];
+
+  for (size_t i = 0; i < numRadios; i++) {
+    radios[i] = radioFactory->create(*MiLightRadioConfig::ALL_CONFIGS[i]);
+  }
+
+  this->currentRadio = radios[0];
+  this->currentRadio->configure();
+}
+
+void MiLightClient::begin() {
+  for (size_t i = 0; i < numRadios; i++) {
+    radios[i]->begin();
+  }
+}
+
+void MiLightClient::setHeld(bool held) {
+  formatter->setHeld(held);
+}
 
 MiLightRadio* MiLightClient::switchRadio(const MiLightRadioType type) {
-  RadioStack* stack = NULL;
-  
+  MiLightRadio* radio = NULL;
+
   for (int i = 0; i < numRadios; i++) {
-    if (radios[i]->config.type == type) {
-      stack = radios[i];
+    if (this->radios[i]->config().type == type) {
+      radio = radios[i];
       break;
     }
   }
-  
-  if (stack != NULL) {
-    MiLightRadio *radio = stack->getRadio();
-    
-    if (currentRadio->config.type != stack->config.type) {
+
+  if (radio != NULL) {
+    if (currentRadio != radio) {
       radio->configure();
     }
-    
-    currentRadio = stack;
-    formatter = stack->config.packetFormatter;
+
+    this->currentRadio = radio;
+    this->formatter = radio->config().packetFormatter;
+
     return radio;
   } else {
     Serial.print(F("MiLightClient - tried to get radio for unknown type: "));
     Serial.println(type);
   }
-  
+
   return NULL;
 }
 
-void MiLightClient::prepare(MiLightRadioConfig& config, 
-  const uint16_t deviceId, 
+
+void MiLightClient::prepare(MiLightRadioConfig& config,
+  const uint16_t deviceId,
   const uint8_t groupId) {
-  
+
   switchRadio(config.type);
-  
+
   if (deviceId >= 0 && groupId >= 0) {
     formatter->prepare(deviceId, groupId);
   }
@@ -45,41 +74,49 @@ void MiLightClient::setResendCount(const unsigned int resendCount) {
   this->resendCount = resendCount;
 }
 
+
 bool MiLightClient::available() {
   if (currentRadio == NULL) {
     return false;
   }
-  
-  return currentRadio->getRadio()->available();
-}
 
+  return currentRadio->available();
+}
 void MiLightClient::read(uint8_t packet[]) {
   if (currentRadio == NULL) {
     return;
   }
-  
-  size_t length;
-  currentRadio->getRadio()->read(packet, length);
+
+  size_t length = currentRadio->config().getPacketLength();
+
+  currentRadio->read(packet, length);
 }
 
 void MiLightClient::write(uint8_t packet[]) {
   if (currentRadio == NULL) {
     return;
   }
-  
+
 #ifdef DEBUG_PRINTF
   printf("Sending packet: ");
-  for (int i = 0; i < currentRadio->config.getPacketLength(); i++) {
+  for (int i = 0; i < currentRadio->config().getPacketLength(); i++) {
     printf("%02X", packet[i]);
   }
   printf("\n");
+  int iStart = millis();
 #endif
-  
+
   for (int i = 0; i < this->resendCount; i++) {
-    currentRadio->getRadio()->write(packet, currentRadio->config.getPacketLength());
+    currentRadio->write(packet, currentRadio->config().getPacketLength());
   }
+
+#ifdef DEBUG_PRINTF
+  int iElapsed = millis() - iStart;
+  Serial.print("Elapsed: ");
+  Serial.println(iElapsed);
+#endif
 }
-    
+
 void MiLightClient::updateColorRaw(const uint8_t color) {
   formatter->updateColorRaw(color);
   flushPacket();
@@ -94,7 +131,7 @@ void MiLightClient::updateBrightness(const uint8_t brightness) {
   formatter->updateBrightness(brightness);
   flushPacket();
 }
-    
+
 void MiLightClient::updateMode(uint8_t mode) {
   formatter->updateMode(mode);
   flushPacket();
@@ -118,7 +155,7 @@ void MiLightClient::modeSpeedUp() {
   formatter->modeSpeedUp();
   flushPacket();
 }
-    
+
 void MiLightClient::updateStatus(MiLightStatus status, uint8_t groupId) {
   formatter->updateStatus(status, groupId);
   flushPacket();
@@ -139,6 +176,11 @@ void MiLightClient::updateColorWhite() {
   flushPacket();
 }
 
+void MiLightClient::enableNightMode() {
+  formatter->enableNightMode();
+  flushPacket();
+}
+
 void MiLightClient::pair() {
   formatter->pair();
   flushPacket();
@@ -148,7 +190,7 @@ void MiLightClient::unpair() {
   formatter->unpair();
   flushPacket();
 }
-    
+
 void MiLightClient::increaseBrightness() {
   formatter->increaseBrightness();
   flushPacket();
@@ -179,27 +221,144 @@ void MiLightClient::command(uint8_t command, uint8_t arg) {
   flushPacket();
 }
 
+void MiLightClient::update(const JsonObject& request) {
+  if (request.containsKey("status") || request.containsKey("state")) {
+    String strStatus;
+
+    if (request.containsKey("status")) {
+      strStatus = request.get<char*>("status");
+    } else {
+      strStatus = request.get<char*>("state");
+    }
+
+    MiLightStatus status = (strStatus.equalsIgnoreCase("on") || strStatus.equalsIgnoreCase("true")) ? ON : OFF;
+    this->updateStatus(status);
+  }
+
+  if (request.containsKey("command")) {
+    this->handleCommand(request["command"]);
+  }
+
+  if (request.containsKey("commands")) {
+    JsonArray& commands = request["commands"];
+
+    if (commands.success()) {
+      for (size_t i = 0; i < commands.size(); i++) {
+        this->handleCommand(commands.get<String>(i));
+      }
+    }
+  }
+
+  if (request.containsKey("hue")) {
+    this->updateHue(request["hue"]);
+  }
+  if (request.containsKey("saturation")) {
+    this->updateSaturation(request["saturation"]);
+  }
+
+  // Convert RGB to HSV
+  if (request.containsKey("color")) {
+    JsonObject& color = request["color"];
+
+    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 (request.containsKey("level")) {
+    this->updateBrightness(request["level"]);
+  }
+  // HomeAssistant
+  if (request.containsKey("brightness")) {
+    uint8_t scaledBrightness = round(request.get<uint8_t>("brightness") * (100/255.0));
+    this->updateBrightness(scaledBrightness);
+  }
+
+  if (request.containsKey("temperature")) {
+    this->updateTemperature(request["temperature"]);
+  }
+  // HomeAssistant
+  if (request.containsKey("color_temp")) {
+    // MiLight CCT bulbs range from 2700K-6500K, or ~370.3-153.8 mireds. Note
+    // that mireds are inversely correlated with color temperature.
+    uint32_t tempMireds = request["color_temp"];
+    tempMireds = tempMireds > COLOR_TEMP_MAX_MIREDS ? COLOR_TEMP_MAX_MIREDS : tempMireds;
+    tempMireds = tempMireds < COLOR_TEMP_MIN_MIREDS ? COLOR_TEMP_MIN_MIREDS : tempMireds;
+
+    uint8_t scaledTemp = round(
+      100*
+      (tempMireds - COLOR_TEMP_MIN_MIREDS)
+        /
+      static_cast<double>(COLOR_TEMP_MAX_MIREDS - COLOR_TEMP_MIN_MIREDS)
+    );
+
+    this->updateTemperature(100 - scaledTemp);
+  }
+
+  if (request.containsKey("mode")) {
+    this->updateMode(request["mode"]);
+  }
+}
+
+void MiLightClient::handleCommand(const String& command) {
+  if (command == "unpair") {
+    this->unpair();
+  } else if (command == "pair") {
+    this->pair();
+  } else if (command == "set_white") {
+    this->updateColorWhite();
+  } else if (command == "night_mode") {
+    this->enableNightMode();
+  } else if (command == "level_up") {
+    this->increaseBrightness();
+  } else if (command == "level_down") {
+    this->decreaseBrightness();
+  } else if (command == "temperature_up") {
+    this->increaseTemperature();
+  } else if (command == "temperature_down") {
+    this->decreaseTemperature();
+  } else if (command == "next_mode") {
+    this->nextMode();
+  } else if (command == "previous_mode") {
+    this->previousMode();
+  } else if (command == "mode_speed_down") {
+    this->modeSpeedDown();
+  } else if (command == "mode_speed_up") {
+    this->modeSpeedUp();
+  }
+}
+
 void MiLightClient::formatPacket(uint8_t* packet, char* buffer) {
   formatter->format(packet, buffer);
 }
-    
+
 void MiLightClient::flushPacket() {
   PacketStream& stream = formatter->buildPackets();
   const size_t prevNumRepeats = this->resendCount;
-  
+
   // When sending multiple packets, normalize the number of repeats
   if (stream.numPackets > 1) {
     setResendCount(MILIGHT_DEFAULT_RESEND_COUNT);
   }
-  
+
   while (stream.hasNext()) {
     write(stream.next());
-    
+
     if (stream.hasNext()) {
       delay(10);
     }
   }
-  
+
   setResendCount(prevNumRepeats);
   formatter->reset();
 }

+ 66 - 79
lib/MiLight/MiLightClient.h

@@ -1,92 +1,79 @@
 #include <Arduino.h>
 #include <MiLightRadio.h>
-#include <PL1167_nRF24.h>
-#include <RF24.h>
+#include <MiLightRadioFactory.h>
 #include <MiLightButtons.h>
-#include <RadioStack.h>
+#include <Settings.h>
 
 #ifndef _MILIGHTCLIENT_H
 #define _MILIGHTCLIENT_H
 
-// #define DEBUG_PRINTF
+//#define DEBUG_PRINTF
 
 #define MILIGHT_DEFAULT_RESEND_COUNT 10
 
 class MiLightClient {
-  public:
-    MiLightClient(uint8_t cePin, uint8_t csnPin)
-      : rf(RF24(cePin, csnPin)),
-      resendCount(MILIGHT_DEFAULT_RESEND_COUNT),
-      currentRadio(NULL),
-      numRadios(MiLightRadioConfig::NUM_CONFIGS)
-    {
-      radios = new RadioStack*[numRadios];
-      
-      for (size_t i = 0; i < numRadios; i++) {
-        radios[i] = new RadioStack(rf, *MiLightRadioConfig::ALL_CONFIGS[i]);
-      }
-      
-      currentRadio = radios[0];
-      currentRadio->getRadio()->configure();
-    }
-    
-    ~MiLightClient() {
-      delete[] radios;
-    }
-    
-    void begin() {
-      for (size_t i = 0; i < numRadios; i++) {
-        radios[i]->getRadio()->begin();
-      }
-    }
-    
-    void prepare(MiLightRadioConfig& config, const uint16_t deviceId = -1, const uint8_t groupId = -1);
-    void setResendCount(const unsigned int resendCount);
-    bool available();
-    void read(uint8_t packet[]);
-    void write(uint8_t packet[]);
-    
-    // Common methods
-    void updateStatus(MiLightStatus status);
-    void updateStatus(MiLightStatus status, uint8_t groupId);
-    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);
-    void updateBrightness(const uint8_t brightness);
-    void updateColorWhite();
-    void updateColorRaw(const uint8_t color);
-
-    // CCT methods
-    void updateTemperature(const uint8_t colorTemperature);
-    void decreaseTemperature();
-    void increaseTemperature();
-    void increaseBrightness();
-    void decreaseBrightness();
-    
-    void updateSaturation(const uint8_t saturation);
-    
-    void formatPacket(uint8_t* packet, char* buffer);
-    
-    
-  protected:
-    RF24 rf;
-    RadioStack** radios;
-    RadioStack* currentRadio;
-    PacketFormatter* formatter;
-    const size_t numRadios;
-    
-    unsigned int resendCount;
-    
-    MiLightRadio* switchRadio(const MiLightRadioType type);
-    void flushPacket();
+public:
+  MiLightClient(MiLightRadioFactory* radioFactory);
+
+  ~MiLightClient() {
+    delete[] radios;
+  }
+
+  void begin();
+  void prepare(MiLightRadioConfig& config, const uint16_t deviceId = -1, const uint8_t groupId = -1);
+
+  void setResendCount(const unsigned int resendCount);
+  bool available();
+  void read(uint8_t packet[]);
+  void write(uint8_t packet[]);
+
+  void setHeld(bool held);
+
+  // Common methods
+  void updateStatus(MiLightStatus status);
+  void updateStatus(MiLightStatus status, uint8_t groupId);
+  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);
+  void updateBrightness(const uint8_t brightness);
+  void updateColorWhite();
+  void updateColorRaw(const uint8_t color);
+  void enableNightMode();
+
+  // CCT methods
+  void updateTemperature(const uint8_t colorTemperature);
+  void decreaseTemperature();
+  void increaseTemperature();
+  void increaseBrightness();
+  void decreaseBrightness();
+
+  void updateSaturation(const uint8_t saturation);
+
+  void formatPacket(uint8_t* packet, char* buffer);
+
+  void update(const JsonObject& object);
+  void handleCommand(const String& command);
+
+protected:
+
+  MiLightRadio** radios;
+  MiLightRadio* currentRadio;
+  PacketFormatter* formatter;
+  const size_t numRadios;
+
+  unsigned int resendCount;
+
+  MiLightRadio* switchRadio(const MiLightRadioType type);
+
+  void flushPacket();
 };
 
-#endif
+#endif

+ 13 - 32
lib/MiLight/MiLightRadio.h

@@ -1,49 +1,30 @@
-/*
- * MiLightRadio.h
- *
- *  Created on: 29 May 2015
- *      Author: henryk
- */
 
 #ifdef ARDUINO
 #include "Arduino.h"
 #else
 #include <stdint.h>
 #include <stdlib.h>
-#include <string.h>
 #endif
 
-#include "AbstractPL1167.h"
 #include <MiLightRadioConfig.h>
 
-// #define DEBUG_PRINTF
-
-#ifndef MILIGHTRADIO_H_
-#define MILIGHTRADIO_H_
+#ifndef _MILIGHT_RADIO_H_
+#define _MILIGHT_RADIO_H_
 
 class MiLightRadio {
   public:
-    MiLightRadio(AbstractPL1167 &pl1167, const MiLightRadioConfig& config);
-    
-    int begin();
-    bool available();
-    int read(uint8_t frame[], size_t &frame_length);
-    int dupesReceived();
-    int write(uint8_t frame[], size_t frame_length);
-    int resend();
-    int configure();
-    
-  private:
-    AbstractPL1167 &_pl1167;
-    const MiLightRadioConfig& config;
-    uint32_t _prev_packet_id;
-
-    uint8_t _packet[10];
-    uint8_t _out_packet[10];
-    bool _waiting;
-    int _dupes_received;
+
+    virtual int begin();
+    virtual bool available();
+    virtual int read(uint8_t frame[], size_t &frame_length);
+    virtual int write(uint8_t frame[], size_t frame_length);
+    virtual int resend();
+    virtual int configure();
+    virtual const MiLightRadioConfig& config();
+
 };
 
 
 
-#endif /* MILIGHTRADIO_H_ */
+
+#endif

+ 32 - 0
lib/MiLight/MiLightRadioFactory.cpp

@@ -0,0 +1,32 @@
+#include <MiLightRadioFactory.h>
+
+MiLightRadioFactory* MiLightRadioFactory::fromSettings(const Settings& settings) {
+  switch (settings.radioInterfaceType) {
+    case nRF24:
+      return new NRF24Factory(settings.csnPin, settings.cePin);
+
+    case LT8900:
+      return new LT8900Factory(settings.csnPin, settings.resetPin, settings.cePin);
+
+    default:
+      return NULL;
+  }
+}
+
+NRF24Factory::NRF24Factory(uint8_t csnPin, uint8_t cePin)
+  : rf24(RF24(cePin, csnPin))
+{ }
+
+MiLightRadio* NRF24Factory::create(const MiLightRadioConfig &config) {
+  return new NRF24MiLightRadio(rf24, config);
+}
+
+LT8900Factory::LT8900Factory(uint8_t csPin, uint8_t resetPin, uint8_t pktFlag)
+  : _csPin(csPin),
+    _resetPin(resetPin),
+    _pktFlag(pktFlag)
+{ }
+
+MiLightRadio* LT8900Factory::create(const MiLightRadioConfig& config) {
+  return new LT8900MiLightRadio(_csPin, _resetPin, _pktFlag, config);
+}

+ 49 - 0
lib/MiLight/MiLightRadioFactory.h

@@ -0,0 +1,49 @@
+#include <RF24.h>
+#include <PL1167_nRF24.h>
+#include <MiLightRadioConfig.h>
+#include <MiLightRadio.h>
+#include <NRF24MiLightRadio.h>
+#include <LT8900MiLightRadio.h>
+#include <Settings.h>
+
+#ifndef _MILIGHT_RADIO_FACTORY_H
+#define _MILIGHT_RADIO_FACTORY_H
+
+class MiLightRadioFactory {
+public:
+
+  virtual MiLightRadio* create(const MiLightRadioConfig& config) = 0;
+
+  static MiLightRadioFactory* fromSettings(const Settings& settings);
+  
+};
+
+class NRF24Factory : public MiLightRadioFactory {
+public:
+
+  NRF24Factory(uint8_t cePin, uint8_t csnPin);
+
+  virtual MiLightRadio* create(const MiLightRadioConfig& config);
+
+protected:
+
+  RF24 rf24;
+
+};
+
+class LT8900Factory : public MiLightRadioFactory {
+public:
+
+  LT8900Factory(uint8_t csPin, uint8_t resetPin, uint8_t pktFlag);
+
+  virtual MiLightRadio* create(const MiLightRadioConfig& config);
+
+protected:
+
+  uint8_t _csPin;
+  uint8_t _resetPin;
+  uint8_t _pktFlag;
+
+};
+
+#endif

+ 28 - 36
lib/MiLight/MiLightRadio.cpp

@@ -1,26 +1,22 @@
-/*
- * MiLightRadio.cpp
- *
- *  Created on: 29 May 2015
- *      Author: henryk
- */
+// Adapated from code from henryk
 
-#include "MiLightRadio.h"
+#include <PL1167_nRF24.h>
+#include <NRF24MiLightRadio.h>
 
 #define PACKET_ID(packet, packet_length) ( (packet[1] << 8) | packet[packet_length - 1] )
 
-MiLightRadio::MiLightRadio(AbstractPL1167 &pl1167, const MiLightRadioConfig& config)
-  : _pl1167(pl1167), config(config) {
-  _waiting = false;
-}
+NRF24MiLightRadio::NRF24MiLightRadio(RF24& rf24, const MiLightRadioConfig& config)
+  : _pl1167(PL1167_nRF24(rf24)),
+    _waiting(false),
+    _config(config)
+{ }
 
-int MiLightRadio::begin()
-{
+int NRF24MiLightRadio::begin() {
   int retval = _pl1167.open();
   if (retval < 0) {
     return retval;
   }
-  
+
   retval = configure();
   if (retval < 0) {
     return retval;
@@ -31,7 +27,7 @@ int MiLightRadio::begin()
   return 0;
 }
 
-int MiLightRadio::configure() {
+int NRF24MiLightRadio::configure() {
   int retval = _pl1167.setCRC(true);
   if (retval < 0) {
     return retval;
@@ -47,38 +43,38 @@ int MiLightRadio::configure() {
     return retval;
   }
 
-  retval = _pl1167.setSyncword(config.syncword0, config.syncword3);
+  retval = _pl1167.setSyncword(_config.syncword0, _config.syncword3);
   if (retval < 0) {
     return retval;
   }
 
-  // +1 to be able to buffer the length 
-  retval = _pl1167.setMaxPacketLength(config.getPacketLength() + 1);
+  // +1 to be able to buffer the length
+  retval = _pl1167.setMaxPacketLength(_config.getPacketLength() + 1);
   if (retval < 0) {
     return retval;
   }
-  
+
   return 0;
 }
 
-bool MiLightRadio::available() {
+bool NRF24MiLightRadio::available() {
   if (_waiting) {
 #ifdef DEBUG_PRINTF
   printf("_waiting\n");
 #endif
     return true;
   }
-  
-  if (_pl1167.receive(config.channels[0]) > 0) {
+
+  if (_pl1167.receive(_config.channels[0]) > 0) {
 #ifdef DEBUG_PRINTF
-  printf("MiLightRadio - received packet!\n");
+  printf("NRF24MiLightRadio - received packet!\n");
 #endif
     size_t packet_length = sizeof(_packet);
     if (_pl1167.readFIFO(_packet, packet_length) < 0) {
       return false;
     }
 #ifdef DEBUG_PRINTF
-  printf("MiLightRadio - Checking packet length (expecting %d, is %d)\n", _packet[0] + 1U, packet_length);
+  printf("NRF24MiLightRadio - Checking packet length (expecting %d, is %d)\n", _packet[0] + 1U, packet_length);
 #endif
     if (packet_length == 0 || packet_length != _packet[0] + 1U) {
       return false;
@@ -98,13 +94,7 @@ bool MiLightRadio::available() {
   return _waiting;
 }
 
-int MiLightRadio::dupesReceived()
-{
-  return _dupes_received;
-}
-
-
-int MiLightRadio::read(uint8_t frame[], size_t &frame_length)
+int NRF24MiLightRadio::read(uint8_t frame[], size_t &frame_length)
 {
   if (!_waiting) {
     frame_length = 0;
@@ -125,8 +115,7 @@ int MiLightRadio::read(uint8_t frame[], size_t &frame_length)
   return _packet[0];
 }
 
-int MiLightRadio::write(uint8_t frame[], size_t frame_length)
-{
+int NRF24MiLightRadio::write(uint8_t frame[], size_t frame_length) {
   if (frame_length > sizeof(_out_packet) - 1) {
     return -1;
   }
@@ -141,11 +130,14 @@ int MiLightRadio::write(uint8_t frame[], size_t frame_length)
   return frame_length;
 }
 
-int MiLightRadio::resend()
-{
+int NRF24MiLightRadio::resend() {
   for (size_t i = 0; i < MiLightRadioConfig::NUM_CHANNELS; i++) {
     _pl1167.writeFIFO(_out_packet, _out_packet[0] + 1);
-    _pl1167.transmit(config.channels[i]);
+    _pl1167.transmit(_config.channels[i]);
   }
   return 0;
 }
+
+const MiLightRadioConfig& NRF24MiLightRadio::config() {
+  return _config;
+}

+ 43 - 0
lib/MiLight/NRF24MiLightRadio.h

@@ -0,0 +1,43 @@
+#ifdef ARDUINO
+#include "Arduino.h"
+#else
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#endif
+
+#include <RF24.h>
+#include <PL1167_nRF24.h>
+#include <MiLightRadioConfig.h>
+#include <MiLightRadio.h>
+
+#ifndef _NRF24_MILIGHT_RADIO_H_
+#define _NRF24_MILIGHT_RADIO_H_
+
+class NRF24MiLightRadio : public MiLightRadio {
+  public:
+    NRF24MiLightRadio(RF24& rf, const MiLightRadioConfig& config);
+
+    int begin();
+    bool available();
+    int read(uint8_t frame[], size_t &frame_length);
+    int dupesReceived();
+    int write(uint8_t frame[], size_t frame_length);
+    int resend();
+    int configure();
+    const MiLightRadioConfig& config();
+
+  private:
+    PL1167_nRF24 _pl1167;
+    const MiLightRadioConfig& _config;
+    uint32_t _prev_packet_id;
+
+    uint8_t _packet[10];
+    uint8_t _out_packet[10];
+    bool _waiting;
+    int _dupes_received;
+};
+
+
+
+#endif

+ 6 - 5
lib/MiLight/PL1167_nRF24.cpp

@@ -12,7 +12,8 @@ static uint8_t reverse_bits(uint8_t data);
 static void demangle_packet(uint8_t *in, uint8_t *out) ;
 
 PL1167_nRF24::PL1167_nRF24(RF24 &radio)
-  : _radio(radio) { }
+  : _radio(radio)
+{ }
 
 static const uint8_t pipe[] = {0xd1, 0x28, 0x5e, 0x55, 0x55};
 
@@ -266,7 +267,7 @@ int PL1167_nRF24::transmit(uint8_t channel)
       buffer_fill -= 8;
     }
   }
-  
+
   yield();
 
   _radio.write(tmp, outp);
@@ -374,14 +375,14 @@ int PL1167_nRF24::internal_receive()
   }
 
   memcpy(_packet, tmp, outp);
-  
+
   _packet_length = outp;
   _received = true;
-  
+
 #ifdef DEBUG_PRINTF
   printf("Successfully parsed packet of length %d\n", _packet_length);
 #endif
-  
+
   return outp;
 }
 

+ 2 - 3
lib/MiLight/PL1167_nRF24.h

@@ -9,7 +9,6 @@
 #include "Arduino.h"
 #endif
 
-#include "AbstractPL1167.h"
 #include "RF24.h"
 
 // #define DEBUG_PRINTF
@@ -17,9 +16,9 @@
 #ifndef PL1167_NRF24_H_
 #define PL1167_NRF24_H_
 
-class PL1167_nRF24 : public AbstractPL1167 {
+class PL1167_nRF24 {
   public:
-    PL1167_nRF24(RF24 &radio);
+    PL1167_nRF24(RF24& radio);
     int open();
     int setPreambleLength(uint8_t preambleLength);
     int setSyncword(uint16_t syncword0, uint16_t syncword3);

+ 25 - 18
lib/MiLight/PacketFormatter.cpp

@@ -7,8 +7,8 @@ PacketStream::PacketStream()
       numPackets(0),
       packetLength(0),
       currentPacket(0)
-  { }
-  
+{ }
+
 bool PacketStream::hasNext() {
   return currentPacket < numPackets;
 }
@@ -22,18 +22,23 @@ uint8_t* PacketStream::next() {
 PacketFormatter::PacketFormatter(const size_t packetLength, const size_t maxPackets)
   : packetLength(packetLength),
     numPackets(0),
-    currentPacket(NULL)
-{ 
+    currentPacket(NULL),
+    held(false)
+{
   packetStream.packetLength = packetLength;
   packetStream.packetStream = PACKET_BUFFER;
 }
-  
+
 void PacketFormatter::finalizePacket(uint8_t* packet) { }
-  
-void PacketFormatter::updateStatus(MiLightStatus status) { 
+
+void PacketFormatter::updateStatus(MiLightStatus status) {
   updateStatus(status, groupId);
 }
 
+void PacketFormatter::setHeld(bool held) {
+  this->held = held;
+}
+
 void PacketFormatter::updateStatus(MiLightStatus status, uint8_t groupId) { }
 void PacketFormatter::updateBrightness(uint8_t value) { }
 void PacketFormatter::updateMode(uint8_t value) { }
@@ -51,36 +56,37 @@ void PacketFormatter::increaseTemperature() { }
 void PacketFormatter::decreaseTemperature() { }
 void PacketFormatter::increaseBrightness() { }
 void PacketFormatter::decreaseBrightness() { }
+void PacketFormatter::enableNightMode() { }
 
 void PacketFormatter::updateTemperature(uint8_t value) { }
 void PacketFormatter::updateSaturation(uint8_t value) { }
-  
-void PacketFormatter::pair() { 
+
+void PacketFormatter::pair() {
   for (size_t i = 0; i < 5; i++) {
     updateStatus(ON);
   }
 }
 
-void PacketFormatter::unpair() { 
+void PacketFormatter::unpair() {
   pair();
 }
-  
+
 PacketStream& PacketFormatter::buildPackets() {
   if (numPackets > 0) {
     finalizePacket(currentPacket);
   }
-  
+
   packetStream.numPackets = numPackets;
   packetStream.currentPacket = 0;
-  
+
   return packetStream;
 }
-  
+
 void PacketFormatter::valueByStepFunction(StepFunction increase, StepFunction decrease, uint8_t numSteps, uint8_t value) {
   for (size_t i = 0; i < numSteps; i++) {
     (this->*decrease)();
   }
-  
+
   for (size_t i = 0; i < value; i++) {
     (this->*increase)();
   }
@@ -95,13 +101,14 @@ void PacketFormatter::prepare(uint16_t deviceId, uint8_t groupId) {
 void PacketFormatter::reset() {
   this->numPackets = 0;
   this->currentPacket = currentPacket;
+  this->held = false;
 }
 
 void PacketFormatter::pushPacket() {
   if (numPackets > 0) {
     finalizePacket(currentPacket);
   }
-  
+
   currentPacket = PACKET_BUFFER + (numPackets * packetLength);
   numPackets++;
   initializePacket(currentPacket);
@@ -123,7 +130,7 @@ void PacketFormatter::formatV1Packet(uint8_t const* packet, char* buffer) {
   buffer += sprintf_P(buffer, "b3            : %02X\n", packet[5]);
   buffer += sprintf_P(buffer, "Sequence Num. : %02X", packet[6]);
 }
-  
+
 size_t PacketFormatter::getPacketLength() const {
   return packetLength;
-}
+}

+ 24 - 20
lib/MiLight/PacketFormatter.h

@@ -6,14 +6,14 @@
 #define PACKET_FORMATTER_BUFFER_SIZE 48
 
 #ifndef _PACKET_FORMATTER_H
-#define _PACKET_FORMATTER_H 
+#define _PACKET_FORMATTER_H
 
 struct PacketStream {
   PacketStream();
-  
+
   uint8_t* next();
   bool hasNext();
-  
+
   uint8_t* packetStream;
   size_t numPackets;
   size_t packetLength;
@@ -23,69 +23,73 @@ struct PacketStream {
 class PacketFormatter {
 public:
   PacketFormatter(const size_t packetLength, const size_t maxPackets = 1);
-  
+
   typedef void (PacketFormatter::*StepFunction)();
-  
+
   void updateStatus(MiLightStatus status);
   virtual void updateStatus(MiLightStatus status, uint8_t groupId);
   virtual void command(uint8_t command, uint8_t arg);
-  
+
+  virtual void setHeld(bool held);
+
   // 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();
-  
+
   // Color
   virtual void updateHue(uint16_t value);
   virtual void updateColorRaw(uint8_t value);
   virtual void updateColorWhite();
-  
+
   // 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();
-  
+  virtual void enableNightMode();
+
   virtual void updateSaturation(uint8_t value);
-  
+
   virtual void reset();
-  
+
   virtual PacketStream& buildPackets();
   virtual void prepare(uint16_t deviceId, uint8_t groupId);
   virtual void format(uint8_t const* packet, char* buffer);
-  
+
   static void formatV1Packet(uint8_t const* packet, char* buffer);
-  
+
   template <typename T>
   static T rescale(T value, uint8_t newMax, float oldMax = 255.0) {
     return round(value * (newMax / oldMax));
   }
-  
+
   size_t getPacketLength() const;
-  
+
 protected:
   static uint8_t* PACKET_BUFFER;
-  
+
   uint8_t* currentPacket;
   size_t packetLength;
   uint16_t deviceId;
   uint8_t groupId;
   uint8_t sequenceNum;
   size_t numPackets;
+  bool held;
   PacketStream packetStream;
-  
+
   void pushPacket();
   void valueByStepFunction(StepFunction increase, StepFunction decrease, uint8_t numSteps, uint8_t value);
-  
+
   virtual void initializePacket(uint8_t* packetStart) = 0;
   virtual void finalizePacket(uint8_t* packet);
 };

+ 0 - 34
lib/MiLight/RadioStack.h

@@ -1,34 +0,0 @@
-#include <RF24.h>
-#include <PL1167_nRF24.h>
-#include <MiLightRadioConfig.h>
-#include <MiLightRadio.h>
-
-#ifndef _RADIO_STACK_H
-#define _RADIO_STACK_H 
-
-class RadioStack {
-public:
-  RadioStack(RF24& rf, const MiLightRadioConfig& config) 
-    : config(config)
-  {
-    nrf = new PL1167_nRF24(rf);
-    radio = new MiLightRadio(*nrf, config);
-  }
-  
-  ~RadioStack() {
-    delete radio;
-    delete nrf;
-  }
-  
-  inline MiLightRadio* getRadio() {
-    return this->radio;
-  }
-  
-  const MiLightRadioConfig& config;
-  
-private:
-  PL1167_nRF24 *nrf;
-  MiLightRadio *radio;
-};
-
-#endif

+ 28 - 18
lib/MiLight/RgbCctPacketFormatter.cpp

@@ -6,6 +6,8 @@
   ((jumpStart > 0 && key >= jumpStart && key <= jumpStart+0x80) ? 0x80 : 0) \
 )
 
+#define GROUP_COMMAND_ARG(status, groupId) ( groupId + (status == OFF ? 5 : 0) )
+
 uint8_t const RgbCctPacketFormatter::V2_OFFSETS[][4] = {
   { 0x45, 0x1F, 0x14, 0x5C }, // request type
   { 0x2B, 0xC9, 0xE3, 0x11 }, // id 1
@@ -19,10 +21,10 @@ uint8_t const RgbCctPacketFormatter::V2_OFFSETS[][4] = {
 
 void RgbCctPacketFormatter::initializePacket(uint8_t* packet) {
   size_t packetPtr = 0;
-  
+
   // Always encode with 0x00 key. No utility in varying it.
   packet[packetPtr++] = 0x00;
-  
+
   packet[packetPtr++] = 0x20;
   packet[packetPtr++] = deviceId >> 8;
   packet[packetPtr++] = deviceId & 0xFF;
@@ -33,22 +35,25 @@ void RgbCctPacketFormatter::initializePacket(uint8_t* packet) {
   packet[packetPtr++] = 0;
 }
 
-void RgbCctPacketFormatter::unpair() { 
+void RgbCctPacketFormatter::unpair() {
   for (size_t i = 0; i < 5; i++) {
     updateStatus(ON, 0);
   }
 }
-  
+
 void RgbCctPacketFormatter::command(uint8_t command, uint8_t arg) {
   pushPacket();
+  if (held) {
+    command |= 0x80;
+  }
   currentPacket[RGB_CCT_COMMAND_INDEX] = command;
   currentPacket[RGB_CCT_ARGUMENT_INDEX] = arg;
 }
 
 void RgbCctPacketFormatter::updateStatus(MiLightStatus status, uint8_t groupId) {
-  command(RGB_CCT_ON, groupId + (status == OFF ? 5 : 0));
+  command(RGB_CCT_ON, GROUP_COMMAND_ARG(status, groupId));
 }
-  
+
 void RgbCctPacketFormatter::modeSpeedDown() {
   command(RGB_CCT_ON, RGB_CCT_MODE_SPEED_DOWN);
 }
@@ -73,7 +78,7 @@ void RgbCctPacketFormatter::previousMode() {
 void RgbCctPacketFormatter::updateBrightness(uint8_t brightness) {
   command(RGB_CCT_BRIGHTNESS, 0x8F + brightness);
 }
-  
+
 void RgbCctPacketFormatter::updateHue(uint16_t value) {
   uint8_t remapped = rescale(value, 255, 360);
   updateColorRaw(remapped);
@@ -82,7 +87,7 @@ void RgbCctPacketFormatter::updateHue(uint16_t value) {
 void RgbCctPacketFormatter::updateColorRaw(uint8_t value) {
   command(RGB_CCT_COLOR, 0x5F + value);
 }
-  
+
 void RgbCctPacketFormatter::updateTemperature(uint8_t value) {
   command(RGB_CCT_KELVIN, 0x94 - (value*2));
 }
@@ -91,11 +96,16 @@ void RgbCctPacketFormatter::updateSaturation(uint8_t value) {
   uint8_t remapped = value + 0xD;
   command(RGB_CCT_SATURATION, remapped);
 }
-  
+
 void RgbCctPacketFormatter::updateColorWhite() {
   updateTemperature(0);
 }
-  
+
+void RgbCctPacketFormatter::enableNightMode() {
+  uint8_t arg = GROUP_COMMAND_ARG(OFF, groupId);
+  command(RGB_CCT_ON | 0x80, arg);
+}
+
 void RgbCctPacketFormatter::finalizePacket(uint8_t* packet) {
   encodeV2Packet(packet);
 }
@@ -116,7 +126,7 @@ uint8_t RgbCctPacketFormatter::decodeByte(uint8_t byte, uint8_t s1, uint8_t xorK
   uint8_t value = byte - s2;
   value = value ^ xorKey;
   value = value - s1;
-  
+
   return value;
 }
 
@@ -124,13 +134,13 @@ uint8_t RgbCctPacketFormatter::encodeByte(uint8_t byte, uint8_t s1, uint8_t xorK
   uint8_t value = byte + s1;
   value = value ^ xorKey;
   value = value + s2;
-  
+
   return value;
 }
 
 void RgbCctPacketFormatter::decodeV2Packet(uint8_t *packet) {
   uint8_t key = xorKey(packet[0]);
-  
+
   for (size_t i = 1; i <= 8; i++) {
     packet[i] = decodeByte(packet[i], 0, key, V2_OFFSET(i, packet[0], V2_OFFSET_JUMP_START));
   }
@@ -139,12 +149,12 @@ void RgbCctPacketFormatter::decodeV2Packet(uint8_t *packet) {
 void RgbCctPacketFormatter::encodeV2Packet(uint8_t *packet) {
   uint8_t key = xorKey(packet[0]);
   uint8_t sum = key;
-  
+
   for (size_t i = 1; i <= 7; i++) {
     sum += packet[i];
     packet[i] = encodeByte(packet[i], 0, key, V2_OFFSET(i, packet[0], V2_OFFSET_JUMP_START));
   }
-  
+
   packet[8] = encodeByte(sum, 2, key, V2_OFFSET(8, packet[0], 0));
 }
 
@@ -153,12 +163,12 @@ void RgbCctPacketFormatter::format(uint8_t const* packet, char* buffer) {
   for (int i = 0; i < packetLength; i++) {
     buffer += sprintf_P(buffer, PSTR("%02X "), packet[i]);
   }
-  
+
   uint8_t decodedPacket[packetLength];
   memcpy(decodedPacket, packet, packetLength);
-  
+
   decodeV2Packet(decodedPacket);
-  
+
   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]);

+ 10 - 9
lib/MiLight/RgbCctPacketFormatter.h

@@ -6,7 +6,7 @@
 #define V2_OFFSET_JUMP_START 0x54
 
 #ifndef _RGB_CCT_PACKET_FORMATTER_H
-#define _RGB_CCT_PACKET_FORMATTER_H 
+#define _RGB_CCT_PACKET_FORMATTER_H
 
 enum MiLightRgbCctCommand {
   RGB_CCT_ON = 0x01,
@@ -26,14 +26,14 @@ enum MiLightRgbCctArguments {
 class RgbCctPacketFormatter : public PacketFormatter {
 public:
   static uint8_t const V2_OFFSETS[][4] PROGMEM;
-    
+
   RgbCctPacketFormatter()
     : PacketFormatter(9),
       lastMode(0)
   { }
-  
+
   virtual void initializePacket(uint8_t* packet);
-  
+
   virtual void updateStatus(MiLightStatus status, uint8_t group);
   virtual void updateBrightness(uint8_t value);
   virtual void command(uint8_t command, uint8_t arg);
@@ -44,15 +44,16 @@ public:
   virtual void updateSaturation(uint8_t value);
   virtual void format(uint8_t const* packet, char* buffer);
   virtual void unpair();
-  
+  virtual void enableNightMode();
+
   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);
   static void decodeV2Packet(uint8_t* packet);
   static uint8_t xorKey(uint8_t key);
@@ -60,8 +61,8 @@ public:
   static uint8_t decodeByte(uint8_t byte, uint8_t s1, uint8_t xorKey, uint8_t s2);
 
 protected:
-  
+
   uint8_t lastMode;
 };
 
-#endif
+#endif

+ 10 - 7
lib/MiLight/RgbPacketFormatter.cpp

@@ -2,7 +2,7 @@
 
 void RgbPacketFormatter::initializePacket(uint8_t *packet) {
   size_t packetPtr = 0;
-  
+
   packet[packetPtr++] = RGB;
   packet[packetPtr++] = deviceId >> 8;
   packet[packetPtr++] = deviceId & 0xFF;
@@ -11,13 +11,13 @@ void RgbPacketFormatter::initializePacket(uint8_t *packet) {
   packet[packetPtr++] = sequenceNum++;
 }
 
-void RgbPacketFormatter::pair() { 
+void RgbPacketFormatter::pair() {
   for (size_t i = 0; i < 5; i++) {
     command(RGB_SPEED_UP, 0);
   }
 }
 
-void RgbPacketFormatter::unpair() { 
+void RgbPacketFormatter::unpair() {
   for (size_t i = 0; i < 5; i++) {
     command(RGB_SPEED_UP | 0x10, 0);
   }
@@ -29,9 +29,12 @@ void RgbPacketFormatter::updateStatus(MiLightStatus status, uint8_t groupId) {
 
 void RgbPacketFormatter::command(uint8_t command, uint8_t arg) {
   pushPacket();
+  if (held) {
+    command |= 0x80;
+  }
   currentPacket[RGB_COMMAND_INDEX] = command;
 }
-  
+
 void RgbPacketFormatter::updateHue(uint16_t value) {
   const int16_t remappedColor = (value + 40) % 360;
   updateColorRaw(rescale(remappedColor, 255, 360));
@@ -58,11 +61,11 @@ 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);
 }
@@ -81,4 +84,4 @@ void RgbPacketFormatter::format(uint8_t const* packet, char* buffer) {
   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 - 4
lib/MiLight/RgbPacketFormatter.h

@@ -1,7 +1,7 @@
 #include <PacketFormatter.h>
 
 #ifndef _RGB_PACKET_FORMATTER_H
-#define _RGB_PACKET_FORMATTER_H 
+#define _RGB_PACKET_FORMATTER_H
 
 #define RGB_COMMAND_INDEX 4
 #define RGB_COLOR_INDEX 3
@@ -24,7 +24,7 @@ public:
   RgbPacketFormatter()
     : PacketFormatter(6, 20)
   { }
-  
+
   virtual void updateStatus(MiLightStatus status, uint8_t groupId);
   virtual void updateBrightness(uint8_t value);
   virtual void increaseBrightness();
@@ -39,8 +39,8 @@ public:
   virtual void modeSpeedUp();
   virtual void nextMode();
   virtual void previousMode();
-  
+
   virtual void initializePacket(uint8_t* packet);
 };
 
-#endif
+#endif

+ 20 - 9
lib/MiLight/RgbwPacketFormatter.cpp

@@ -1,9 +1,11 @@
 #include <RgbwPacketFormatter.h>
 #include <MiLightButtons.h>
 
+#define STATUS_COMMAND(status, groupId) ( RGBW_GROUP_1_ON + ((groupId - 1)*2) + status )
+
 void RgbwPacketFormatter::initializePacket(uint8_t* packet) {
   size_t packetPtr = 0;
-  
+
   packet[packetPtr++] = RGBW;
   packet[packetPtr++] = deviceId >> 8;
   packet[packetPtr++] = deviceId & 0xFF;
@@ -13,11 +15,11 @@ void RgbwPacketFormatter::initializePacket(uint8_t* packet) {
   packet[packetPtr++] = sequenceNum++;
 }
 
-void RgbwPacketFormatter::unpair() { 
+void RgbwPacketFormatter::unpair() {
   PacketFormatter::updateStatus(ON);
   updateColorWhite();
 }
-  
+
 void RgbwPacketFormatter::modeSpeedDown() {
   command(RGBW_SPEED_DOWN, 0);
 }
@@ -36,10 +38,9 @@ void RgbwPacketFormatter::updateMode(uint8_t mode) {
 }
 
 void RgbwPacketFormatter::updateStatus(MiLightStatus status, uint8_t groupId) {
-  uint8_t button = RGBW_GROUP_1_ON + ((groupId - 1)*2) + status;
-  command(button, 0);
+  command(STATUS_COMMAND(status, groupId), 0);
 }
-  
+
 void RgbwPacketFormatter::updateBrightness(uint8_t value) {
   // Expect an input value in [0, 100]. Map it down to [0, 25].
   const uint8_t adjustedBrightness = rescale(value, 25, 100);
@@ -49,16 +50,19 @@ void RgbwPacketFormatter::updateBrightness(uint8_t value) {
   const uint8_t packetBrightnessValue = (
     ((31 - adjustedBrightness) + 17) % 32
   );
-  
+
   command(RGBW_BRIGHTNESS, 0);
   currentPacket[RGBW_BRIGHTNESS_GROUP_INDEX] |= (packetBrightnessValue << 3);
 }
 
 void RgbwPacketFormatter::command(uint8_t command, uint8_t arg) {
   pushPacket();
+  if (held) {
+    command |= 0x80;
+  }
   currentPacket[RGBW_COMMAND_INDEX] = command;
 }
-  
+
 void RgbwPacketFormatter::updateHue(uint16_t value) {
   const int16_t remappedColor = (value + 40) % 360;
   updateColorRaw(rescale(remappedColor, 255, 360));
@@ -74,6 +78,13 @@ void RgbwPacketFormatter::updateColorWhite() {
   command(button, 0);
 }
 
+void RgbwPacketFormatter::enableNightMode() {
+  uint8_t button = STATUS_COMMAND(ON, groupId);
+
+  command(button, 0);
+  command(button | 0x10, 0);
+}
+
 void RgbwPacketFormatter::format(uint8_t const* packet, char* buffer) {
   PacketFormatter::formatV1Packet(packet, buffer);
-}
+}

+ 8 - 7
lib/MiLight/RgbwPacketFormatter.h

@@ -1,7 +1,7 @@
 #include <PacketFormatter.h>
 
 #ifndef _RGBW_PACKET_FORMATTER_H
-#define _RGBW_PACKET_FORMATTER_H 
+#define _RGBW_PACKET_FORMATTER_H
 
 enum MiLightRgbwButton {
   RGBW_ALL_ON            = 0x01,
@@ -14,14 +14,14 @@ enum MiLightRgbwButton {
   RGBW_GROUP_3_OFF       = 0x08,
   RGBW_GROUP_4_ON        = 0x09,
   RGBW_GROUP_4_OFF       = 0x0A,
-  RGBW_SPEED_UP          = 0x0B, 
-  RGBW_SPEED_DOWN        = 0x0C, 
+  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,
@@ -43,7 +43,7 @@ public:
   RgbwPacketFormatter()
     : PacketFormatter(7)
   { }
-  
+
   virtual void updateStatus(MiLightStatus status, uint8_t groupId);
   virtual void updateBrightness(uint8_t value);
   virtual void command(uint8_t command, uint8_t arg);
@@ -56,8 +56,9 @@ public:
   virtual void modeSpeedUp();
   virtual void nextMode();
   virtual void updateMode(uint8_t mode);
-  
+  virtual void enableNightMode();
+
   virtual void initializePacket(uint8_t* packet);
 };
 
-#endif
+#endif

+ 441 - 0
lib/SSDP/New_ESP8266SSDP.cpp

@@ -0,0 +1,441 @@
+/*
+ESP8266 Simple Service Discovery
+Copyright (c) 2015 Hristo Gochkov
+
+Original (Arduino) version by Filippo Sallemi, July 23, 2014.
+Can be found at: https://github.com/nomadnt/uSSDP
+
+License (MIT license):
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+
+*/
+#define LWIP_OPEN_SRC
+#include <functional>
+#include "New_ESP8266SSDP.h"
+#include "WiFiUdp.h"
+#include "debug.h"
+
+extern "C" {
+  #include "osapi.h"
+  #include "ets_sys.h"
+  #include "user_interface.h"
+}
+
+#include "lwip/opt.h"
+#include "lwip/udp.h"
+#include "lwip/inet.h"
+#include "lwip/igmp.h"
+#include "lwip/mem.h"
+#include "include/UdpContext.h"
+
+// #define DEBUG_SSDP  Serial
+
+#define SSDP_INTERVAL     1200
+#define SSDP_PORT         1900
+#define SSDP_METHOD_SIZE  10
+#define SSDP_URI_SIZE     2
+#define SSDP_BUFFER_SIZE  64
+#define SSDP_MULTICAST_TTL 2
+static const IPAddress SSDP_MULTICAST_ADDR(239, 255, 255, 250);
+
+
+
+static const char _ssdp_response_template[] PROGMEM =
+  "HTTP/1.1 200 OK\r\n"
+  "EXT:\r\n";
+
+static const char _ssdp_notify_template[] PROGMEM =
+  "NOTIFY * HTTP/1.1\r\n"
+  "HOST: 239.255.255.250:1900\r\n"
+  "NTS: ssdp:alive\r\n";
+
+static const char _ssdp_packet_template[] PROGMEM =
+  "%s" // _ssdp_response_template / _ssdp_notify_template
+  "CACHE-CONTROL: max-age=%u\r\n" // SSDP_INTERVAL
+  "SERVER: Arduino/1.0 UPNP/1.1 %s/%s\r\n" // _modelName, _modelNumber
+  "USN: uuid:%s\r\n" // _uuid
+  "%s: %s\r\n"  // "NT" or "ST", _deviceType
+  "LOCATION: http://%u.%u.%u.%u:%u/%s\r\n" // WiFi.localIP(), _port, _schemaURL
+  "\r\n";
+
+static const char _ssdp_schema_template[] PROGMEM =
+  "HTTP/1.1 200 OK\r\n"
+  "Content-Type: text/xml\r\n"
+  "Connection: close\r\n"
+  "Access-Control-Allow-Origin: *\r\n"
+  "\r\n"
+  "<?xml version=\"1.0\"?>"
+  "<root xmlns=\"urn:schemas-upnp-org:device-1-0\">"
+    "<specVersion>"
+      "<major>1</major>"
+      "<minor>0</minor>"
+    "</specVersion>"
+    "<URLBase>http://%u.%u.%u.%u:%u/</URLBase>" // WiFi.localIP(), _port
+    "<device>"
+      "<deviceType>%s</deviceType>"
+      "<friendlyName>%s</friendlyName>"
+      "<presentationURL>%s</presentationURL>"
+      "<serialNumber>%s</serialNumber>"
+      "<modelName>%s</modelName>"
+      "<modelNumber>%s</modelNumber>"
+      "<modelURL>%s</modelURL>"
+      "<manufacturer>%s</manufacturer>"
+      "<manufacturerURL>%s</manufacturerURL>"
+      "<UDN>uuid:%s</UDN>"
+    "</device>"
+//    "<iconList>"
+//      "<icon>"
+//        "<mimetype>image/png</mimetype>"
+//        "<height>48</height>"
+//        "<width>48</width>"
+//        "<depth>24</depth>"
+//        "<url>icon48.png</url>"
+//      "</icon>"
+//      "<icon>"
+//       "<mimetype>image/png</mimetype>"
+//       "<height>120</height>"
+//       "<width>120</width>"
+//       "<depth>24</depth>"
+//       "<url>icon120.png</url>"
+//      "</icon>"
+//    "</iconList>"
+  "</root>\r\n"
+  "\r\n";
+
+
+struct SSDPTimer {
+  ETSTimer timer;
+};
+
+SSDPClass::SSDPClass() :
+_server(0),
+_timer(new SSDPTimer),
+_port(80),
+_ttl(SSDP_MULTICAST_TTL),
+_respondToPort(0),
+_pending(false),
+_delay(0),
+_process_time(0),
+_notify_time(0)
+{
+  _uuid[0] = '\0';
+  _modelNumber[0] = '\0';
+  sprintf(_deviceType, "urn:schemas-upnp-org:device:Basic:1");
+  _friendlyName[0] = '\0';
+  _presentationURL[0] = '\0';
+  _serialNumber[0] = '\0';
+  _modelName[0] = '\0';
+  _modelURL[0] = '\0';
+  _manufacturer[0] = '\0';
+  _manufacturerURL[0] = '\0';
+  sprintf(_schemaURL, "ssdp/schema.xml");
+}
+
+SSDPClass::~SSDPClass(){
+  delete _timer;
+}
+
+bool SSDPClass::begin(){
+  _pending = false;
+
+  uint32_t chipId = ESP.getChipId();
+  sprintf(_uuid, "38323636-4558-4dda-9188-cda0e6%02x%02x%02x",
+    (uint16_t) ((chipId >> 16) & 0xff),
+    (uint16_t) ((chipId >>  8) & 0xff),
+    (uint16_t)   chipId        & 0xff  );
+
+#ifdef DEBUG_SSDP
+  DEBUG_SSDP.printf("SSDP UUID: %s\n", (char *)_uuid);
+#endif
+
+  if (_server) {
+    _server->unref();
+    _server = 0;
+  }
+
+  _server = new UdpContext;
+  _server->ref();
+
+  ip_addr_t ifaddr;
+  ifaddr.addr = WiFi.localIP();
+  ip_addr_t multicast_addr;
+  multicast_addr.addr = (uint32_t) SSDP_MULTICAST_ADDR;
+  if (igmp_joingroup(&ifaddr, &multicast_addr) != ERR_OK ) {
+    DEBUGV("SSDP failed to join igmp group");
+    return false;
+  }
+
+  if (!_server->listen(*IP_ADDR_ANY, SSDP_PORT)) {
+    return false;
+  }
+
+  _server->setMulticastInterface(ifaddr);
+  _server->setMulticastTTL(_ttl);
+  _server->onRx(std::bind(&SSDPClass::_update, this));
+  if (!_server->connect(multicast_addr, SSDP_PORT)) {
+    return false;
+  }
+
+  _startTimer();
+
+  return true;
+}
+
+void SSDPClass::_send(ssdp_method_t method){
+  char buffer[1460];
+  uint32_t ip = WiFi.localIP();
+
+  char valueBuffer[strlen(_ssdp_notify_template)+1];
+  strcpy_P(valueBuffer, (method == NONE)?_ssdp_response_template:_ssdp_notify_template);
+
+  int len = snprintf_P(buffer, sizeof(buffer),
+    _ssdp_packet_template,
+    valueBuffer,
+    SSDP_INTERVAL,
+    _modelName, _modelNumber,
+    _uuid,
+    (method == NONE)?"ST":"NT",
+    _deviceType,
+    IP2STR(&ip), _port, _schemaURL
+  );
+
+  _server->append(buffer, len);
+
+  ip_addr_t remoteAddr;
+  uint16_t remotePort;
+  if(method == NONE) {
+    remoteAddr.addr = _respondToAddr;
+    remotePort = _respondToPort;
+#ifdef DEBUG_SSDP
+    DEBUG_SSDP.print("Sending Response to ");
+#endif
+  } else {
+    remoteAddr.addr = SSDP_MULTICAST_ADDR;
+    remotePort = SSDP_PORT;
+#ifdef DEBUG_SSDP
+    DEBUG_SSDP.println("Sending Notify to ");
+#endif
+  }
+#ifdef DEBUG_SSDP
+  DEBUG_SSDP.print(IPAddress(remoteAddr.addr));
+  DEBUG_SSDP.print(":");
+  DEBUG_SSDP.println(remotePort);
+#endif
+
+  _server->send(&remoteAddr, remotePort);
+}
+
+void SSDPClass::schema(WiFiClient client){
+  uint32_t ip = WiFi.localIP();
+  char buffer[strlen(_ssdp_schema_template)+1];
+  strcpy_P(buffer, _ssdp_schema_template);
+  client.printf(buffer,
+    IP2STR(&ip), _port,
+    _deviceType,
+    _friendlyName,
+    _presentationURL,
+    _serialNumber,
+    _modelName,
+    _modelNumber,
+    _modelURL,
+    _manufacturer,
+    _manufacturerURL,
+    _uuid
+  );
+}
+
+void SSDPClass::_update(){
+  if(!_pending && _server->next()) {
+    ssdp_method_t method = NONE;
+
+    _respondToAddr = _server->getRemoteAddress();
+    _respondToPort = _server->getRemotePort();
+
+    typedef enum {METHOD, URI, PROTO, KEY, VALUE, ABORT} states;
+    states state = METHOD;
+
+    typedef enum {START, MAN, ST, MX} headers;
+    headers header = START;
+
+    uint8_t cursor = 0;
+    uint8_t cr = 0;
+
+    char buffer[SSDP_BUFFER_SIZE] = {0};
+
+    while(_server->getSize() > 0){
+      char c = _server->read();
+
+      (c == '\r' || c == '\n') ? cr++ : cr = 0;
+
+      switch(state){
+        case METHOD:
+          if(c == ' '){
+            if(strcmp(buffer, "M-SEARCH") == 0) method = SEARCH;
+            else if(strcmp(buffer, "NOTIFY") == 0) method = NOTIFY;
+
+            if(method == NONE) state = ABORT;
+            else state = URI;
+            cursor = 0;
+
+          } else if(cursor < SSDP_METHOD_SIZE - 1){ buffer[cursor++] = c; buffer[cursor] = '\0'; }
+          break;
+        case URI:
+          if(c == ' '){
+            if(strcmp(buffer, "*")) state = ABORT;
+            else state = PROTO;
+            cursor = 0;
+          } else if(cursor < SSDP_URI_SIZE - 1){ buffer[cursor++] = c; buffer[cursor] = '\0'; }
+          break;
+        case PROTO:
+          if(cr == 2){ state = KEY; cursor = 0; }
+          break;
+        case KEY:
+          if(cr == 4){ _pending = true; _process_time = millis(); }
+          else if(c == ' '){ cursor = 0; state = VALUE; }
+          else if(c != '\r' && c != '\n' && c != ':' && cursor < SSDP_BUFFER_SIZE - 1){ buffer[cursor++] = c; buffer[cursor] = '\0'; }
+          break;
+        case VALUE:
+          if(cr == 2){
+            switch(header){
+              case START:
+                break;
+              case MAN:
+#ifdef DEBUG_SSDP
+                DEBUG_SSDP.printf("MAN: %s\n", (char *)buffer);
+#endif
+                break;
+              case ST:
+                if(strcmp(buffer, "ssdp:all")){
+                  state = ABORT;
+#ifdef DEBUG_SSDP
+                  DEBUG_SSDP.printf("REJECT: %s\n", (char *)buffer);
+#endif
+                }
+                // if the search type matches our type, we should respond instead of ABORT
+                if(strcmp(buffer, _deviceType) == 0){
+                  _pending = true;
+                  _process_time = millis();
+                  state = KEY;
+                }
+                break;
+              case MX:
+                _delay = random(0, atoi(buffer)) * 1000L;
+                break;
+            }
+
+            if(state != ABORT){ state = KEY; header = START; cursor = 0; }
+          } else if(c != '\r' && c != '\n'){
+            if(header == START){
+              if(strncmp(buffer, "MA", 2) == 0) header = MAN;
+              else if(strcmp(buffer, "ST") == 0) header = ST;
+              else if(strcmp(buffer, "MX") == 0) header = MX;
+            }
+
+            if(cursor < SSDP_BUFFER_SIZE - 1){ buffer[cursor++] = c; buffer[cursor] = '\0'; }
+          }
+          break;
+        case ABORT:
+          _pending = false; _delay = 0;
+          break;
+      }
+    }
+  }
+
+  if(_pending && (millis() - _process_time) > _delay){
+    _pending = false; _delay = 0;
+    _send(NONE);
+  } else if(_notify_time == 0 || (millis() - _notify_time) > (SSDP_INTERVAL * 1000L)){
+    _notify_time = millis();
+    _send(NOTIFY);
+  }
+
+  if (_pending) {
+    while (_server->next())
+      _server->flush();
+  }
+
+}
+
+void SSDPClass::setSchemaURL(const char *url){
+  strlcpy(_schemaURL, url, sizeof(_schemaURL));
+}
+
+void SSDPClass::setHTTPPort(uint16_t port){
+  _port = port;
+}
+
+void SSDPClass::setDeviceType(const char *deviceType){
+  strlcpy(_deviceType, deviceType, sizeof(_deviceType));
+}
+
+void SSDPClass::setName(const char *name){
+  strlcpy(_friendlyName, name, sizeof(_friendlyName));
+}
+
+void SSDPClass::setURL(const char *url){
+  strlcpy(_presentationURL, url, sizeof(_presentationURL));
+}
+
+void SSDPClass::setSerialNumber(const char *serialNumber){
+  strlcpy(_serialNumber, serialNumber, sizeof(_serialNumber));
+}
+
+void SSDPClass::setSerialNumber(const uint32_t serialNumber){
+  snprintf(_serialNumber, sizeof(uint32_t)*2+1, "%08X", serialNumber);
+}
+
+void SSDPClass::setModelName(const char *name){
+  strlcpy(_modelName, name, sizeof(_modelName));
+}
+
+void SSDPClass::setModelNumber(const char *num){
+  strlcpy(_modelNumber, num, sizeof(_modelNumber));
+}
+
+void SSDPClass::setModelURL(const char *url){
+  strlcpy(_modelURL, url, sizeof(_modelURL));
+}
+
+void SSDPClass::setManufacturer(const char *name){
+  strlcpy(_manufacturer, name, sizeof(_manufacturer));
+}
+
+void SSDPClass::setManufacturerURL(const char *url){
+  strlcpy(_manufacturerURL, url, sizeof(_manufacturerURL));
+}
+
+void SSDPClass::setTTL(const uint8_t ttl){
+  _ttl = ttl;
+}
+
+void SSDPClass::_onTimerStatic(SSDPClass* self) {
+  self->_update();
+}
+
+void SSDPClass::_startTimer() {
+  ETSTimer* tm = &(_timer->timer);
+  const int interval = 1000;
+  os_timer_disarm(tm);
+  os_timer_setfn(tm, reinterpret_cast<ETSTimerFunc*>(&SSDPClass::_onTimerStatic), reinterpret_cast<void*>(this));
+  os_timer_arm(tm, interval, 1 /* repeat */);
+}
+
+#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_SSDP)
+SSDPClass SSDP;
+#endif

+ 128 - 0
lib/SSDP/New_ESP8266SSDP.h

@@ -0,0 +1,128 @@
+/*
+ESP8266 Simple Service Discovery
+Copyright (c) 2015 Hristo Gochkov
+
+Original (Arduino) version by Filippo Sallemi, July 23, 2014.
+Can be found at: https://github.com/nomadnt/uSSDP
+
+License (MIT license):
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+
+*/
+
+#ifndef ESP8266SSDP_H
+#define ESP8266SSDP_H
+
+#include <Arduino.h>
+#include <ESP8266WiFi.h>
+#include <WiFiUdp.h>
+
+class UdpContext;
+
+#define SSDP_UUID_SIZE              37
+#define SSDP_SCHEMA_URL_SIZE        64
+#define SSDP_DEVICE_TYPE_SIZE       64
+#define SSDP_FRIENDLY_NAME_SIZE     64
+#define SSDP_SERIAL_NUMBER_SIZE     32
+#define SSDP_PRESENTATION_URL_SIZE  128
+#define SSDP_MODEL_NAME_SIZE        64
+#define SSDP_MODEL_URL_SIZE         128
+#define SSDP_MODEL_VERSION_SIZE     32
+#define SSDP_MANUFACTURER_SIZE      64
+#define SSDP_MANUFACTURER_URL_SIZE  128
+
+typedef enum {
+  NONE,
+  SEARCH,
+  NOTIFY
+} ssdp_method_t;
+
+
+struct SSDPTimer;
+
+class SSDPClass{
+  public:
+    SSDPClass();
+    ~SSDPClass();
+
+    bool begin();
+
+    void schema(WiFiClient client);
+
+    void setDeviceType(const String& deviceType) { setDeviceType(deviceType.c_str()); }
+    void setDeviceType(const char *deviceType);
+    void setName(const String& name) { setName(name.c_str()); }
+    void setName(const char *name);
+    void setURL(const String& url) { setURL(url.c_str()); }
+    void setURL(const char *url);
+    void setSchemaURL(const String& url) { setSchemaURL(url.c_str()); }
+    void setSchemaURL(const char *url);
+    void setSerialNumber(const String& serialNumber) { setSerialNumber(serialNumber.c_str()); }
+    void setSerialNumber(const char *serialNumber);
+    void setSerialNumber(const uint32_t serialNumber);
+    void setModelName(const String& name) { setModelName(name.c_str()); }
+    void setModelName(const char *name);
+    void setModelNumber(const String& num) { setModelNumber(num.c_str()); }
+    void setModelNumber(const char *num);
+    void setModelURL(const String& url) { setModelURL(url.c_str()); }
+    void setModelURL(const char *url);
+    void setManufacturer(const String& name) { setManufacturer(name.c_str()); }
+    void setManufacturer(const char *name);
+    void setManufacturerURL(const String& url) { setManufacturerURL(url.c_str()); }
+    void setManufacturerURL(const char *url);
+    void setHTTPPort(uint16_t port);
+    void setTTL(uint8_t ttl);
+
+  protected:
+    void _send(ssdp_method_t method);
+    void _update();
+    void _startTimer();
+    static void _onTimerStatic(SSDPClass* self);
+
+    UdpContext* _server;
+    SSDPTimer* _timer;
+    uint16_t _port;
+    uint8_t _ttl;
+
+    IPAddress _respondToAddr;
+    uint16_t  _respondToPort;
+
+    bool _pending;
+    unsigned short _delay;
+    unsigned long _process_time;
+    unsigned long _notify_time;
+
+    char _schemaURL[SSDP_SCHEMA_URL_SIZE];
+    char _uuid[SSDP_UUID_SIZE];
+    char _deviceType[SSDP_DEVICE_TYPE_SIZE];
+    char _friendlyName[SSDP_FRIENDLY_NAME_SIZE];
+    char _serialNumber[SSDP_SERIAL_NUMBER_SIZE];
+    char _presentationURL[SSDP_PRESENTATION_URL_SIZE];
+    char _manufacturer[SSDP_MANUFACTURER_SIZE];
+    char _manufacturerURL[SSDP_MANUFACTURER_URL_SIZE];
+    char _modelName[SSDP_MODEL_NAME_SIZE];
+    char _modelURL[SSDP_MODEL_URL_SIZE];
+    char _modelNumber[SSDP_MODEL_VERSION_SIZE];
+};
+
+#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_SSDP)
+extern SSDPClass SSDP;
+#endif
+
+#endif

+ 78 - 73
lib/Settings/Settings.cpp

@@ -3,7 +3,9 @@
 #include <FS.h>
 #include <IntParsing.h>
 #include <algorithm>
-  
+
+#define PORT_POSITION(s) ( s.indexOf(':') )
+
 bool Settings::hasAuthSettings() {
   return adminUsername.length() > 0 && adminPassword.length() > 0;
 }
@@ -16,52 +18,14 @@ 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;
   JsonObject& parsedSettings = jsonBuffer.parseObject(json);
-  deserialize(settings, parsedSettings);
-}
-
-void Settings::deserialize(Settings& settings, JsonObject& parsedSettings) {
-  if (parsedSettings.success()) {
-    if (parsedSettings.containsKey("admin_username")) {
-      settings.adminUsername = parsedSettings.get<String>("admin_username");
-    }
-    
-    if (parsedSettings.containsKey("admin_password")) {
-      settings.adminPassword = parsedSettings.get<String>("admin_password");
-    }
-    
-    if (parsedSettings.containsKey("ce_pin")) {
-      settings.cePin = parsedSettings["ce_pin"];
-    }
-    
-    if (parsedSettings.containsKey("csn_pin")) {
-      settings.csnPin = parsedSettings["csn_pin"];
-    }
-    
-    if (parsedSettings.containsKey("packet_repeats")) {
-      settings.packetRepeats = parsedSettings["packet_repeats"];
-    }
-    
-    if (parsedSettings.containsKey("http_repeat_factor")) {
-      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);
-    
-    JsonArray& gatewayArr = parsedSettings["gateway_configs"];
-    settings.updateGatewayConfigs(gatewayArr);
-  }
+  settings.patch(parsedSettings);
 }
 
 void Settings::updateDeviceIds(JsonArray& arr) {
@@ -69,7 +33,7 @@ void Settings::updateDeviceIds(JsonArray& arr) {
     if (this->deviceIds) {
       delete this->deviceIds;
     }
-    
+
     this->deviceIds = new uint16_t[arr.size()];
     this->numDeviceIds = arr.size();
     arr.copyTo(this->deviceIds, arr.size());
@@ -81,13 +45,13 @@ void Settings::updateGatewayConfigs(JsonArray& arr) {
     if (this->gatewayConfigs) {
       delete[] this->gatewayConfigs;
     }
-    
+
     this->gatewayConfigs = new GatewayConfig*[arr.size()];
     this->numGatewayConfigs = arr.size();
-    
+
     for (size_t i = 0; i < arr.size(); i++) {
       JsonArray& params = arr[i];
-      
+
       if (params.success() && params.size() == 3) {
         this->gatewayConfigs[i] = new GatewayConfig(parseInt<uint16_t>(params[0]), params[1], params[2]);
       } else {
@@ -100,27 +64,23 @@ void Settings::updateGatewayConfigs(JsonArray& arr) {
 
 void Settings::patch(JsonObject& parsedSettings) {
   if (parsedSettings.success()) {
-    if (parsedSettings.containsKey("admin_username")) {
-      this->adminUsername = parsedSettings.get<String>("admin_username");
-    }
-    if (parsedSettings.containsKey("admin_password")) {
-      this->adminPassword = parsedSettings.get<String>("admin_password");
-    }
-    if (parsedSettings.containsKey("ce_pin")) {
-      this->cePin = parsedSettings["ce_pin"];
-    }
-    if (parsedSettings.containsKey("csn_pin")) {
-      this->csnPin = parsedSettings["csn_pin"];
-    }
-    if (parsedSettings.containsKey("packet_repeats")) {
-      this->packetRepeats = parsedSettings["packet_repeats"];
-    }
-    if (parsedSettings.containsKey("http_repeat_factor")) {
-      this->httpRepeatFactor = parsedSettings["http_repeat_factor"];
-    }
-    if (parsedSettings.containsKey("auto_restart_period")) {
-      this->_autoRestartPeriod = parsedSettings["auto_restart_period"];
+    this->setIfPresent<String>(parsedSettings, "admin_username", adminUsername);
+    this->setIfPresent(parsedSettings, "admin_password", adminPassword);
+    this->setIfPresent(parsedSettings, "ce_pin", cePin);
+    this->setIfPresent(parsedSettings, "csn_pin", csnPin);
+    this->setIfPresent(parsedSettings, "reset_pin", resetPin);
+    this->setIfPresent(parsedSettings, "packet_repeats", packetRepeats);
+    this->setIfPresent(parsedSettings, "http_repeat_factor", httpRepeatFactor);
+    this->setIfPresent(parsedSettings, "auto_restart_period", _autoRestartPeriod);
+    this->setIfPresent(parsedSettings, "mqtt_server", _mqttServer);
+    this->setIfPresent(parsedSettings, "mqtt_username", mqttUsername);
+    this->setIfPresent(parsedSettings, "mqtt_password", mqttPassword);
+    this->setIfPresent(parsedSettings, "mqtt_topic_pattern", mqttTopicPattern);
+
+    if (parsedSettings.containsKey("radio_interface_type")) {
+      this->radioInterfaceType = Settings::typeFromString(parsedSettings["radio_interface_type"]);
     }
+
     if (parsedSettings.containsKey("device_ids")) {
       JsonArray& arr = parsedSettings["device_ids"];
       updateDeviceIds(arr);
@@ -137,7 +97,7 @@ void Settings::load(Settings& settings) {
     File f = SPIFFS.open(SETTINGS_FILE, "r");
     String settingsContents = f.readStringUntil(SETTINGS_TERMINATOR);
     f.close();
-    
+
     deserialize(settings, settingsContents);
   } else {
     settings.save();
@@ -153,7 +113,7 @@ String Settings::toJson(const bool prettyPrint) {
 
 void Settings::save() {
   File f = SPIFFS.open(SETTINGS_FILE, "w");
-  
+
   if (!f) {
     Serial.println(F("Opening settings file failed"));
   } else {
@@ -165,21 +125,27 @@ void Settings::save() {
 void Settings::serialize(Stream& stream, const bool prettyPrint) {
   DynamicJsonBuffer jsonBuffer;
   JsonObject& root = jsonBuffer.createObject();
-  
+
   root["admin_username"] = this->adminUsername;
   root["admin_password"] = this->adminPassword;
   root["ce_pin"] = this->cePin;
   root["csn_pin"] = this->csnPin;
+  root["reset_pin"] = this->resetPin;
+  root["radio_interface_type"] = typeToString(this->radioInterfaceType);
   root["packet_repeats"] = this->packetRepeats;
   root["http_repeat_factor"] = this->httpRepeatFactor;
   root["auto_restart_period"] = this->_autoRestartPeriod;
-  
+  root["mqtt_server"] = this->_mqttServer;
+  root["mqtt_username"] = this->mqttUsername;
+  root["mqtt_password"] = this->mqttPassword;
+  root["mqtt_topic_pattern"] = this->mqttTopicPattern;
+
   if (this->deviceIds) {
     JsonArray& arr = jsonBuffer.createArray();
     arr.copyFrom(this->deviceIds, this->numDeviceIds);
     root["device_ids"] = arr;
   }
-  
+
   if (this->gatewayConfigs) {
     JsonArray& arr = jsonBuffer.createArray();
     for (size_t i = 0; i < this->numGatewayConfigs; i++) {
@@ -189,13 +155,52 @@ void Settings::serialize(Stream& stream, const bool prettyPrint) {
       elmt.add(this->gatewayConfigs[i]->protocolVersion);
       arr.add(elmt);
     }
-    
+
     root["gateway_configs"] = arr;
   }
-  
+
   if (prettyPrint) {
     root.prettyPrintTo(stream);
   } else {
     root.printTo(stream);
   }
-}
+}
+
+String Settings::mqttServer() {
+  int pos = PORT_POSITION(_mqttServer);
+
+  if (pos == -1) {
+    return _mqttServer;
+  } else {
+    return _mqttServer.substring(0, pos);
+  }
+}
+
+uint16_t Settings::mqttPort() {
+  int pos = PORT_POSITION(_mqttServer);
+
+  if (pos == -1) {
+    return DEFAULT_MQTT_PORT;
+  } else {
+    return atoi(_mqttServer.c_str() + pos + 1);
+  }
+}
+
+RadioInterfaceType Settings::typeFromString(const String& s) {
+  if (s.equalsIgnoreCase("lt8900")) {
+    return LT8900;
+  } else {
+    return nRF24;
+  }
+}
+
+String Settings::typeToString(RadioInterfaceType type) {
+  switch (type) {
+    case LT8900:
+      return "LT8900";
+
+    case nRF24:
+    default:
+      return "nRF24";
+  }
+}

+ 26 - 1
lib/Settings/Settings.h

@@ -26,6 +26,12 @@
 #define MILIGHT_REPO_WEB_PATH "/data/web/index.html"
 
 #define MINIMUM_RESTART_PERIOD 1
+#define DEFAULT_MQTT_PORT 1883
+
+enum RadioInterfaceType {
+  nRF24 = 0,
+  LT8900 = 1,
+};
 
 class GatewayConfig {
 public:
@@ -48,6 +54,8 @@ public:
     // CE and CSN pins from nrf24l01
     cePin(D0),
     csnPin(D8),
+    resetPin(0),
+    radioInterfaceType(nRF24),
     deviceIds(NULL),
     gatewayConfigs(NULL),
     numDeviceIds(0),
@@ -68,29 +76,46 @@ public:
   size_t getAutoRestartPeriod();
 
   static void deserialize(Settings& settings, String json);
-  static void deserialize(Settings& settings, JsonObject& json);
   static void load(Settings& settings);
 
+  static RadioInterfaceType typeFromString(const String& s);
+  static String typeToString(RadioInterfaceType type);
+
   void save();
   String toJson(const bool prettyPrint = true);
   void serialize(Stream& stream, const bool prettyPrint = false);
   void updateDeviceIds(JsonArray& arr);
   void updateGatewayConfigs(JsonArray& arr);
   void patch(JsonObject& obj);
+  String mqttServer();
+  uint16_t mqttPort();
 
   String adminUsername;
   String adminPassword;
   uint8_t cePin;
   uint8_t csnPin;
+  uint8_t resetPin;
+  RadioInterfaceType radioInterfaceType;
   uint16_t *deviceIds;
   GatewayConfig **gatewayConfigs;
   size_t numGatewayConfigs;
   size_t numDeviceIds;
   size_t packetRepeats;
   size_t httpRepeatFactor;
+  String _mqttServer;
+  String mqttUsername;
+  String mqttPassword;
+  String mqttTopicPattern;
 
 protected:
   size_t _autoRestartPeriod;
+
+  template <typename T>
+  void setIfPresent(JsonObject& obj, const char* key, T& var) {
+    if (obj.containsKey(key)) {
+      var = obj.get<T>(key);
+    }
+  }
 };
 
 #endif

+ 26 - 16
lib/Udp/V6CctCommandHandler.cpp

@@ -1,49 +1,59 @@
 #include <V6CctCommandHandler.h>
 
+bool V6CctCommandHandler::handlePreset(
+    MiLightClient* client,
+    uint8_t commandLsb,
+    uint32_t commandArg)
+{
+  return false;
+}
+
 bool V6CctCommandHandler::handleCommand(
-    MiLightClient* client, 
-    uint16_t deviceId,
-    uint8_t group,
+    MiLightClient* client,
     uint32_t command,
     uint32_t commandArg)
 {
-  const uint8_t cmd = command & 0xFF;
+  const uint8_t cmd = command & 0x7F;
   const uint8_t arg = commandArg >> 24;
-  
-  client->prepare(MilightCctConfig, deviceId, group);
-  
+
+  client->setHeld((command & 0x80) == 0x80);
+
   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;
-        
+
+      case V2_CCT_NIGHT_LIGHT:
+        client->enableNightMode();
+        break;
+
       default:
         return false;
     }
-    
+
     return true;
   }
-  
+
   return false;
-}
+}

+ 12 - 8
lib/Udp/V6CctCommandHandler.h

@@ -1,11 +1,11 @@
 #include <V6CommandHandler.h>
 
 #ifndef _V6_CCT_COMMAND_HANDLER_H
-#define _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,
@@ -20,15 +20,19 @@ public:
   V6CctCommandHandler()
     : V6CommandHandler(0x0100, MilightCctConfig)
   { }
-  
+
   virtual bool handleCommand(
-    MiLightClient* client, 
-    uint16_t deviceId,
-    uint8_t group,
+    MiLightClient* client,
     uint32_t command,
     uint32_t commandArg
   );
-  
+
+  virtual bool handlePreset(
+    MiLightClient* client,
+    uint8_t commandLsb,
+    uint32_t commandArg
+  );
+
 };
 
-#endif
+#endif

+ 20 - 13
lib/Udp/V6ComamndHandler.cpp

@@ -13,30 +13,32 @@ V6CommandHandler* V6CommandHandler::ALL_HANDLERS[] = {
 };
 
 const size_t V6CommandHandler::NUM_HANDLERS = size(ALL_HANDLERS);
-  
-bool V6CommandHandler::handleCommand(MiLightClient* client, 
+
+bool V6CommandHandler::handleCommand(MiLightClient* client,
   uint16_t deviceId,
   uint8_t group,
   uint8_t commandType,
   uint32_t command,
-  uint32_t commandArg) 
+  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_PRESET) {
+    return this->handlePreset(client, command, commandArg);
   } else if (commandType == V6_COMMAND) {
-    return this->handleCommand(client, deviceId, group, command, commandArg);
+    return this->handleCommand(client, command, commandArg);
   } else {
     return false;
   }
-  
+
   return true;
 }
 
-bool V6CommandDemuxer::handleCommand(MiLightClient* client, 
+bool V6CommandDemuxer::handleCommand(MiLightClient* client,
   uint16_t deviceId,
   uint8_t group,
   uint8_t commandType,
@@ -49,15 +51,20 @@ bool V6CommandDemuxer::handleCommand(MiLightClient* client,
       return true;
     }
   }
-  
+
   return false;
 }
 
-bool V6CommandDemuxer::handleCommand(MiLightClient* client, 
-  uint16_t deviceId,
-  uint8_t group,
-  uint32_t command,
+bool V6CommandDemuxer::handleCommand(MiLightClient* client,
+  uint32_t commandLsb,
   uint32_t commandArg)
 {
   return false;
-}
+}
+
+bool V6CommandDemuxer::handlePreset(MiLightClient* client,
+  uint8_t commandLsb,
+  uint32_t commandArg)
+{
+  return false;
+}

+ 28 - 19
lib/Udp/V6CommandHandler.h

@@ -2,11 +2,12 @@
 #include <MiLightRadioConfig.h>
 
 #ifndef _V6_COMMAND_HANDLER_H
-#define _V6_COMMAND_HANDLER_H 
+#define _V6_COMMAND_HANDLER_H
 
 enum V6CommandTypes {
   V6_PAIR = 0x3D,
   V6_UNPAIR = 0x3E,
+  V6_PRESET = 0x3F,
   V6_COMMAND = 0x31
 };
 
@@ -14,63 +15,71 @@ 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, 
+    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,
+    MiLightClient* client,
     uint32_t command,
     uint32_t commandArg
   ) = 0;
+
+  virtual bool handlePreset(
+    MiLightClient* client,
+    uint8_t commandLsb,
+    uint32_t commandArg
+  ) = 0;
 };
 
 class V6CommandDemuxer : public V6CommandHandler {
 public:
   V6CommandDemuxer(V6CommandHandler* handlers[], size_t numHandlers)
-    : V6CommandHandler(0, MilightRgbwConfig),  
+    : V6CommandHandler(0, MilightRgbwConfig),
       handlers(handlers),
       numHandlers(numHandlers)
   { }
-  
+
   virtual bool handleCommand(
-    MiLightClient* client, 
+    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,
+    MiLightClient* client,
     uint32_t command,
     uint32_t commandArg
   );
+
+  virtual bool handlePreset(
+    MiLightClient* client,
+    uint8_t commandLsb,
+    uint32_t commandArg
+  );
 };
 
-#endif
+#endif

+ 63 - 56
lib/Udp/V6MiLightUdpServer.cpp

@@ -12,14 +12,14 @@ 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, 
+  0x20, 0x00, 0x00, 0x00, 0x16, 0x02, 0x62, 0x3A, 0xD5, 0xED, 0xA3, 0x01, 0xAE,
   0x08, 0x2D, 0x46, 0x61, 0x41, 0xA7, 0xF6, 0xDC, 0xAF
 };
 
 uint8_t V6MiLightUdpServer::START_SESSION_RESPONSE[] = {
-   0x28, 0x00, 0x00, 0x00, 0x11, 0x00, 0x02, 
+   0x28, 0x00, 0x00, 0x00, 0x11, 0x00, 0x02,
    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,  // should be replaced with hw addr
    0x69, 0xF0, 0x3C, 0x23, 0x00, 0x01,
    0xFF, 0xFF, // should be replaced with a session ID
@@ -49,20 +49,19 @@ uint8_t V6MiLightUdpServer::SEARCH_COMMAND[] = {
 };
 
 uint8_t V6MiLightUdpServer::SEARCH_RESPONSE[] = {
-  0x18, 0x00, 0x00, 0x00, 0x40, 0x02, 
+  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 
+  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, 
+  0x00, 0x00, 0x05, 0x00, 0x09, 0x78,
+  0x6C, 0x69, 0x6E, 0x6B, 0x5F, 0x64,
   0x65, 0x76, 0x07, 0x5B, 0xCD, 0x15
 };
 
@@ -70,13 +69,13 @@ 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 ,0x00 ,0x00, 0x00, 0x00,
   0x00, 0x00, 0x34
 };
 
 V6MiLightUdpServer::~V6MiLightUdpServer() {
   V6Session* cur = firstSession;
-  
+
   while (cur != NULL) {
     V6Session* next = cur->next;
     delete cur;
@@ -88,45 +87,45 @@ template <typename T>
 T V6MiLightUdpServer::readInt(uint8_t* packet) {
   size_t numBytes = sizeof(T);
   T value = 0;
-  
+
   for (size_t i = 0; i < numBytes; i++) {
     value |= packet[i] << (8 * (numBytes - i - 1));
   }
-  
+
   return value;
 }
 
 template <typename T>
 uint8_t* V6MiLightUdpServer::writeInt(const T& value, uint8_t* packet) {
   size_t numBytes = sizeof(T);
-  
+
   for (size_t i = 0; i < numBytes; i++) {
     packet[i] = (value >> (8 * (numBytes - i - 1))) & 0xFF;
   }
-  
+
   return packet + numBytes;
 }
-    
+
 uint16_t V6MiLightUdpServer::beginSession() {
   const uint16_t id = sessionId++;
-  
+
   V6Session* session = new V6Session(socket.remoteIP(), socket.remotePort(), id);
   session->next = firstSession;
   firstSession = session;
-  
+
   if (numSessions >= V6_MAX_SESSIONS) {
     V6Session* cur = firstSession;
-    
+
     for (size_t i = 1; i < V6_MAX_SESSIONS; i++) {
       cur = cur->next;
     }
-    
+
     delete cur->next;
     cur->next = NULL;
   } else {
     numSessions++;
   }
-  
+
   return id;
 }
 
@@ -134,8 +133,8 @@ void V6MiLightUdpServer::handleSearch() {
   const size_t packetLen = size(SEARCH_RESPONSE);
   uint8_t response[packetLen];
   memcpy(response, SEARCH_RESPONSE, packetLen);
-  WiFi.macAddress(response + 6);
-  
+  writeMacAddr(response + 6);
+
   socket.beginPacket(socket.remoteIP(), socket.remotePort());
   socket.write(response, packetLen);
   socket.endPacket();
@@ -145,39 +144,40 @@ void V6MiLightUdpServer::handleStartSession() {
   size_t len = size(START_SESSION_RESPONSE);
   uint8_t response[len];
   uint16_t sessionId = beginSession();
-  
+
   memcpy(response, START_SESSION_RESPONSE, len);
-  WiFi.macAddress(response + 7);
+  writeMacAddr(response + 7);
+
   response[19] = sessionId >> 8;
   response[20] = sessionId & 0xFF;
-  
+
   sendResponse(sessionId, response, len);
 }
-  
+
 bool V6MiLightUdpServer::sendResponse(uint16_t sessionId, uint8_t* responseBuffer, size_t responseSize) {
   V6Session* session = firstSession;
-  
+
   while (session != NULL) {
     if (session->sessionId == sessionId) {
       break;
     }
     session = session->next;
   }
-  
+
   if (session == NULL || session->sessionId != sessionId) {
     Serial.print("Received request with untracked session ID: ");
     Serial.println(sessionId);
     return false;
   }
-  
+
 #ifdef MILIGHT_UDP_DEBUG
   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;
 }
 
@@ -185,11 +185,11 @@ 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);
-  
+  writeMacAddr(response + 5);
+
   return sendResponse(sessionId, response, len);
 }
-  
+
 void V6MiLightUdpServer::handleCommand(
   uint16_t sessionId,
   uint8_t sequenceNum,
@@ -197,17 +197,17 @@ void V6MiLightUdpServer::handleCommand(
   uint8_t group,
   uint8_t checksum
 ) {
-  
+
   uint8_t cmdType = readInt<uint8_t>(cmd);
   uint32_t cmdHeader = readInt<uint32_t>(cmd+1);
   uint32_t cmdArg = readInt<uint32_t>(cmd+5);
-  
+
 #ifdef MILIGHT_UDP_DEBUG
   printf_P("Command cmdType: %02X, cmdHeader: %08X, cmdArg: %08X\n", cmdType, cmdHeader, cmdArg);
 #endif
-  
+
   bool handled = false;
-  
+
   if (cmdHeader == 0) {
     handled = handleOpenCommand(sessionId);
   } else {
@@ -220,17 +220,17 @@ void V6MiLightUdpServer::handleCommand(
       cmdArg
     );
   }
-  
+
   if (handled) {
     size_t len = size(COMMAND_RESPONSE);
     memcpy(responseBuffer, COMMAND_RESPONSE, len);
     responseBuffer[6] = sequenceNum;
-    
+
     sendResponse(sessionId, responseBuffer, len);
-    
+
     return;
   }
-  
+
 #ifdef MILIGHT_UDP_DEBUG
   printf_P("V6MiLightUdpServer - Unhandled command: ");
   for (size_t i = 0; i < V6_COMMAND_LEN; i++) {
@@ -243,12 +243,13 @@ void V6MiLightUdpServer::handleCommand(
 void V6MiLightUdpServer::handleHeartbeat(uint16_t sessionId) {
   char header[] = { 0xD8, 0x00, 0x00, 0x00, 0x07 };
   memcpy(responseBuffer, header, size(header));
-  WiFi.macAddress(responseBuffer+5);
-  responseBuffer[11] = 0;
+  writeMacAddr(responseBuffer + 5);
   
+  responseBuffer[11] = 0;
+
   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;
 }
@@ -257,7 +258,7 @@ void V6MiLightUdpServer::handlePacket(uint8_t* packet, size_t packetSize) {
 #ifdef MILIGHT_UDP_DEBUG
   printf_P("Packet size: %d\n", packetSize);
 #endif
-  
+
   if (MATCHES_PACKET(START_SESSION_COMMAND)) {
     handleStartSession();
   } else if (MATCHES_PACKET(HEARTBEAT_HEADER) || MATCHES_PACKET(HEARTBEAT_HEADER2)) {
@@ -271,13 +272,19 @@ void V6MiLightUdpServer::handlePacket(uint8_t* packet, size_t packetSize) {
     uint8_t* cmd = packet+10;
     uint8_t group = packet[19];
     uint8_t checksum = packet[21];
-    
+
 #ifdef MILIGHT_UDP_DEBUG
     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(F("Unhandled V6 packet"));
   }
-}
+}
+
+void V6MiLightUdpServer::writeMacAddr(uint8_t* packet) {
+  memset(packet, 0, 6);
+  packet[4] = deviceId >> 8;
+  packet[5] = deviceId;
+}

+ 14 - 15
lib/Udp/V6MiLightUdpServer.h

@@ -5,14 +5,13 @@
 #include <MiLightClient.h>
 #include <WiFiUdp.h>
 #include <MiLightUdpServer.h>
-#include <Vector.h>
 #include <V6CommandHandler.h>
 
 #define V6_COMMAND_LEN 8
 #define V6_MAX_SESSIONS 10
 
 #ifndef _V6_MILIGHT_UDP_SERVER
-#define _V6_MILIGHT_UDP_SERVER 
+#define _V6_MILIGHT_UDP_SERVER
 
 struct V6Session {
   V6Session(IPAddress ipAddr, uint16_t port, uint16_t sessionId)
@@ -21,7 +20,7 @@ struct V6Session {
       sessionId(sessionId),
       next(NULL)
   { }
-  
+
   IPAddress ipAddr;
   uint16_t port;
   uint16_t sessionId;
@@ -36,21 +35,21 @@ public:
       numSessions(0),
       firstSession(NULL)
   { }
-  
+
   ~V6MiLightUdpServer();
-  
+
   // Should return size of the response packet
   virtual void handlePacket(uint8_t* packet, size_t packetSize);
-  
+
   template <typename T>
   static T readInt(uint8_t* packet);
-  
+
   template <typename T>
   static uint8_t* writeInt(const T& value, uint8_t* packet);
-    
+
 protected:
   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;
@@ -58,21 +57,21 @@ protected:
   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();
   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 writeMacAddr(uint8_t* packet);
+
   void handleSearch();
   void handleStartSession();
   bool handleOpenCommand(uint16_t sessionId);

+ 48 - 24
lib/Udp/V6RgbCctCommandHandler.cpp

@@ -1,83 +1,107 @@
 #include <V6RgbCctCommandHandler.h>
 
+bool V6RgbCctCommandHandler::handlePreset(
+    MiLightClient* client,
+    uint8_t commandLsb,
+    uint32_t commandArg)
+{
+  if (commandLsb == 0) {
+    const uint8_t saturation = commandArg >> 24;
+    const uint8_t color = (commandArg >> 16);
+    const uint8_t brightness = (commandArg >> 8);
+
+    client->updateBrightness(brightness);
+    client->updateColorRaw(color);
+    client->updateSaturation(saturation);
+  } else if (commandLsb == 1) {
+    const uint8_t brightness = (commandArg >> 16);
+    const uint8_t kelvin = (commandArg >> 8);
+
+    client->updateBrightness(brightness);
+    client->updateTemperature(0x64 - kelvin);
+  } else {
+    return false;
+  }
+
+  return true;
+}
+
 bool V6RgbCctCommandHandler::handleCommand(
-    MiLightClient* client, 
-    uint16_t deviceId,
-    uint8_t group,
+    MiLightClient* client,
     uint32_t command,
     uint32_t commandArg)
 {
-  const uint8_t cmd = command & 0xFF;
+  const uint8_t cmd = command & 0x7F;
   const uint8_t arg = commandArg >> 24;
-  
-  client->prepare(MilightRgbCctConfig, deviceId, group);
-  
+
+  client->setHeld((command & 0x80) == 0x80);
+
   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);
+        client->enableNightMode();
         break;
-        
+
       case V2_RGB_CCT_SPEED_DOWN:
         client->modeSpeedDown();
         break;
-        
+
       case V2_RGB_CCT_SPEED_UP:
         client->modeSpeedUp();
         break;
-        
-      default: 
+
+      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);
   }
 }

+ 12 - 8
lib/Udp/V6RgbCctCommandHandler.h

@@ -1,7 +1,7 @@
 #include <V6CommandHandler.h>
 
 #ifndef _V6_RGB_CCT_COMMAND_HANDLER_H
-#define _V6_RGB_CCT_COMMAND_HANDLER_H 
+#define _V6_RGB_CCT_COMMAND_HANDLER_H
 
 enum V2CommandIds {
   V2_COLOR = 0x01,
@@ -25,17 +25,21 @@ public:
   V6RgbCctCommandHandler()
     : V6CommandHandler(0x0800, MilightRgbCctConfig)
   { }
-  
+
   virtual bool handleCommand(
-    MiLightClient* client, 
-    uint16_t deviceId,
-    uint8_t group,
+    MiLightClient* client,
     uint32_t command,
     uint32_t commandArg
   );
-  
+
+  virtual bool handlePreset(
+    MiLightClient* client,
+    uint8_t commandLsb,
+    uint32_t commandArg
+  );
+
   void handleUpdateColor(MiLightClient* client, uint32_t color);
-  
+
 };
 
-#endif
+#endif

+ 23 - 18
lib/Udp/V6RgbCommandHandler.cpp

@@ -1,60 +1,65 @@
 #include <V6RgbCommandHandler.h>
 
+bool V6RgbCommandHandler::handlePreset(
+    MiLightClient* client,
+    uint8_t commandLsb,
+    uint32_t commandArg)
+{
+}
+
 bool V6RgbCommandHandler::handleCommand(
-    MiLightClient* client, 
-    uint16_t deviceId,
-    uint8_t group,
+    MiLightClient* client,
     uint32_t command,
     uint32_t commandArg)
 {
-  const uint8_t cmd = command & 0xFF;
+  const uint8_t cmd = command & 0x7F;
   const uint8_t arg = commandArg >> 24;
-  
-  client->prepare(MilightRgbConfig, deviceId, 0);
-  
+
+  client->setHeld((command & 0x80) == 0x80);
+
   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;
-}
+}

+ 11 - 7
lib/Udp/V6RgbCommandHandler.h

@@ -1,7 +1,7 @@
 #include <V6CommandHandler.h>
 
 #ifndef _V6_RGB_COMMAND_HANDLER_H
-#define _V6_RGB_COMMAND_HANDLER_H 
+#define _V6_RGB_COMMAND_HANDLER_H
 
 enum RgbCommandIds {
   V2_RGB_COMMAND_PREFIX  = 0x02,
@@ -21,15 +21,19 @@ public:
   V6RgbCommandHandler()
     : V6CommandHandler(0x0500, MilightRgbConfig)
   { }
-  
+
   virtual bool handleCommand(
-    MiLightClient* client, 
-    uint16_t deviceId,
-    uint8_t group,
+    MiLightClient* client,
     uint32_t command,
     uint32_t commandArg
   );
-  
+
+  virtual bool handlePreset(
+    MiLightClient* client,
+    uint8_t commandLsb,
+    uint32_t commandArg
+  );
+
 };
 
-#endif
+#endif

+ 33 - 18
lib/Udp/V6RgbwCommandHandler.cpp

@@ -1,48 +1,63 @@
 #include <V6RgbwCommandHandler.h>
 
+bool V6RgbwCommandHandler::handlePreset(
+    MiLightClient* client,
+    uint8_t commandLsb,
+    uint32_t commandArg)
+{
+  if (commandLsb == 0) {
+    client->updateColorRaw(commandArg >> 24);
+    client->updateBrightness(commandArg >> 16);
+  } else if (commandLsb == 1) {
+    client->updateColorWhite();
+    client->updateBrightness(commandArg >> 16);
+  } else {
+    return false;
+  }
+
+  return true;
+}
+
 bool V6RgbwCommandHandler::handleCommand(
-    MiLightClient* client, 
-    uint16_t deviceId,
-    uint8_t group,
+    MiLightClient* client,
     uint32_t command,
     uint32_t commandArg)
 {
-  const uint8_t cmd = command & 0xFF;
+  const uint8_t cmd = command & 0x7F;
   const uint8_t arg = commandArg >> 24;
-  
-  client->prepare(MilightRgbwConfig, deviceId, 0);
-  
+
+  client->setHeld((command & 0x80) == 0x80);
+
   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);
+        client->enableNightMode();
         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);
@@ -54,6 +69,6 @@ bool V6RgbwCommandHandler::handleCommand(
     client->updateMode(arg);
     return true;
   }
-  
+
   return false;
-}
+}

+ 12 - 8
lib/Udp/V6RgbwCommandHandler.h

@@ -1,14 +1,14 @@
 #include <V6CommandHandler.h>
 
 #ifndef _V6_RGBW_COMMAND_HANDLER_H
-#define _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,
@@ -22,15 +22,19 @@ public:
   V6RgbwCommandHandler()
     : V6CommandHandler(0x0700, MilightRgbwConfig)
   { }
-  
+
   virtual bool handleCommand(
-    MiLightClient* client, 
-    uint16_t deviceId,
-    uint8_t group,
+    MiLightClient* client,
     uint32_t command,
     uint32_t commandArg
   );
-  
+
+  virtual bool handlePreset(
+    MiLightClient* client,
+    uint8_t commandLsb,
+    uint32_t commandArg
+  );
+
 };
 
-#endif
+#endif

+ 0 - 74
lib/Vector/Vector.h

@@ -1,74 +0,0 @@
-/**
- * Taken from: https://redstoner.com/forums/threads/840-minimal-class-to-replace-std-vector-in-c-for-arduino
- */
- 
-#ifndef _VECTOR_H
-#define _VECTOR_H
-
-// Minimal class to replace std::vector
-template<typename Data>
-class Vector {
-
-    size_t d_size; // Stores no. of actually stored objects
-    size_t d_capacity; // Stores allocated capacity
-    Data *d_data; // Stores data this is this "heap" we need a function that returns a pointer to this value, to print it
-public:
-    Vector() : d_size(0), d_capacity(0), d_data(0) {}; // Default constructor
-
-    Vector(Vector const &other) : d_size(other.d_size), d_capacity(other.d_capacity), d_data(0) //for when you set 1 vector = to another
-    {
-        d_data = (Data *)malloc(d_capacity*sizeof(Data));
-        memcpy(d_data, other.d_data, d_size*sizeof(Data));
-    }; // Copy constuctor
-
-    ~Vector() //this gets called
-    {
-        free(d_data);
-    }; // Destructor
-
-    Vector &operator=(Vector const &other)
-    {
-        free(d_data);
-        d_size = other.d_size;
-        d_capacity = other.d_capacity;
-        d_data = (Data *)malloc(d_capacity*sizeof(Data));
-        memcpy(d_data, other.d_data, d_size*sizeof(Data));
-        return *this;
-    }; // Needed for memory management
-
-    void push_back(Data const &x)
-    {
-        if (d_capacity == d_size) //when he pushes data onto the heap, he checks to see if the storage is full
-            resize();  //if full - resize
-
-        d_data[d_size++] = x;
-    }; // Adds new value. If needed, allocates more space
-
-    void Clear() //here
-    {
-        memset(d_data, 0, d_size);
-        d_capacity = 0;
-        d_size = 0;
-        free(d_data);
-    }
-
-    size_t size() const { return d_size; }; // Size getter
-
-    Data const &operator[](size_t idx) const { return d_data[idx]; }; // Const getter
-
-    Data &operator[](size_t idx) { return d_data[idx]; }; // Changeable getter
-
-    Data *pData() { return (Data*)d_data; }
-
-private:
-    void resize()
-    {
-        d_capacity = d_capacity ? d_capacity * 2 : 1;
-        Data *newdata = (Data *)malloc(d_capacity*sizeof(Data)); //allocates new memory
-        memcpy(newdata, d_data, d_size * sizeof(Data));  //copies all the old memory over
-        free(d_data);                                          //free old
-        d_data = newdata;
-    };// Allocates double the old space
-};
-
-#endif

+ 72 - 104
lib/WebServer/MiLightHttpServer.cpp

@@ -5,20 +5,22 @@
 #include <MiLightHttpServer.h>
 #include <MiLightRadioConfig.h>
 #include <GithubClient.h>
+#include <string.h>
+#include <TokenIterator.h>
 
 void MiLightHttpServer::begin() {
   applySettings(settings);
 
   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_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("/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("/raw_commands/:type", HTTP_PUT, [this](const UrlTokenBindings* b) { handleSendRaw(b); });
+  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(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(); });
@@ -30,13 +32,13 @@ void MiLightHttpServer::begin() {
       if (Update.hasError()) {
         server.send_P(
           500,
-          "text/plain",
+          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",
+          TEXT_PLAIN,
           PSTR("Success. Device will now reboot.")
         );
       }
@@ -91,12 +93,12 @@ void MiLightHttpServer::handleGetLatestRelease() {
   }
 
   if (!SPIFFS.exists(fsPath)) {
-    server.send_P(500, "text/plain", PSTR("Failed to stream API request from GitHub. Check Serial logs for more information."));
+    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");
+  server.streamFile(file, APPLICATION_JSON);
   SPIFFS.remove(fsPath);
 }
 
@@ -104,6 +106,14 @@ void MiLightHttpServer::handleClient() {
   server.handleClient();
 }
 
+void MiLightHttpServer::on(const char* path, HTTPMethod method, ESP8266WebServer::THandlerFunction handler) {
+  server.on(path, method, handler);
+}
+
+WiFiClient MiLightHttpServer::client() {
+  return server.client();
+}
+
 void MiLightHttpServer::handleSystemPost() {
   DynamicJsonBuffer buffer;
   JsonObject& request = buffer.parse(server.arg("plain"));
@@ -113,7 +123,7 @@ void MiLightHttpServer::handleSystemPost() {
   if (request.containsKey("command")) {
     if (request["command"] == "restart") {
       Serial.println(F("Restarting..."));
-      server.send(200, "text/plain", "true");
+      server.send(200, TEXT_PLAIN, "true");
 
       delay(100);
 
@@ -122,9 +132,9 @@ void MiLightHttpServer::handleSystemPost() {
   }
 
   if (handled) {
-    server.send(200, "text/plain", "true");
+    server.send(200, TEXT_PLAIN, "true");
   } else {
-    server.send(400, "text/plain", F("{\"error\":\"Unhandled command\"}"));
+    server.send(400, TEXT_PLAIN, F("{\"error\":\"Unhandled command\"}"));
   }
 }
 
@@ -139,14 +149,14 @@ void MiLightHttpServer::handleDownloadUpdate(const UrlTokenBindings* bindings) {
     size_t tries = 0;
 
     while (!result && tries++ <= MAX_DOWNLOAD_ATTEMPTS) {
-      printf("building url\n");
+      Serial.println(F("building url\n"));
       String urlPath = GithubClient::buildRepoPath(
         MILIGHT_GITHUB_USER,
         MILIGHT_GITHUB_REPO,
         MILIGHT_REPO_WEB_PATH
       );
 
-      printf("URL: %s\n", urlPath.c_str());
+      printf_P(PSTR("URL: %s\n"), urlPath.c_str());
 
       result = downloader.download(urlPath, WEB_INDEX_FILENAME);
     }
@@ -157,11 +167,11 @@ void MiLightHttpServer::handleDownloadUpdate(const UrlTokenBindings* bindings) {
       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."));
+      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(400, TEXT_PLAIN, body);
   }
 }
 
@@ -185,6 +195,7 @@ void MiLightHttpServer::handleAbout() {
 
   response["version"] = QUOTE(MILIGHT_HUB_VERSION);
   response["variant"] = QUOTE(FIRMWARE_VARIANT);
+  response["free_heap"] = ESP.getFreeHeap();
 
   String body;
   response.printTo(body);
@@ -204,7 +215,7 @@ void MiLightHttpServer::handleGetRadioConfigs() {
   String body;
   arr.printTo(body);
 
-  server.send(200, "application/json", body);
+  server.send(200, APPLICATION_JSON, body);
 }
 
 ESP8266WebServer::THandlerFunction MiLightHttpServer::handleServeFile(
@@ -262,9 +273,9 @@ void MiLightHttpServer::handleUpdateSettings() {
     this->applySettings(settings);
     this->settingsSavedHandler();
 
-    server.send(200, "application/json", "true");
+    server.send(200, APPLICATION_JSON, "true");
   } else {
-    server.send(400, "application/json", "\"Invalid JSON\"");
+    server.send(400, APPLICATION_JSON, "\"Invalid JSON\"");
   }
 }
 
@@ -276,7 +287,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;
   }
 
@@ -300,10 +311,10 @@ void MiLightHttpServer::handleListenGateway(const UrlTokenBindings* bindings) {
   char response[200];
   char* responseBuffer = response;
 
-  responseBuffer += sprintf(responseBuffer, "\nPacket received (%d bytes):\n", sizeof(packet));
+  responseBuffer += sprintf_P(responseBuffer, PSTR("\nPacket received (%d bytes):\n"), sizeof(packet));
   milightClient->formatPacket(packet, responseBuffer);
 
-  server.send(200, "text/plain", response);
+  server.send(200, TEXT_PLAIN, response);
 }
 
 void MiLightHttpServer::handleUpdateGroup(const UrlTokenBindings* urlBindings) {
@@ -311,101 +322,58 @@ void MiLightHttpServer::handleUpdateGroup(const UrlTokenBindings* urlBindings) {
   JsonObject& request = buffer.parse(server.arg("plain"));
 
   if (!request.success()) {
-    server.send(400, "text/plain", F("Invalid JSON"));
-    return;
-  }
-
-  const uint16_t deviceId = parseInt<uint16_t>(urlBindings->get("device_id"));
-  const uint8_t groupId = urlBindings->get("group_id").toInt();
-  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);
+    server.send(400, TEXT_PLAIN, F("Invalid JSON"));
     return;
   }
 
   milightClient->setResendCount(
     settings.httpRepeatFactor * settings.packetRepeats
   );
-  milightClient->prepare(*config, deviceId, groupId);
-
-  if (request.containsKey("status")) {
-    const String& statusStr = request.get<String>("status");
-    MiLightStatus status = (statusStr == "on" || statusStr == "true") ? ON : OFF;
-    milightClient->updateStatus(status);
-  }
-
-  if (request.containsKey("command")) {
-    if (request["command"] == "unpair") {
-      milightClient->unpair();
-    }
-
-    if (request["command"] == "pair") {
-      milightClient->pair();
-    }
 
-    if (request["command"] == "set_white") {
-      milightClient->updateColorWhite();
-    }
-
-    if (request["command"] == "level_up") {
-      milightClient->increaseBrightness();
-    }
-
-    if (request["command"] == "level_down") {
-      milightClient->decreaseBrightness();
-    }
-
-    if (request["command"] == "temperature_up") {
-      milightClient->increaseTemperature();
-    }
-
-    if (request["command"] == "temperature_down") {
-      milightClient->decreaseTemperature();
+  String _deviceIds = urlBindings->get("device_id");
+  String _groupIds = urlBindings->get("group_id");
+  String _radioTypes = urlBindings->get("type");
+  char deviceIds[_deviceIds.length()];
+  char groupIds[_groupIds.length()];
+  char radioTypes[_radioTypes.length()];
+  strcpy(radioTypes, _radioTypes.c_str());
+  strcpy(groupIds, _groupIds.c_str());
+  strcpy(deviceIds, _deviceIds.c_str());
+
+  TokenIterator deviceIdItr(deviceIds, _deviceIds.length());
+  TokenIterator groupIdItr(groupIds, _groupIds.length());
+  TokenIterator radioTypesItr(radioTypes, _radioTypes.length());
+
+  while (radioTypesItr.hasNext()) {
+    const char* _radioType = radioTypesItr.nextToken();
+    MiLightRadioConfig* config = MiLightRadioConfig::fromString(_radioType);
+
+    if (config == NULL) {
+      String body = "Unknown device type: ";
+      body += String(_radioType);
+      server.send(400, TEXT_PLAIN, body);
+      return;
     }
 
-    if (request["command"] == "next_mode") {
-      milightClient->nextMode();
-    }
+    deviceIdItr.reset();
+    while (deviceIdItr.hasNext()) {
+      const uint16_t deviceId = parseInt<uint16_t>(deviceIdItr.nextToken());
 
-    if (request["command"] == "previous_mode") {
-      milightClient->previousMode();
-    }
+      groupIdItr.reset();
+      while (groupIdItr.hasNext()) {
+        const uint8_t groupId = atoi(groupIdItr.nextToken());
 
-    if (request["command"] == "mode_speed_down") {
-      milightClient->modeSpeedDown();
-    }
-
-    if (request["command"] == "mode_speed_up") {
-      milightClient->modeSpeedUp();
+        milightClient->prepare(*config, deviceId, groupId);
+        handleRequest(request);
+      }
     }
   }
 
-  if (request.containsKey("hue")) {
-    milightClient->updateHue(request["hue"]);
-  }
-
-  if (request.containsKey("level")) {
-    milightClient->updateBrightness(request["level"]);
-  }
-
-  if (request.containsKey("temperature")) {
-    milightClient->updateTemperature(request["temperature"]);
-  }
-
-  if (request.containsKey("saturation")) {
-    milightClient->updateSaturation(request["saturation"]);
-  }
-
-  if (request.containsKey("mode")) {
-    milightClient->updateMode(request["mode"]);
-  }
-
-  milightClient->setResendCount(settings.packetRepeats);
+  server.send(200, APPLICATION_JSON, "true");
+}
 
-  server.send(200, "application/json", "true");
+void MiLightHttpServer::handleRequest(const JsonObject& request) {
+  milightClient->update(request);
 }
 
 void MiLightHttpServer::handleSendRaw(const UrlTokenBindings* bindings) {
@@ -417,7 +385,7 @@ void MiLightHttpServer::handleSendRaw(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;
   }
 
@@ -436,5 +404,5 @@ void MiLightHttpServer::handleSendRaw(const UrlTokenBindings* bindings) {
     milightClient->write(packet);
   }
 
-  server.send(200, "text/plain", "true");
+  server.send(200, TEXT_PLAIN, "true");
 }

+ 20 - 13
lib/WebServer/MiLightHttpServer.h

@@ -3,7 +3,7 @@
 #include <Settings.h>
 
 #ifndef _MILIGHT_HTTP_SERVER
-#define _MILIGHT_HTTP_SERVER 
+#define _MILIGHT_HTTP_SERVER
 
 #define MAX_DOWNLOAD_ATTEMPTS 3
 
@@ -12,30 +12,35 @@ 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";
+
 class MiLightHttpServer {
 public:
   MiLightHttpServer(Settings& settings, MiLightClient*& milightClient)
     : server(WebServer(80)),
       milightClient(milightClient),
       settings(settings)
-  { 
+  {
     this->applySettings(settings);
   }
-  
+
   void begin();
   void handleClient();
   void onSettingsSaved(SettingsSavedHandler handler);
-  
+  void on(const char* path, HTTPMethod method, ESP8266WebServer::THandlerFunction handler);
+  WiFiClient client();
+
 protected:
   ESP8266WebServer::THandlerFunction handleServeFile(
-    const char* filename, 
-    const char* contentType, 
-    const char* defaultText = NULL); 
-    
+    const char* filename,
+    const char* contentType,
+    const char* defaultText = NULL);
+
   bool serveFile(const char* file, const char* contentType = "text/html");
   ESP8266WebServer::THandlerFunction handleUpdateFile(const char* filename);
   void applySettings(Settings& settings);
-  
+
   void handleUpdateSettings();
   void handleGetRadioConfigs();
   void handleAbout();
@@ -45,14 +50,16 @@ protected:
   void handleSendRaw(const UrlTokenBindings* urlBindings);
   void handleUpdateGroup(const UrlTokenBindings* urlBindings);
   void handleDownloadUpdate(const UrlTokenBindings* urlBindings);
-  
+
+  void handleRequest(const JsonObject& request);
+
   File updateFile;
-  
+
   WebServer server;
   Settings& settings;
   MiLightClient*& milightClient;
   SettingsSavedHandler settingsSavedHandler;
-  
+
 };
 
-#endif
+#endif

+ 46 - 69
lib/WebServer/PatternHandler.cpp

@@ -1,85 +1,62 @@
 #include <PatternHandler.h>
-  
+
 PatternHandler::PatternHandler(
-    const String& pattern, 
-    const HTTPMethod method, 
-    const PatternHandler::TPatternHandlerFn fn
-  ) : method(method), fn(fn), tokenPositions(NULL) {
-  Vector<StringToken>* tokenPositions = new Vector<StringToken>();
-  tokenize(pattern, tokenPositions);
-  
-  numPatternTokens = tokenPositions->size();
-  patternTokens = new String[numPatternTokens];
-  
-  for (int i = 0; i < tokenPositions->size(); i++) {
-    patternTokens[i] = (*tokenPositions)[i].extract(pattern);
-  }
-  
-  delete tokenPositions;
+    const String& pattern,
+    const HTTPMethod method,
+    const PatternHandler::TPatternHandlerFn fn)
+  : method(method),
+    fn(fn),
+    _pattern(new char[pattern.length() + 1]),
+    patternTokens(NULL)
+{
+  strcpy(_pattern, pattern.c_str());
+  patternTokens = new TokenIterator(_pattern, pattern.length(), '/');
 }
-  
+
+PatternHandler::~PatternHandler() {
+  delete _pattern;
+  delete patternTokens;
+}
+
 bool PatternHandler::canHandle(HTTPMethod requestMethod, String requestUri) {
-  if (requestMethod != HTTP_ANY && requestMethod != this->method) {
+  if (this->method != HTTP_ANY && requestMethod != this->method) {
     return false;
   }
-  
-  if (tokenPositions) {
-    delete tokenPositions;
-  }
-  
+
   bool canHandle = true;
-  
-  tokenPositions = new Vector<StringToken>();
-  tokenize(requestUri, tokenPositions);
-  
-  if (numPatternTokens == tokenPositions->size()) {
-    for (int i = 0; i < numPatternTokens; i++) {
-      const StringToken urlTokenP = (*tokenPositions)[i];
-      
-      if (!patternTokens[i].startsWith(":") 
-        && patternTokens[i] != urlTokenP.extract(requestUri)) {
-        canHandle = false;
-        break;
-      }
+
+  char requestUriCopy[requestUri.length() + 1];
+  strcpy(requestUriCopy, requestUri.c_str());
+  TokenIterator requestTokens(requestUriCopy, requestUri.length(), '/');
+
+  patternTokens->reset();
+  while (patternTokens->hasNext() && requestTokens.hasNext()) {
+    const char* patternToken = patternTokens->nextToken();
+    const char* requestToken = requestTokens.nextToken();
+
+    if (patternToken[0] != ':' && strcmp(patternToken, requestToken) != 0) {
+      canHandle = false;
+      break;
+    }
+
+    if (patternTokens->hasNext() != requestTokens.hasNext()) {
+      canHandle = false;
+      break;
     }
-  } else {
-    canHandle = false;
   }
-  
+
   return canHandle;
 }
-  
+
 bool PatternHandler::handle(ESP8266WebServer& server, HTTPMethod requestMethod, String requestUri) {
   if (! canHandle(requestMethod, requestUri)) {
     return false;
   }
-  
-  UrlTokenBindings* bindings = new UrlTokenBindings(patternTokens, tokenPositions, requestUri);
-  fn(bindings);
-  
-  delete bindings;
-}
 
-void PatternHandler::tokenize(const String& path, Vector<StringToken>* tokenPositions) {
-  int lastStart = 0;
-  int currentPosition = 0;
-  
-  for (int i = 0; i < path.length(); i++) {
-    if (path.charAt(i) == '/' || i == path.length()-1) {
-      // If we're in the last position, include the last character if it isn't
-      // a '/'
-      if (path.charAt(i) != '/') {
-        currentPosition++;
-      }
-      
-      if (lastStart > 0 && currentPosition > lastStart) {
-        StringToken token(lastStart, currentPosition);
-        tokenPositions->push_back(token);
-      }
-      
-      lastStart = i+1;
-    }
-      
-    currentPosition++;
-  }
-}
+  char requestUriCopy[requestUri.length()];
+  strcpy(requestUriCopy, requestUri.c_str());
+  TokenIterator requestTokens(requestUriCopy, requestUri.length(), '/');
+
+  UrlTokenBindings bindings(*patternTokens, requestTokens);
+  fn(&bindings);
+}

+ 11 - 71
lib/WebServer/PatternHandler.h

@@ -3,86 +3,26 @@
 
 #include <Arduino.h>
 #include <ESP8266WebServer.h>
-#include <Vector.h>
 #include <functional>
-
-struct StringToken {
-  StringToken(const int start, const int end) 
-    : start(start), end(end) { }
-  
-  int start;
-  int end;
-  
-  const String extract(const String& s) const {
-    return s.substring(start, end);
-  }
-};
-
-class UrlTokenBindings {
-public:
-  
-  UrlTokenBindings(const String* patternTokens, 
-    const Vector<StringToken>* urlTokenPositions,
-    const String& url
-  ) 
-    : patternTokens(patternTokens),
-      numTokens(urlTokenPositions->size()) 
-  {
-    urlTokens = new String[numTokens];
-    for (int i = 0; i < numTokens; i++) {
-      urlTokens[i] = (*urlTokenPositions)[i].extract(url);
-    }
-  }
-  
-  ~UrlTokenBindings() {
-    delete[] urlTokens;
-  }
-  
-  bool hasBinding(const String& key) const {
-    for (int i = 0; i < numTokens; i++) {
-      if (patternTokens[i] == key) {
-        return true;
-      }
-    }
-    
-    return false;
-  }
-  
-  String get(const String& key) const {
-    for (int i = 0; i < numTokens; i++) {
-      if (patternTokens[i].substring(1) == key) {
-        return urlTokens[i];
-      }
-    }
-  }
-  
-private:
-  const String* patternTokens;
-  String* urlTokens;
-  const size_t numTokens;
-};
+#include <TokenIterator.h>
+#include <UrlTokenBindings.h>
 
 class PatternHandler : public RequestHandler {
 public:
   typedef std::function<void(UrlTokenBindings*)> TPatternHandlerFn;
-  
-  PatternHandler(const String& pattern, 
-    const HTTPMethod method, 
+
+  PatternHandler(const String& pattern,
+    const HTTPMethod method,
     const TPatternHandlerFn fn);
-  
-  ~PatternHandler() {
-    delete patternTokens;
-  }
-  
-  static void tokenize(const String& path, Vector<StringToken>* tokenPositions);
-  
+
+  ~PatternHandler();
+
   bool canHandle(HTTPMethod requestMethod, String requestUri) override;
   bool handle(ESP8266WebServer& server, HTTPMethod requesetMethod, String requestUri) override;
-  
+
 private:
-  size_t numPatternTokens;
-  String* patternTokens;
-  Vector<StringToken>* tokenPositions;
+  char* _pattern;
+  TokenIterator* patternTokens;
   const HTTPMethod method;
   const PatternHandler::TPatternHandlerFn fn;
 };

+ 2 - 2
lib/WebServer/WebServer.cpp

@@ -16,7 +16,7 @@ void WebServer::disableAuthentication() {
 }
 
 void WebServer::_handleRequest() {
-  if (this->authEnabled 
+  if (this->authEnabled
     && !this->authenticate(this->username.c_str(), this->password.c_str())) {
     this->requestAuthentication();
   } else {
@@ -82,4 +82,4 @@ void WebServer::handleClient() {
       return;
     }
   }
-}
+}

+ 6 - 2
platformio.ini

@@ -16,26 +16,30 @@ lib_deps_external =
   RF24
   WiFiManager
   ArduinoJson
+  PubSubClient
+  https://github.com/ratkins/RGBConverter
 build_flags = !python .get_version.py
+# -D MQTT_DEBUG
 # -D MILIGHT_UDP_DEBUG
+# -D DEBUG_PRINTF
 
 [env:nodemcuv2]
 platform = espressif8266
 framework = arduino
 board = nodemcuv2
+build_flags = ${common.build_flags} -Wl,-Tesp8266.flash.4m1m.ld -D FIRMWARE_VARIANT=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
 framework = arduino
 board = d1_mini
+build_flags = ${common.build_flags} -Wl,-Tesp8266.flash.4m1m.ld -D FIRMWARE_VARIANT=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

+ 54 - 14
src/main.cpp

@@ -11,13 +11,19 @@
 #include <MiLightHttpServer.h>
 #include <Settings.h>
 #include <MiLightUdpServer.h>
+#include <ESP8266mDNS.h>
+#include <ESP8266SSDP.h>
+#include <MqttClient.h>
+#include <RGBConverter.h>
 
 WiFiManager wifiManager;
 
 Settings settings;
 
 MiLightClient* milightClient;
+MiLightRadioFactory* radioFactory;
 MiLightHttpServer *httpServer;
+MqttClient* mqttClient;
 
 int numUdpServers = 0;
 MiLightUdpServer** udpServers;
@@ -29,13 +35,13 @@ void initMilightUdpServers() {
         delete udpServers[i];
       }
     }
-    
+
     delete udpServers;
   }
-  
+
   udpServers = new MiLightUdpServer*[settings.numGatewayConfigs];
   numUdpServers = settings.numGatewayConfigs;
-  
+
   for (size_t i = 0; i < settings.numGatewayConfigs; i++) {
     GatewayConfig* config = settings.gatewayConfigs[i];
     MiLightUdpServer* server = MiLightUdpServer::fromVersion(
@@ -44,7 +50,7 @@ void initMilightUdpServers() {
       config->port,
       config->deviceId
     );
-    
+
     if (server == NULL) {
       Serial.print(F("Error creating UDP server with protocol version: "));
       Serial.println(config->protocolVersion);
@@ -55,17 +61,32 @@ void initMilightUdpServers() {
   }
 }
 
-void initMilightClient() {
+
+void applySettings() {
   if (milightClient) {
     delete milightClient;
   }
-  
-  milightClient = new MiLightClient(settings.cePin, settings.csnPin);
+  if (radioFactory) {
+    delete radioFactory;
+  }
+  if (mqttClient) {
+    delete mqttClient;
+  }
+
+  radioFactory = MiLightRadioFactory::fromSettings(settings);
+
+  if (radioFactory == NULL) {
+    Serial.println(F("ERROR: unable to construct radio factory"));
+  }
+
+  milightClient = new MiLightClient(radioFactory);
   milightClient->begin();
-}
 
-void applySettings() {
-  initMilightClient();
+  if (settings.mqttServer().length() > 0) {
+    mqttClient = new MqttClient(settings, milightClient);
+    mqttClient->begin();
+  }
+
   initMilightUdpServers();
 }
 
@@ -73,7 +94,7 @@ bool shouldRestart() {
   if (! settings.isAutoRestartEnabled()) {
     return false;
   }
-  
+
   return settings.getAutoRestartPeriod()*60*1000 < millis();
 }
 
@@ -83,21 +104,40 @@ void setup() {
   SPIFFS.begin();
   Settings::load(settings);
   applySettings();
-  
+
+  if (! MDNS.begin("milight-hub")) {
+    Serial.println(F("Error setting up MDNS responder"));
+  }
+
+  MDNS.addService("http", "tcp", 80);
+
+  SSDP.setSchemaURL("description.xml");
+  SSDP.setHTTPPort(80);
+  SSDP.setName("ESP8266 MiLight Gateway");
+  SSDP.setSerialNumber(ESP.getChipId());
+  SSDP.setURL("/");
+  SSDP.setDeviceType("upnp:rootdevice");
+  SSDP.begin();
+
   httpServer = new MiLightHttpServer(settings, milightClient);
   httpServer->onSettingsSaved(applySettings);
+  httpServer->on("/description.xml", HTTP_GET, []() { SSDP.schema(httpServer->client()); });
   httpServer->begin();
 }
 
 void loop() {
   httpServer->handleClient();
-  
+
+  if (mqttClient) {
+    mqttClient->handleClient();
+  }
+
   if (udpServers) {
     for (size_t i = 0; i < settings.numGatewayConfigs; i++) {
       udpServers[i]->handleClient();
     }
   }
-  
+
   if (shouldRestart()) {
     Serial.println(F("Auto-restart triggered. Restarting..."));
     ESP.restart();