Forráskód Böngészése

Merge pull request #22 from sidoh/new_protocol

v1.0.0 release
Chris Mullins 8 éve
szülő
commit
48355c278f

+ 27 - 20
README.md

@@ -26,35 +26,39 @@ This module is an SPI device. [This guide](https://www.mysensors.org/build/esp82
 
 #### Setting up the ESP
 
-1. Build from source. I use [PlatformIO](http://platformio.org/), but it's probably not hard to build this in the Arduino IDE.
-2. Flash an ESP8266 with the firmware.
-3. Connect to the "ESP XXXX" WiFi network to configure network settings. Alternatively you can update `main.cpp` to connect to your network directly.
+You'll need to flash the firmware and a SPIFFS image. It's really easy to do this with [PlatformIO](http://platformio.org/):
 
-#### Installing the Web UI
+```
+export ESP_BOARD=nodemcuv2
+platformio run -u $ESP_BOARD --target upload
+platformio run -u $ESP_BOARD --target uploadfs
+```
 
-The HTTP endpoints (shown below) will be fully functional at this point, but the firmware doesn't ship with a web UI (I didn't want to maintain a website in Arduino Strings).
+Of course make sure to substitute `nodemcuv2` with the board that you're using.
 
-If you want the UI, upload it to the `/web` endpoint. curl command:
+#### Configure WiFi
 
-```
-$ curl -X POST -F 'image=@web/index.html' <ip of ESP>/web
-success%
-```
+This project uses [WiFiManager](https://github.com/tzapu/WiFiManager) to avoid the need to hardcode AP credentials in the firmware.
 
-You should now be able to navigate to `http://<ip of ESP>`. It should look like this:
+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.
+
+#### 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:
 
 ![Web UI](http://imgur.com/XNNigvL.png)
 
 ## REST endpoints
 
 1. `GET /`. Opens web UI. You'll need to upload it first.
-2. `GET /settings`. Gets current settings as JSON.
-3. `PUT /settings`. Patches settings (e.g., doesn't overwrite keys that aren't present). Accepts a JSON blob in the body.
-4. `GET /gateway_traffic`. Starts an HTTP long poll. Returns any Milight traffic it hears. Useful if you need to know what your Milight gateway/remote ID is.
-5. `PUT /gateways/:device_id/:device_type/:group_id`. Controls or sends commands to `:group_id` from `:device_id`. Since protocols for RGBW/CCT are different, specify one of `rgbw` or `cct` as `:device_type. Accepts a JSON blob.
-6. `PUT /gateways/:device_id/:device_type`. A few commands have support for being sent to all groups. You can send those here.
-7. `POST /firmware`. OTA firmware update.
-8. `POST /web`. Update web UI.
+1. `GET /settings`. Gets current settings as JSON.
+1. `PUT /settings`. Patches settings (e.g., doesn't overwrite keys that aren't present). Accepts a JSON blob in the body.
+1. `GET /radio_configs`. Get a list of supported radio configs (aka `device_type`s).
+1. `GET /gateway_traffic/:device_type`. Starts an HTTP long poll. Returns any Milight traffic it hears. Useful if you need to know what your Milight gateway/remote ID is. Since protocols for RGBW/CCT are different, specify one of `rgbw`, `cct`, or `rgb_cct` as `:device_type. Accepts a JSON blob.
+1. `PUT /gateways/:device_id/:device_type/:group_id`. Controls or sends commands to `:group_id` from `:device_id`. 
+1. `PUT /gateways/:device_id/:device_type`. A few commands have support for being sent to all groups. You can send those here.
+1. `POST /firmware`. OTA firmware update.
+1. `POST /web`. Update web UI.
 
 #### Bulb commands
 
@@ -63,8 +67,9 @@ Route (5) supports these commands:
 1. `status`. Toggles on/off. Can be "on", "off", "true", or "false".
 2. `hue`. (RGBW only) This is the only way to control color with these bulbs. Should be in the range `[0, 359]`.
 3. `level`. (RGBW only) Controls brightness. Should be in the range `[0, 100]`.
-4. `temperature`. (CCT only) Controls white temperature. Should be in the range `[0, 10]`.
-5. `command`. Sends a command to the group. Can be one of:
+4. `temperature`. (CCT only) Controls white temperature. Should be in the range `[0, 100]`.
+5. `saturation`. (new RGB+CCT only) Controls saturation.
+6. `command`. Sends a command to the group. Can be one of:
    * `set_white`. (RGBW only) Turns off RGB and enters WW/CW mode.
    * `pair`. Emulates the pairing process. Send this command right as you connect an unpaired bulb and it will pair with the device ID being used.
    * `unpair`. Emulates the unpairing process. Send as you connect a paired bulb to have it disassociate with the device ID being used.
@@ -91,3 +96,5 @@ true%
 ## 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.

+ 83 - 29
web/index.html

@@ -79,6 +79,9 @@
       "http_repeat_factor"
     ];
     
+    var UDP_PROTOCOL_VERSIONS = [ 5, 6 ];
+    var DEFAULT_UDP_PROTOCL_VERSION = 5;
+    
     var selectize;
     
     var toHex = function(v) {
@@ -94,6 +97,10 @@
         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;
     }
@@ -127,7 +134,7 @@
       });
     };
     
-    var gatewayServerRow = function(deviceId, port) {
+    var gatewayServerRow = function(deviceId, port, version) {
       var elmt = '<tr>';
       elmt += '<td>';
       elmt += '<input name="deviceIds[]" class="form-control" value="' + deviceId + '"/>';
@@ -136,6 +143,20 @@
       elmt += '<input name="ports[]" class="form-control" value="' + port + '"/>';;
       elmt += '</td>';
       elmt += '<td>';
+      elmt += '<div class="btn-group" data-toggle="buttons">';
+      
+      for (var i = 0; i < UDP_PROTOCOL_VERSIONS.length; i++) {
+        var val = UDP_PROTOCOL_VERSIONS[i]
+          , selected = (version == val || (val == DEFAULT_UDP_PROTOCL_VERSION && !UDP_PROTOCOL_VERSIONS.includes(version)));
+        
+        elmt += '<label class="btn btn-secondary' + (selected ? ' active' : '') + '">';
+        elmt += '<input type="radio" name="versions[]" autocomplete="off" data-value="' + val + '" ' 
+          + (selected ? 'checked' : '') +'> ' + val;
+        elmt += '</label>';
+      }
+      
+      elmt += '</div></td>';
+      elmt += '<td>';
       elmt += '<button class="btn btn-danger remove-gateway-server">';
       elmt += '<i class="glyphicon glyphicon-remove"></i>';
       elmt += '</button>';
@@ -165,7 +186,7 @@
         var gatewayForm = $('#gateway-server-configs').html('');
         if (val.gateway_configs) {
           val.gateway_configs.forEach(function(v) {
-            gatewayForm.append(gatewayServerRow(toHex(v[0]), v[1]));
+            gatewayForm.append(gatewayServerRow(toHex(v[0]), v[1], v[2]));
           });
         }
       });
@@ -200,11 +221,15 @@
           return val;
         }
       });
+      
+      var versions = $('.active input[name="versions[]"]', form).map(function(i, v) {
+        return $(v).data('value');
+      });
         
       if (!errors) {
         var data = [];
         for (var i = 0; i < deviceIds.length; i++) {
-          data[i] = [deviceIds[i], ports[i], 0];
+          data[i] = [deviceIds[i], ports[i], versions[i]];
         }
         $.ajax(
           '/settings',
@@ -230,10 +255,10 @@
       var currentMode = getCurrentMode();
       
       $('.mode-option').map(function() {
-        if ($(this).data('for') != currentMode) {
-          $(this).hide();
-        } else {
+        if ($(this).data('for').split(',').includes(currentMode)) {
           $(this).show();
+        } else {
+          $(this).hide();
         }
       });
     };
@@ -409,24 +434,26 @@
       </div>
       
       <div class="col-sm-3">
-        <label for="groupId">Group</label>
+        <div class="mode-option" id="group-option" data-for="cct,rgbw,rgb_cct">
+          <label for="groupId">Group</label>
         
-        <div class="btn-group" id="groupId" data-toggle="buttons">
-          <label class="btn btn-secondary active">
-            <input type="radio" name="options" autocomplete="off" data-value="1" checked> 1
-          </label>
-          <label class="btn btn-secondary">
-            <input type="radio" name="options" autocomplete="off" data-value="2"> 2
-          </label>
-          <label class="btn btn-secondary">
-            <input type="radio" name="options" autocomplete="off" data-value="3"> 3
-          </label>
-          <label class="btn btn-secondary">
-            <input type="radio" name="options" autocomplete="off" data-value="4"> 4
-          </label>
-          <label class="btn btn-secondary">
-            <input type="radio" name="options" autocomplete="off" data-value="0"> All
-          </label>
+          <div class="btn-group" id="groupId" data-toggle="buttons">
+            <label class="btn btn-secondary active">
+              <input type="radio" name="options" autocomplete="off" data-value="1" checked> 1
+            </label>
+            <label class="btn btn-secondary">
+              <input type="radio" name="options" autocomplete="off" data-value="2"> 2
+            </label>
+            <label class="btn btn-secondary">
+              <input type="radio" name="options" autocomplete="off" data-value="3"> 3
+            </label>
+            <label class="btn btn-secondary">
+              <input type="radio" name="options" autocomplete="off" data-value="4"> 4
+            </label>
+            <label class="btn btn-secondary">
+              <input type="radio" name="options" autocomplete="off" data-value="0"> All
+            </label>
+          </div>
         </div>
       </div>
       
@@ -440,12 +467,18 @@
           <label class="btn btn-secondary">
             <input type="radio" name="mode" autocomplete="off" data-value="cct"> CCT
           </label>
+          <label class="btn btn-secondary">
+            <input type="radio" name="mode" autocomplete="off" data-value="rgb_cct"> RGB+CCT
+          </label>
+          <label class="btn btn-secondary">
+            <input type="radio" name="mode" autocomplete="off" data-value="rgb"> RGB
+          </label>
         </div>
       </div>
     </div>
     
     <div class="row"><div class="col-sm-12">
-    <div class="mode-option" data-for="rgbw">
+    <div class="mode-option" data-for="rgbw,rgb_cct,rgb">
       <div class="row">
         <div class="col-sm-12">
           <h5>Hue</h5>
@@ -462,8 +495,25 @@
       </div>
     </div>
     </div></div>
+    
+    <div class="mode-option" data-for="rgb_cct">
+      <div class="row">
+        <div class="col-sm-12">
+          <h5>Saturation</h5>
+        </div>
+      </div>
+      <div class="row">
+        <div class="col-sm-6">
+          <input class="slider raw-update" name="saturation"
+              data-slider-min="0"
+              data-slider-max="100"
+              data-slider-value="100"
+          />
+        </div>
+      </div>
+    </div>
           
-    <div class="mode-option" data-for="cct">
+    <div class="mode-option" data-for="cct,rgb_cct">
       <div class="row">
         <div class="col-sm-12">
           <h5>Color Temperature</h5>
@@ -473,8 +523,8 @@
         <div class="col-sm-6">
           <input class="slider raw-update" name="temperature"
               data-slider-min="0"
-              data-slider-max="10"
-              data-slider-value="10"
+              data-slider-max="100"
+              data-slider-value="100"
           />
         </div>
       </div>
@@ -508,7 +558,7 @@
           <li>
             <input type="checkbox" name="status" class="raw-update" data-toggle="toggle" checked/>
           </li>
-          <div class="mode-option inline" data-for="rgbw">
+          <div class="mode-option inline" data-for="rgbw,rgb_cct">
             <li>
               <button type="button" class="btn btn-secondary command-btn" data-command="set_white">White</button>
             </li>
@@ -550,6 +600,7 @@
               <tr>
                 <th>Device ID</th>
                 <th>UDP Port</th>
+                <th>Protocol Version</th>
               </tr>
             </thead>
             <tbody id="gateway-server-configs">
@@ -598,7 +649,10 @@
             <input type="radio" name="options" autocomplete="off" data-value="cct"> CCT
           </label>
           <label class="btn btn-secondary">
-            <input type="radio" name="options" autocomplete="off" data-value="rgbw_cct"> RGBW+CCT
+            <input type="radio" name="options" autocomplete="off" data-value="rgb_cct"> RGB+CCT
+          </label>
+          <label class="btn btn-secondary">
+            <input type="radio" name="options" autocomplete="off" data-value="rgb"> RGB
           </label>
         </div>
         

BIN
dist/firmware-d1-mini.bin


BIN
dist/firmware-esp07.bin


BIN
dist/firmware-esp12.bin


BIN
dist/firmware-nodemcuv2.bin


+ 22 - 3
lib/Helpers/IntParsing.h

@@ -4,12 +4,12 @@
 #include <Arduino.h>
 
 template <typename T>
-const T strToHex(const String& s) {
+const T strToHex(const char* s, size_t length) {
   T value = 0;
   T base = 1;
   
-  for (int i = s.length() - 1; i >= 0; i--) {
-    const char c = s.charAt(i);
+  for (int i = length-1; i >= 0; i--) {
+    const char c = s[i];
     
     if (c >= '0' && c <= '9') {
       value += ((c - '0') * base);
@@ -28,6 +28,11 @@ const T strToHex(const String& s) {
 }
 
 template <typename T>
+const T strToHex(const String& s) {
+  return strToHex<T>(s.c_str(), s.length());
+}
+
+template <typename T>
 const T parseInt(const String& s) {
   if (s.startsWith("0x")) {
     return strToHex<T>(s.substring(2));
@@ -36,4 +41,18 @@ const T parseInt(const String& s) {
   }
 }
 
+template <typename T>
+void hexStrToBytes(const char* s, const size_t sLen, T* buffer, size_t maxLen) {
+  int idx = 0;
+  
+  for (int i = 0; i < sLen && idx < maxLen; ) {
+    buffer[idx++] = strToHex<T>(s+i, 2);
+    i+= 2;
+    
+    while (i < (sLen - 1) && s[i] == ' ') {
+      i++;
+    }
+  }
+}
+
 #endif

+ 99 - 0
lib/MiLight/CctPacketFormatter.cpp

@@ -0,0 +1,99 @@
+#include <CctPacketFormatter.h>
+#include <MiLightButtons.h>
+
+void CctPacketFormatter::initializePacket(uint8_t* packet) {
+  size_t packetPtr = 0;
+  
+  packet[packetPtr++] = CCT;
+  packet[packetPtr++] = deviceId >> 8;
+  packet[packetPtr++] = deviceId & 0xFF;
+  packet[packetPtr++] = groupId;
+  packet[packetPtr++] = 0;
+  packet[packetPtr++] = sequenceNum;
+  packet[packetPtr++] = sequenceNum++;
+}
+
+void CctPacketFormatter::updateBrightness(uint8_t value) {
+  valueByStepFunction(
+    &PacketFormatter::increaseBrightness,
+    &PacketFormatter::decreaseBrightness,
+    CCT_INTERVALS,
+    value / CCT_INTERVALS
+  );
+}
+
+void CctPacketFormatter::updateTemperature(uint8_t value) {
+  valueByStepFunction(
+    &PacketFormatter::increaseTemperature,
+    &PacketFormatter::decreaseTemperature,
+    CCT_INTERVALS,
+    value / CCT_INTERVALS
+  );
+}
+  
+void CctPacketFormatter::command(uint8_t command, uint8_t arg) {
+  pushPacket();
+  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);
+}
+
+void CctPacketFormatter::decreaseTemperature() {
+  command(CCT_TEMPERATURE_DOWN, 0);
+}
+
+void CctPacketFormatter::increaseBrightness() {
+  command(CCT_BRIGHTNESS_UP, 0);
+}
+
+void CctPacketFormatter::decreaseBrightness() {
+  command(CCT_BRIGHTNESS_DOWN, 0);
+}
+
+uint8_t CctPacketFormatter::getCctStatusButton(uint8_t groupId, MiLightStatus status) {
+  uint8_t button = 0;
+  
+  if (status == ON) {
+    switch(groupId) {
+      case 1:
+        button = CCT_GROUP_1_ON;
+        break;
+      case 2:
+        button = CCT_GROUP_2_ON;
+        break;
+      case 3:
+        button = CCT_GROUP_3_ON;
+        break;
+      case 4:
+        button = CCT_GROUP_4_ON;
+        break;
+    }
+  } else {
+    switch(groupId) {
+      case 1:
+        button = CCT_GROUP_1_OFF;
+        break;
+      case 2:
+        button = CCT_GROUP_2_OFF;
+        break;
+      case 3:
+        button = CCT_GROUP_3_OFF;
+        break;
+      case 4:
+        button = CCT_GROUP_4_OFF;
+        break;
+    }
+  }
+  
+  return button;
+}
+
+void CctPacketFormatter::format(uint8_t const* packet, char* buffer) {
+  PacketFormatter::formatV1Packet(packet, buffer);
+}

+ 32 - 0
lib/MiLight/CctPacketFormatter.h

@@ -0,0 +1,32 @@
+#include <PacketFormatter.h>
+
+#ifndef _CCT_PACKET_FORMATTER_H
+#define _CCT_PACKET_FORMATTER_H 
+
+#define CCT_COMMAND_INDEX 4
+#define CCT_INTERVALS 10
+
+class CctPacketFormatter : public PacketFormatter {
+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 format(uint8_t const* packet, char* buffer);
+  virtual void initializePacket(uint8_t* packet);
+  
+  static uint8_t getCctStatusButton(uint8_t groupId, MiLightStatus status);
+};
+
+#endif

+ 25 - 0
lib/MiLight/MiLightButtons.h

@@ -1,6 +1,31 @@
 #ifndef _MILIGHT_BUTTONS
 #define _MILIGHT_BUTTONS 
 
+enum MiLightRadioType {
+  UNKNOWN = 0,
+  RGBW  = 0xB8,
+  CCT   = 0x5A,
+  RGB_CCT = 0x20,
+  RGB = 0xA4
+};
+
+enum MiLightRgbCctCommand {
+  RGB_CCT_ON = 0x01,
+  RGB_CCT_OFF = 0x01,
+  RGB_CCT_MODE_SPEED_UP = 0x01,
+  RGB_CCT_MODE_SPEED_DOWN = 0x01,
+  RGB_CCT_COLOR = 0x02,
+  RGB_CCT_KELVIN = 0x03,
+  RGB_CCT_BRIGHTNESS = 0x04,
+  RGB_CCT_SATURATION = 0x04,
+  RGB_CCT_MODE = 0x05,
+};
+
+enum MiLightStatus { 
+  ON = 0, 
+  OFF = 1 
+};
+
 enum MiLightRgbwButton {
   RGBW_ALL_ON            = 0x01,
   RGBW_ALL_OFF           = 0x02,

+ 106 - 243
lib/MiLight/MiLightClient.cpp

@@ -1,318 +1,181 @@
 #include <MiLightClient.h>
 #include <MiLightRadioConfig.h>
+#include <Arduino.h>
 
-MiLightRadio* MiLightClient::getRadio(const MiLightRadioType type) {
-  MiLightRadioStack* stack = NULL;
+MiLightRadio* MiLightClient::switchRadio(const MiLightRadioType type) {
+  RadioStack* stack = NULL;
   
-  if (type == RGBW) {
-    stack = rgbwRadio;
-  } else if (type == CCT) {
-    stack = cctRadio;
-  } else if (type == RGB_CCT) {
-    stack = rgbCctRadio;
+  for (int i = 0; i < numRadios; i++) {
+    if (radios[i]->config.type == type) {
+      stack = radios[i];
+      break;
+    }
   }
   
   if (stack != NULL) {
     MiLightRadio *radio = stack->getRadio();
     
-    if (currentRadio != stack->type) {
+    if (currentRadio->config.type != stack->config.type) {
       radio->configure();
     }
     
-    currentRadio = stack->type;
+    currentRadio = stack;
+    formatter = stack->config.packetFormatter;
     return radio;
+  } else {
+    Serial.print("MiLightClient - tried to get radio for unknown type: ");
+    Serial.println(type);
   }
   
   return NULL;
 }
 
-void MiLightClient::setResendCount(const unsigned int resendCount) {
-  this->resendCount = resendCount;
+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);
+  }
 }
 
-uint8_t MiLightClient::nextSequenceNum() {
-  return sequenceNum++;
+void MiLightClient::setResendCount(const unsigned int resendCount) {
+  this->resendCount = resendCount;
 }
 
-bool MiLightClient::available(const MiLightRadioType radioType) {
-  MiLightRadio* radio = getRadio(radioType);
-  
-  if (radio == NULL) {
+bool MiLightClient::available() {
+  if (currentRadio == NULL) {
     return false;
   }
   
-  return radio->available();
+  return currentRadio->getRadio()->available();
 }
 
-void MiLightClient::read(const MiLightRadioType radioType, uint8_t packet[]) {
-  MiLightRadio* radio = getRadio(radioType);
-  
-  if (radio == NULL) {
+void MiLightClient::read(uint8_t packet[]) {
+  if (currentRadio == NULL) {
     return;
   }
   
   size_t length;
-  radio->read(packet, length);
+  currentRadio->getRadio()->read(packet, length);
 }
 
-void MiLightClient::write(const MiLightRadioType radioType, 
-  uint8_t packet[]) {
-    
-  MiLightRadio* radio = getRadio(radioType);
-  
-  if (radio == NULL) {
+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++) {
+    printf("%02X", packet[i]);
+  }
+  printf("\n");
+#endif
+  
   for (int i = 0; i < this->resendCount; i++) {
-    radio->write(packet, MILIGHT_PACKET_LENGTH);
+    currentRadio->getRadio()->write(packet, currentRadio->config.getPacketLength());
   }
 }
+    
+void MiLightClient::updateColorRaw(const uint8_t color) {
+  formatter->updateColorRaw(color);
+  flushPacket();
+}
 
-void MiLightClient::writeRgbw(
-  const uint16_t deviceId,
-  const uint8_t color,
-  const uint8_t brightness,
-  const uint8_t groupId,
-  const uint8_t button) {
-  
-  uint8_t packet[MilightRgbwConfig.packetLength];
-  size_t packetPtr = 0;
-  
-  packet[packetPtr++] = RGBW;
-  packet[packetPtr++] = deviceId >> 8;
-  packet[packetPtr++] = deviceId & 0xFF;
-  packet[packetPtr++] = color;
-  packet[packetPtr++] = (brightness << 3) | (groupId & 0x07);
-  packet[packetPtr++] = button;
-  packet[packetPtr++] = nextSequenceNum();
-  
-  write(RGBW, packet);
+void MiLightClient::updateHue(const uint16_t hue) {
+  formatter->updateHue(hue);
+  flushPacket();
 }
 
-void MiLightClient::writeCct(const uint16_t deviceId,
-  const uint8_t groupId,
-  const uint8_t button) {
-    
-  uint8_t packet[MilightRgbwConfig.packetLength];
-  uint8_t sequenceNum = nextSequenceNum();
-  size_t packetPtr = 0;
-  
-  packet[packetPtr++] = CCT;
-  packet[packetPtr++] = deviceId >> 8;
-  packet[packetPtr++] = deviceId & 0xFF;
-  packet[packetPtr++] = groupId;
-  packet[packetPtr++] = button;
-  packet[packetPtr++] = sequenceNum;
-  packet[packetPtr++] = sequenceNum;
-  
-  write(CCT, packet);
+void MiLightClient::updateBrightness(const uint8_t brightness) {
+  formatter->updateBrightness(brightness);
+  flushPacket();
 }
     
-void MiLightClient::updateColorRaw(const uint16_t deviceId, const uint8_t groupId, const uint16_t color) {
-  writeRgbw(deviceId, color, 0, groupId, RGBW_COLOR);
+void MiLightClient::updateStatus(MiLightStatus status, uint8_t groupId) {
+  formatter->updateStatus(status, groupId);
+  flushPacket();
 }
 
-void MiLightClient::updateHue(const uint16_t deviceId, const uint8_t groupId, const uint16_t hue) {
-  // Map color as a Hue value in [0, 359] to [0, 255]. The protocol also has
-  // 0 being roughly magenta (#FF00FF)
-  const int16_t remappedColor = (hue + 40) % 360;
-  const uint8_t adjustedColor = round(remappedColor * (255 / 360.0));
-  
-  writeRgbw(deviceId, adjustedColor, 0, groupId, RGBW_COLOR);
+void MiLightClient::updateStatus(MiLightStatus status) {
+  formatter->updateStatus(status);
+  flushPacket();
 }
 
-void MiLightClient::updateBrightness(const uint16_t deviceId, const uint8_t groupId, const uint8_t brightness) {
-  // Expect an input value in [0, 100]. Map it down to [0, 25].
-  const uint8_t adjustedBrightness = round(brightness * (25 / 100.0));
-  
-  // The actual protocol uses a bizarre range where min is 16, max is 23:
-  // [16, 15, ..., 0, 31, ..., 23]
-  const uint8_t packetBrightnessValue = (
-    ((31 - adjustedBrightness) + 17) % 32
-  );
-  
-  writeRgbw(deviceId, 0, packetBrightnessValue, groupId, RGBW_BRIGHTNESS);
+void MiLightClient::updateSaturation(const uint8_t value) {
+  formatter->updateSaturation(value);
+  flushPacket();
 }
 
-void MiLightClient::updateStatus(const MiLightRadioType type, const uint16_t deviceId, const uint8_t groupId, MiLightStatus status) {
-  if (type == RGBW) {
-    uint8_t button = RGBW_GROUP_1_ON + ((groupId - 1)*2) + status;
-    writeRgbw(deviceId, 0, 0, groupId, button);
-  } else {
-    writeCct(deviceId, groupId, getCctStatusButton(groupId, status));
-  }
+void MiLightClient::updateColorWhite() {
+  formatter->updateColorWhite();
+  flushPacket();
 }
 
-void MiLightClient::updateColorWhite(const uint16_t deviceId, const uint8_t groupId) {
-  uint8_t button = RGBW_GROUP_1_MAX_LEVEL + ((groupId - 1)*2);
-  pressButton(RGBW, deviceId, groupId, button);
+void MiLightClient::pair() {
+  formatter->pair();
+  flushPacket();
 }
 
-void MiLightClient::pair(const MiLightRadioType type, const uint16_t deviceId, const uint8_t groupId) {
-  updateStatus(type, deviceId, groupId, ON);
-}
-
-void MiLightClient::unpair(const MiLightRadioType type, const uint16_t deviceId, const uint8_t groupId) {
-  if (type == RGBW) {
-    updateStatus(RGBW, deviceId, groupId, ON);
-    delay(1);
-    updateColorWhite(deviceId, groupId);
-  } else if (type == CCT) {
-    for (int i = 0; i < 5; i++) {
-      updateStatus(CCT, deviceId, groupId, ON);
-      delay(1);
-    }
-  }
+void MiLightClient::unpair() {
+  formatter->unpair();
+  flushPacket();
 }
     
-void MiLightClient::pressButton(const MiLightRadioType type, const uint16_t deviceId, const uint8_t groupId, const uint8_t button) {
-  if (type == RGBW) {
-    writeRgbw(deviceId, 0, 0, groupId, button);
-  } else if (type == CCT) {
-    writeCct(deviceId, groupId, button);
-  }
+void MiLightClient::increaseBrightness() {
+  formatter->increaseBrightness();
+  flushPacket();
 }
 
-void MiLightClient::allOn(const MiLightRadioType type, const uint16_t deviceId) {
-  if (type == RGBW) {
-    writeRgbw(deviceId, 0, 0, 0, RGBW_ALL_ON);
-  } else if (type == CCT) {
-    writeCct(deviceId, 0, CCT_ALL_ON);
-  }
+void MiLightClient::decreaseBrightness() {
+  formatter->decreaseBrightness();
+  flushPacket();
 }
 
-void MiLightClient::allOff(const MiLightRadioType type, const uint16_t deviceId) {
-  if (type == RGBW) {
-    writeRgbw(deviceId, 0, 0, 0, RGBW_ALL_OFF);
-  } else if (type == CCT) {
-    writeCct(deviceId, 0, CCT_ALL_OFF);
-  }
+void MiLightClient::increaseTemperature() {
+  formatter->increaseTemperature();
+  flushPacket();
 }
 
-void MiLightClient::increaseCctBrightness(const uint16_t deviceId, const uint8_t groupId) {
-  writeCct(deviceId, groupId, CCT_BRIGHTNESS_UP);
+void MiLightClient::decreaseTemperature() {
+  formatter->decreaseTemperature();
+  flushPacket();
 }
 
-void MiLightClient::decreaseCctBrightness(const uint16_t deviceId, const uint8_t groupId) {
-  writeCct(deviceId, groupId, CCT_BRIGHTNESS_DOWN);
+void MiLightClient::updateTemperature(const uint8_t temperature) {
+  formatter->updateTemperature(temperature);
+  flushPacket();
 }
 
-void MiLightClient::updateCctBrightness(const uint16_t deviceId, const uint8_t groupId, const uint8_t brightness) {
-  for (int i = 0; i < MILIGHT_CCT_INTERVALS; i++) {
-    decreaseCctBrightness(deviceId, groupId);
-  }
-  for (int i = 0; i < brightness/10; i++) {
-    increaseCctBrightness(deviceId, groupId);
-  }
+void MiLightClient::command(uint8_t command, uint8_t arg) {
+  formatter->command(command, arg);
+  flushPacket();
 }
 
-void MiLightClient::increaseTemperature(const uint16_t deviceId, const uint8_t groupId) {
-  writeCct(deviceId, groupId, CCT_TEMPERATURE_UP);
+void MiLightClient::formatPacket(uint8_t* packet, char* buffer) {
+  formatter->format(packet, buffer);
 }
-
-void MiLightClient::decreaseTemperature(const uint16_t deviceId, const uint8_t groupId) {
-  writeCct(deviceId, groupId, CCT_TEMPERATURE_DOWN);
-}
-
-void MiLightClient::updateTemperature(const uint16_t deviceId, const uint8_t groupId, const uint8_t temperature) {
-  for (int i = 0; i < MILIGHT_CCT_INTERVALS; i++) {
-    decreaseTemperature(deviceId, groupId);
-  }
-  for (int i = 0; i < temperature; i++) {
-    increaseTemperature(deviceId, groupId);
-  }
-}
-
-uint8_t MiLightClient::getCctStatusButton(uint8_t groupId, MiLightStatus status) {
-  uint8_t button = 0;
+    
+void MiLightClient::flushPacket() {
+  PacketStream& stream = formatter->buildPackets();
+  const size_t prevNumRepeats = this->resendCount;
   
-  if (status == ON) {
-    switch(groupId) {
-      case 1:
-        button = CCT_GROUP_1_ON;
-        break;
-      case 2:
-        button = CCT_GROUP_2_ON;
-        break;
-      case 3:
-        button = CCT_GROUP_3_ON;
-        break;
-      case 4:
-        button = CCT_GROUP_4_ON;
-        break;
-    }
-  } else {
-    switch(groupId) {
-      case 1:
-        button = CCT_GROUP_1_OFF;
-        break;
-      case 2:
-        button = CCT_GROUP_2_OFF;
-        break;
-      case 3:
-        button = CCT_GROUP_3_OFF;
-        break;
-      case 4:
-        button = CCT_GROUP_4_OFF;
-        break;
-    }
+  // When sending multiple packets, normalize the number of repeats
+  if (stream.numPackets > 1) {
+    setResendCount(MILIGHT_DEFAULT_RESEND_COUNT);
   }
   
-  return button;
-}
-
-MiLightRadioType MiLightClient::getRadioType(const String& typeName) {
-  if (typeName.equalsIgnoreCase("rgbw")) {
-    return RGBW;
-  } else if (typeName.equalsIgnoreCase("cct")) {
-    return CCT;
-  } else if (typeName.equalsIgnoreCase("rgb_cct")) {
-    return RGB_CCT;
-  } else {
-    return UNKNOWN;
-  }
-}
-
-const MiLightRadioConfig& MiLightClient::getRadioConfig(const String& typeName) {
-  switch (getRadioType(typeName)) {
-    case RGBW:
-      return MilightRgbwConfig;
-    case CCT:
-      return MilightCctConfig;
-    case RGB_CCT:
-      return MilightRgbCctConfig;
-    default:
-      Serial.print("Unknown radio type: ");
-      Serial.println(typeName);
-      return MilightRgbwConfig;
-  }
-}
+  while (stream.hasNext()) {
+    write(stream.next());
     
-void MiLightClient::formatPacket(MiLightRadioConfig& config, uint8_t* packet, char* buffer) {
-  if (config.type == RGBW || config.type == CCT) {
-    String format = String("Request type  : %02X\n") 
-      + "Device ID     : %02X%02X\n"
-      + "b1            : %02X\n"
-      + "b2            : %02X\n"
-      + "b3            : %02X\n"
-      + "Sequence Num. : %02X";
-      
-    sprintf(
-      buffer,
-      format.c_str(),
-      packet[0],
-      packet[1], packet[2],
-      packet[3],
-      packet[4],
-      packet[5],
-      packet[6]
-    );
-  } else {
-    for (int i = 0; i < config.packetLength; i++) {
-      sprintf(buffer, "%02X ", packet[i]);
-      buffer += 3;
+    if (stream.hasNext()) {
+      delay(10);
     }
-    sprintf(buffer, "\n\n");
   }
-}
+  
+  setResendCount(prevNumRepeats);
+  formatter->reset();
+}

+ 47 - 87
lib/MiLight/MiLightClient.h

@@ -3,125 +3,85 @@
 #include <PL1167_nRF24.h>
 #include <RF24.h>
 #include <MiLightButtons.h>
+#include <RadioStack.h>
 
 #ifndef _MILIGHTCLIENT_H
 #define _MILIGHTCLIENT_H
 
-#define MILIGHT_PACKET_LENGTH 7
-#define MILIGHT_CCT_INTERVALS 10
-#define MILIGHT_DEFAULT_RESEND_COUNT 10
-
-enum MiLightStatus { ON = 0, OFF = 1 };
+// #define DEBUG_PRINTF
 
-class MiLightRadioStack {
-public:
-  MiLightRadioStack(RF24& rf, const MiLightRadioConfig& config) 
-    : type(config.type)
-  {
-    nrf = new PL1167_nRF24(rf);
-    radio = new MiLightRadio(*nrf, config);
-  }
-  
-  ~MiLightRadioStack() {
-    delete radio;
-    delete nrf;
-  }
-  
-  inline MiLightRadio* getRadio() {
-    return this->radio;
-  }
-  
-  const MiLightRadioType& type;
-  
-private:
-  PL1167_nRF24 *nrf;
-  MiLightRadio *radio;
-};
+#define MILIGHT_DEFAULT_RESEND_COUNT 10
 
 class MiLightClient {
   public:
     MiLightClient(uint8_t cePin, uint8_t csnPin)
-    : sequenceNum(0),
-      rf(RF24(cePin, csnPin)),
-      resendCount(MILIGHT_DEFAULT_RESEND_COUNT)
+      : rf(RF24(cePin, csnPin)),
+      resendCount(MILIGHT_DEFAULT_RESEND_COUNT),
+      currentRadio(NULL),
+      numRadios(MiLightRadioConfig::NUM_CONFIGS)
     {
-      rgbwRadio = new MiLightRadioStack(rf, MilightRgbwConfig);
-      cctRadio = new MiLightRadioStack(rf, MilightCctConfig);
-      rgbCctRadio = new MiLightRadioStack(rf, MilightRgbCctConfig);
+      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 rgbwRadio;
-      delete cctRadio;
-      delete rgbCctRadio;
+      delete[] radios;
     }
     
     void begin() {
-      rgbwRadio->getRadio()->begin();
-      cctRadio->getRadio()->begin();
-      rgbCctRadio->getRadio()->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(const MiLightRadioType radioType);
-    void read(const MiLightRadioType radioType, uint8_t packet[]);
-    void write(const MiLightRadioType radioType, uint8_t packet[]);
-    
-    void writeRgbw(
-      const uint16_t deviceId,
-      const uint8_t color,
-      const uint8_t brightness,
-      const uint8_t groupId,
-      const uint8_t button
-    );
-    
-    void writeCct(
-      const uint16_t deviceId,
-      const uint8_t groupId,
-      const uint8_t button
-    );
+    bool available();
+    void read(uint8_t packet[]);
+    void write(uint8_t packet[]);
     
     // Common methods
-    void updateStatus(const MiLightRadioType type,const uint16_t deviceId, const uint8_t groupId, MiLightStatus status);
-    void pair(const MiLightRadioType type,const uint16_t deviceId, const uint8_t groupId);
-    void unpair(const MiLightRadioType type,const uint16_t deviceId, const uint8_t groupId);
-    void allOn(const MiLightRadioType type,const uint16_t deviceId);
-    void allOff(const MiLightRadioType type,const uint16_t deviceId);
-    void pressButton(const MiLightRadioType type, const uint16_t deviceId, const uint8_t groupId, uint8_t button);
+    void updateStatus(MiLightStatus status);
+    void updateStatus(MiLightStatus status, uint8_t groupId);
+    void pair();
+    void unpair();
+    void command(uint8_t command, uint8_t arg);
     
     // RGBW methods
-    void updateHue(const uint16_t deviceId, const uint8_t groupId, const uint16_t hue);
-    void updateBrightness(const uint16_t deviceId, const uint8_t groupId, const uint8_t brightness);
-    void updateColorWhite(const uint16_t deviceId, const uint8_t groupId);
-    void updateColorRaw(const uint16_t deviceId, const uint8_t groupId, const uint16_t color);
+    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 uint16_t deviceId, const uint8_t groupId, const uint8_t colorTemperature);
-    void decreaseTemperature(const uint16_t deviceId, const uint8_t groupId);
-    void increaseTemperature(const uint16_t deviceId, const uint8_t groupId);
-    void updateCctBrightness(const uint16_t deviceId, const uint8_t groupId, const uint8_t brightness);
-    void decreaseCctBrightness(const uint16_t deviceId, const uint8_t groupId);
-    void increaseCctBrightness(const uint16_t deviceId, const uint8_t groupId);
+    void updateTemperature(const uint8_t colorTemperature);
+    void decreaseTemperature();
+    void increaseTemperature();
+    void increaseBrightness();
+    void decreaseBrightness();
     
-    MiLightRadio* getRadio(const MiLightRadioType type);
+    void updateSaturation(const uint8_t saturation);
     
-    static uint8_t getCctStatusButton(uint8_t groupId, MiLightStatus status);
-    static MiLightRadioType getRadioType(const String& typeName);
-    static const MiLightRadioConfig& getRadioConfig(const String& typeName);
+    void formatPacket(uint8_t* packet, char* buffer);
     
-    void formatPacket(MiLightRadioConfig& config, uint8_t* packet, char* buffer);
     
-  private:
+  protected:
     RF24 rf;
-    MiLightRadioStack* rgbwRadio;
-    MiLightRadioStack* cctRadio;
-    MiLightRadioStack* rgbCctRadio;
-    MiLightRadioType currentRadio;
+    RadioStack** radios;
+    RadioStack* currentRadio;
+    PacketFormatter* formatter;
+    const size_t numRadios;
     
-    uint8_t sequenceNum;
-    uint8_t nextSequenceNum();
     unsigned int resendCount;
+    
+    MiLightRadio* switchRadio(const MiLightRadioType type);
+    void flushPacket();
 };
 
 #endif

+ 5 - 5
lib/MiLight/MiLightRadio.cpp

@@ -7,7 +7,7 @@
 
 #include "MiLightRadio.h"
 
-#define PACKET_ID(packet) ( ((packet[1] & 0xF0)<<24) | (packet[2]<<16) | (packet[3]<<8) | (packet[7]) )
+#define PACKET_ID(packet, packet_length) ( (packet[1] << 8) | packet[packet_length - 1] )
 
 MiLightRadio::MiLightRadio(AbstractPL1167 &pl1167, const MiLightRadioConfig& config)
   : _pl1167(pl1167), config(config) {
@@ -53,7 +53,7 @@ int MiLightRadio::configure() {
   }
 
   // +1 to be able to buffer the length 
-  retval = _pl1167.setMaxPacketLength(config.packetLength + 1);
+  retval = _pl1167.setMaxPacketLength(config.getPacketLength() + 1);
   if (retval < 0) {
     return retval;
   }
@@ -83,10 +83,10 @@ bool MiLightRadio::available() {
     if (packet_length == 0 || packet_length != _packet[0] + 1U) {
       return false;
     }
+    uint32_t packet_id = PACKET_ID(_packet, packet_length);
 #ifdef DEBUG_PRINTF
-  printf("2");
+  printf("Packet id: %d\n", packet_id);
 #endif
-    uint32_t packet_id = PACKET_ID(_packet);
     if (packet_id == _prev_packet_id) {
       _dupes_received++;
     } else {
@@ -143,7 +143,7 @@ int MiLightRadio::write(uint8_t frame[], size_t frame_length)
 
 int MiLightRadio::resend()
 {
-  for (size_t i = 0; i < config.numChannels; i++) {
+  for (size_t i = 0; i < MiLightRadioConfig::NUM_CHANNELS; i++) {
     _pl1167.writeFIFO(_out_packet, _out_packet[0] + 1);
     _pl1167.transmit(config.channels[i]);
   }

+ 1 - 1
lib/MiLight/MiLightRadio.h

@@ -16,7 +16,7 @@
 #include "AbstractPL1167.h"
 #include <MiLightRadioConfig.h>
 
-#define DEBUG_PRINTF
+// #define DEBUG_PRINTF
 
 #ifndef MILIGHTRADIO_H_
 #define MILIGHTRADIO_H_

+ 26 - 0
lib/MiLight/MiLightRadioConfig.cpp

@@ -0,0 +1,26 @@
+#include <MiLightRadioConfig.h>
+  
+const MiLightRadioConfig* MiLightRadioConfig::ALL_CONFIGS[] = {
+  &MilightRgbwConfig,
+  &MilightCctConfig,
+  &MilightRgbCctConfig,
+  &MilightRgbConfig
+};
+
+MiLightRadioConfig* MiLightRadioConfig::fromString(const String& s) {
+  if (s.equalsIgnoreCase("rgbw")) {
+    return &MilightRgbwConfig;
+  } else if (s.equalsIgnoreCase("cct")) {
+    return &MilightCctConfig;
+  } else if (s.equalsIgnoreCase("rgb_cct")) {
+    return &MilightRgbCctConfig;
+  } else if (s.equalsIgnoreCase("rgb")) {
+    return &MilightRgbConfig;
+  }
+  
+  return NULL;
+}
+
+size_t MiLightRadioConfig::getPacketLength() const {
+  return packetFormatter->getPacketLength();
+}

+ 38 - 25
lib/MiLight/MiLightRadioConfig.h

@@ -1,52 +1,65 @@
 #include <Arduino.h>
+#include <PacketFormatter.h>
+#include <RgbCctPacketFormatter.h>
+#include <RgbwPacketFormatter.h>
+#include <CctPacketFormatter.h>
+#include <RgbPacketFormatter.h>
+#include <MiLightButtons.h>
 
 #ifndef _MILIGHT_RADIO_CONFIG
 #define _MILIGHT_RADIO_CONFIG 
 
-enum MiLightRadioType {
-  UNKNOWN = 0,
-  RGBW  = 0xB8,
-  CCT   = 0x5A,
-  RGB_CCT = 0x99
-};
-
 class MiLightRadioConfig {
 public:
+  static const size_t NUM_CHANNELS = 3;
+  
   MiLightRadioConfig(const uint16_t syncword0,
   const uint16_t syncword3,
-  const size_t packetLength,
-  const uint8_t* channels,
-  const size_t numChannels,
-  const MiLightRadioType type) 
+  PacketFormatter* packetFormatter,
+  const MiLightRadioType type,
+  const char* name,
+  const uint8_t channel0,
+  const uint8_t channel1,
+  const uint8_t channel2) 
     : syncword0(syncword0),
       syncword3(syncword3),
-      packetLength(packetLength),
-      channels(channels),
-      numChannels(numChannels),
-      type(type)
-  {}
+      packetFormatter(packetFormatter),
+      type(type),
+      name(name)
+  {
+    channels[0] = channel0;
+    channels[1] = channel1;
+    channels[2] = channel2;
+  }
     
   const uint16_t syncword0;
   const uint16_t syncword3;
-  const size_t packetLength;
-  const uint8_t* channels;
-  const size_t numChannels;
+  uint8_t channels[3];
+  PacketFormatter* packetFormatter;
   const MiLightRadioType type;
+  const char* name;
+  
+  static const size_t NUM_CONFIGS = 4;
+  static const MiLightRadioConfig* ALL_CONFIGS[NUM_CONFIGS];
+  
+  static MiLightRadioConfig* fromString(const String& s);
+  size_t getPacketLength() const;
 };
 
-const uint8_t RGBW_CHANNELS[] = {9, 40, 71};
 static MiLightRadioConfig MilightRgbwConfig(
-  0x147A, 0x258B, 7, RGBW_CHANNELS, 3, RGBW
+  0x147A, 0x258B, new RgbwPacketFormatter(), RGBW, "rgbw", 9, 40, 71
 );
 
-const uint8_t CCT_CHANNELS[] = {4, 39, 74};
 static MiLightRadioConfig MilightCctConfig(
-  0x050A, 0x55AA, 7, CCT_CHANNELS, 3, CCT
+  0x050A, 0x55AA, new CctPacketFormatter(), CCT, "cct", 4, 39, 74
 );
 
-const uint8_t RGBCCT_CHANNELS[] = {70, 39, 8};
 static MiLightRadioConfig MilightRgbCctConfig(
-  0x7236, 0x1809, 9, RGBCCT_CHANNELS, 3, RGB_CCT
+  0x7236, 0x1809, new RgbCctPacketFormatter(), RGB_CCT, "rgb_cct", 8, 39, 70
+);
+
+static MiLightRadioConfig MilightRgbConfig(
+  0x9AAB, 0xBCCD, new RgbPacketFormatter(), RGB, "rgb", 3, 38, 73
 );
 
 #endif

+ 0 - 161
lib/MiLight/MiLightUdpServer.cpp

@@ -1,161 +0,0 @@
-#include <MiLightUdpServer.h>
-
-MiLightUdpServer::MiLightUdpServer(MiLightClient*& client, uint16_t port, uint16_t deviceId)
-  : client(client), 
-    port(port),
-    deviceId(deviceId),
-    lastGroup(0)
-{ }
-
-MiLightUdpServer::~MiLightUdpServer() {
-  stop();
-}
-
-void MiLightUdpServer::begin() {
-  socket.begin(this->port);
-}
-
-void MiLightUdpServer::stop() {
-  socket.stop();
-}
-
-void MiLightUdpServer::handleClient() {
-  const size_t packetSize = socket.parsePacket();
-  
-  if (packetSize) {
-    if (packetSize >= 2 && packetSize <= 3) {
-      socket.read(packetBuffer, packetSize);
-      
-#ifdef MILIGHT_UDP_DEBUG
-      Serial.print("Handling command: ");
-      Serial.print(String(packetBuffer[0], HEX));
-      Serial.print(" ");
-      Serial.println(String(packetBuffer[1], HEX));
-#endif
-      
-      handleCommand(packetBuffer[0], packetBuffer[1]);
-    } else {
-      Serial.print("Error, unexpected packet length (should always be 2-3, was: ");
-      Serial.println(packetSize);
-    }
-  }
-}
-
-void MiLightUdpServer::handleCommand(uint8_t command, uint8_t commandArg) {
-  if (command >= UDP_RGBW_GROUP_1_ON && command <= UDP_RGBW_GROUP_4_OFF) {
-    const MiLightStatus status = (command % 2) == 1 ? ON : OFF;
-    const uint8_t groupId = (command - UDP_RGBW_GROUP_1_ON + 2)/2;
-    
-    client->updateStatus(RGBW, deviceId, groupId, status);
-    
-    this->lastGroup = groupId;
-  } else if (command >= UDP_RGBW_GROUP_ALL_WHITE && command <= UDP_RGBW_GROUP_4_WHITE) {
-    const uint8_t groupId = (command - UDP_RGBW_GROUP_ALL_WHITE)/2;
-    client->updateColorWhite(deviceId, groupId);
-    this->lastGroup = groupId;
-  } else if (uint8_t cctGroup = cctCommandIdToGroup(command)) {
-    client->updateStatus(
-      CCT,
-      deviceId,
-      cctGroup,
-      cctCommandToStatus(command)
-    );
-    this->lastGroup = cctGroup;
-  }
-  else {
-    switch (command) {
-      case UDP_RGBW_ALL_ON:
-        client->allOn(RGBW, deviceId);
-        break;
-      
-      case UDP_RGBW_ALL_OFF:
-        client->allOff(RGBW, deviceId);
-        break;
-      
-      case UDP_RGBW_COLOR:
-        // UDP color is shifted by 0xC8 from 2.4 GHz color, and the spectrum is
-        // flipped (R->B->G instead of R->G->B)
-        client->updateColorRaw(deviceId, this->lastGroup, 0xFF-(commandArg + 0x35));
-        break;
-        
-      case UDP_RGBW_DISCO_MODE:
-        pressButton(this->lastGroup, RGBW_DISCO_MODE);
-        break;
-        
-      case UDP_RGBW_SPEED_DOWN:
-        pressButton(this->lastGroup, RGBW_SPEED_DOWN);
-        break;
-        
-      case UDP_RGBW_SPEED_UP:
-        pressButton(this->lastGroup, RGBW_SPEED_UP);
-        break;
-        
-      case UDP_RGBW_BRIGHTNESS:
-        // map [2, 27] --> [0, 100]
-        client->updateBrightness(
-          deviceId, 
-          this->lastGroup, 
-          round(((commandArg - 2) / 25.0)*100)
-        );
-        break;
-        
-      case UDP_CCT_BRIGHTNESS_DOWN:
-        client->decreaseCctBrightness(deviceId, this->lastGroup);
-        break;
-        
-      case UDP_CCT_BRIGHTNESS_UP:
-        client->increaseCctBrightness(deviceId, this->lastGroup);
-        break;
-        
-      case UDP_CCT_TEMPERATURE_DOWN:
-        client->decreaseTemperature(deviceId, this->lastGroup);
-        break;
-        
-      case UDP_CCT_TEMPERATURE_UP:
-        client->increaseTemperature(deviceId, this->lastGroup);
-        break;
-        
-      default:
-        Serial.print("MiLightUdpServer - Unhandled command: ");
-        Serial.println(command);
-    }
-  }
-}
-
-void MiLightUdpServer::pressButton(uint8_t group, uint8_t button) {
-  client->writeRgbw(deviceId, 0, 0, group, button);
-}  
-
-uint8_t MiLightUdpServer::cctCommandIdToGroup(uint8_t command) {
-  switch (command) {
-    case UDP_CCT_GROUP_1_ON:
-    case UDP_CCT_GROUP_1_OFF:
-      return 1;
-    case UDP_CCT_GROUP_2_ON:
-    case UDP_CCT_GROUP_2_OFF:
-      return 2;
-    case UDP_CCT_GROUP_3_ON:
-    case UDP_CCT_GROUP_3_OFF:
-      return 3;
-    case UDP_CCT_GROUP_4_ON:
-    case UDP_CCT_GROUP_4_OFF:
-      return 4;
-  }
-  
-  return 0;
-}  
-  
-MiLightStatus MiLightUdpServer::cctCommandToStatus(uint8_t command) {
-  switch (command) {
-    case UDP_CCT_GROUP_1_ON:
-    case UDP_CCT_GROUP_2_ON:
-    case UDP_CCT_GROUP_3_ON:
-    case UDP_CCT_GROUP_4_ON:
-      return ON;
-    case UDP_CCT_GROUP_1_OFF:
-    case UDP_CCT_GROUP_2_OFF:
-    case UDP_CCT_GROUP_3_OFF:
-    case UDP_CCT_GROUP_4_OFF:
-      return OFF;
-  }
-}

+ 1 - 1
lib/MiLight/PL1167_nRF24.h

@@ -12,7 +12,7 @@
 #include "AbstractPL1167.h"
 #include "RF24.h"
 
-#define DEBUG_PRINTF
+// #define DEBUG_PRINTF
 
 #ifndef PL1167_NRF24_H_
 #define PL1167_NRF24_H_

+ 137 - 0
lib/MiLight/PacketFormatter.cpp

@@ -0,0 +1,137 @@
+#include <PacketFormatter.h>
+
+PacketStream::PacketStream()
+    : packetStream(NULL),
+      numPackets(0),
+      packetLength(0),
+      currentPacket(0)
+  { }
+  
+bool PacketStream::hasNext() {
+  return currentPacket < numPackets;
+}
+
+uint8_t* PacketStream::next() {
+  uint8_t* packet = packetStream + (currentPacket * packetLength);
+  currentPacket++;
+  return packet;
+}
+
+PacketFormatter::PacketFormatter(const size_t packetLength, const size_t maxPackets)
+  : packetLength(packetLength),
+    packetBuffer(new uint8_t[packetLength * maxPackets]),
+    numPackets(0),
+    currentPacket(NULL)
+{ 
+  packetStream.packetLength = packetLength;
+  packetStream.packetStream = packetBuffer;
+}
+  
+void PacketFormatter::finalizePacket(uint8_t* packet) { }
+  
+void PacketFormatter::updateStatus(MiLightStatus status) { 
+  updateStatus(status, groupId);
+}
+
+void PacketFormatter::updateStatus(MiLightStatus status, uint8_t groupId) { }
+void PacketFormatter::updateBrightness(uint8_t value) { }
+void PacketFormatter::updateMode(uint8_t value) { }
+void PacketFormatter::modeSpeedDown() { }
+void PacketFormatter::modeSpeedUp() { }
+void PacketFormatter::command(uint8_t command, uint8_t arg) { }
+
+void PacketFormatter::updateHue(uint16_t value) { }
+void PacketFormatter::updateColorRaw(uint8_t value) { }
+void PacketFormatter::updateColorWhite() { }
+
+void PacketFormatter::increaseTemperature() { }
+void PacketFormatter::decreaseTemperature() { }
+void PacketFormatter::increaseBrightness() { }
+void PacketFormatter::decreaseBrightness() { }
+
+void PacketFormatter::updateTemperature(uint8_t value) { }
+void PacketFormatter::updateSaturation(uint8_t value) { }
+  
+void PacketFormatter::pair() { 
+  for (size_t i = 0; i < 5; i++) {
+    updateStatus(ON);
+  }
+}
+
+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)();
+  }
+}
+
+void PacketFormatter::prepare(uint16_t deviceId, uint8_t groupId) {
+  this->deviceId = deviceId;
+  this->groupId = groupId;
+  reset();
+}
+
+void PacketFormatter::reset() {
+  this->numPackets = 0;
+  this->currentPacket = currentPacket;
+}
+
+void PacketFormatter::pushPacket() {
+  if (numPackets > 0) {
+    finalizePacket(currentPacket);
+  }
+  
+  currentPacket = packetBuffer + (numPackets * packetLength);
+  numPackets++;
+  initializePacket(currentPacket);
+}
+
+void PacketFormatter::format(uint8_t const* packet, char* buffer) {
+  for (int i = 0; i < packetLength; i++) {
+    sprintf(buffer, "%02X ", packet[i]);
+    buffer += 3;
+  }
+  sprintf(buffer, "\n\n");
+}
+
+void PacketFormatter::formatV1Packet(uint8_t const* packet, char* buffer) {
+  String format = String("Request type  : %02X\n") 
+    + "Device ID     : %02X%02X\n"
+    + "b1            : %02X\n"
+    + "b2            : %02X\n"
+    + "b3            : %02X\n"
+    + "Sequence Num. : %02X";
+    
+  sprintf(
+    buffer,
+    format.c_str(),
+    packet[0],
+    packet[1], packet[2],
+    packet[3],
+    packet[4],
+    packet[5],
+    packet[6]
+  );
+}
+  
+size_t PacketFormatter::getPacketLength() const {
+  return packetLength;
+}

+ 89 - 0
lib/MiLight/PacketFormatter.h

@@ -0,0 +1,89 @@
+#include <Arduino.h>
+#include <inttypes.h>
+#include <functional>
+#include <MiLightButtons.h>
+
+#ifndef _PACKET_FORMATTER_H
+#define _PACKET_FORMATTER_H 
+
+struct PacketStream {
+  PacketStream();
+  
+  uint8_t* next();
+  bool hasNext();
+  
+  uint8_t* packetStream;
+  size_t numPackets;
+  size_t packetLength;
+  size_t currentPacket;
+};
+
+class PacketFormatter {
+public:
+  PacketFormatter(const size_t packetLength, const size_t maxPackets = 1);
+  
+  ~PacketFormatter() {
+    delete this->packetBuffer;
+  }
+  
+  typedef void (PacketFormatter::*StepFunction)();
+  
+  // all
+  void updateStatus(MiLightStatus status);
+  virtual void updateStatus(MiLightStatus status, uint8_t groupId);
+  virtual void updateBrightness(uint8_t value);
+  virtual void command(uint8_t command, uint8_t arg);
+  virtual void updateMode(uint8_t value);
+  virtual void modeSpeedDown();
+  virtual void modeSpeedUp();
+  virtual void pair();
+  virtual void unpair();
+  
+  // rgbw, rgb+cct
+  virtual void updateHue(uint16_t value);
+  virtual void updateColorRaw(uint8_t value);
+  virtual void updateColorWhite();
+  
+  // cct 
+  virtual void increaseTemperature();
+  virtual void decreaseTemperature();
+  virtual void increaseBrightness();
+  virtual void decreaseBrightness();
+  
+  // rgb+cct
+  virtual void updateTemperature(uint8_t value);
+  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:
+  uint8_t* packetBuffer;
+  uint8_t* currentPacket;
+  size_t packetLength;
+  uint16_t deviceId;
+  uint8_t groupId;
+  uint8_t sequenceNum;
+  size_t numPackets;
+  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);
+};
+
+#endif

+ 34 - 0
lib/MiLight/RadioStack.h

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

+ 150 - 0
lib/MiLight/RgbCctPacketFormatter.cpp

@@ -0,0 +1,150 @@
+#include <RgbCctPacketFormatter.h>
+
+#define V2_OFFSET(byte, key, jumpStart) ( \
+  V2_OFFSETS[byte-1][key%4] \
+    + \
+  ((jumpStart > 0 && key >= jumpStart && key <= jumpStart+0x80) ? 0x80 : 0) \
+)
+
+uint8_t const RgbCctPacketFormatter::V2_OFFSETS[][4] = {
+  { 0x45, 0x1F, 0x14, 0x5C },
+  { 0x2B, 0xC9, 0xE3, 0x11 },
+  { 0xEE, 0xDE, 0x0B, 0xAA },
+  { 0xAF, 0x03, 0x1D, 0xF3 },
+  { 0x1A, 0xE2, 0xF0, 0xD1 },
+  { 0x04, 0xD8, 0x71, 0x42 },
+  { 0xAF, 0x04, 0xDD, 0x07 },
+  { 0xE1, 0x93, 0xB8, 0xE4 }
+};
+
+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;
+  packet[packetPtr++] = 0;
+  packet[packetPtr++] = 0;
+  packet[packetPtr++] = sequenceNum++;
+  packet[packetPtr++] = groupId;
+  packet[packetPtr++] = 0;
+}
+
+void RgbCctPacketFormatter::unpair() { 
+  for (size_t i = 0; i < 5; i++) {
+    updateStatus(ON, 0);
+  }
+}
+  
+void RgbCctPacketFormatter::command(uint8_t command, uint8_t arg) {
+  pushPacket();
+  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));
+}
+
+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);
+}
+
+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));
+}
+
+void RgbCctPacketFormatter::updateSaturation(uint8_t value) {
+  uint8_t remapped = value + 0xD;
+  command(RGB_CCT_SATURATION, remapped);
+}
+  
+void RgbCctPacketFormatter::updateColorWhite() {
+  updateTemperature(0);
+}
+  
+void RgbCctPacketFormatter::finalizePacket(uint8_t* packet) {
+  encodeV2Packet(packet);
+}
+
+uint8_t RgbCctPacketFormatter::xorKey(uint8_t key) {
+  // Generate most significant nibble
+  const uint8_t shift = (key & 0x0F) < 0x04 ? 0 : 1;
+  const uint8_t x = (((key & 0xF0) >> 4) + shift + 6) % 8;
+  const uint8_t msn = (((4 + x) ^ 1) & 0x0F) << 4;
+
+  // Generate least significant nibble
+  const uint8_t lsn = ((((key & 0xF) + 4)^2) & 0x0F);
+
+  return ( msn | lsn );
+}
+
+uint8_t RgbCctPacketFormatter::decodeByte(uint8_t byte, uint8_t s1, uint8_t xorKey, uint8_t s2) {
+  uint8_t value = byte - s2;
+  value = value ^ xorKey;
+  value = value - s1;
+  
+  return value;
+}
+
+uint8_t RgbCctPacketFormatter::encodeByte(uint8_t byte, uint8_t s1, uint8_t xorKey, uint8_t s2) {
+  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));
+  }
+}
+
+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, 3, key, V2_OFFSET(8, packet[0], 0));
+}
+
+void RgbCctPacketFormatter::format(uint8_t const* packet, char* buffer) {
+  buffer += sprintf(buffer, "Raw packet: ");
+  for (int i = 0; i < packetLength; i++) {
+    buffer += sprintf(buffer, "%02X ", packet[i]);
+  }
+  
+  uint8_t decodedPacket[packetLength];
+  memcpy(decodedPacket, packet, packetLength);
+  
+  decodeV2Packet(decodedPacket);
+  
+  buffer += sprintf(buffer, "\n\nDecoded:\n");
+  buffer += sprintf(buffer, "Key      : %02X\n", decodedPacket[0]);
+  buffer += sprintf(buffer, "b1       : %02X\n", decodedPacket[1]);
+  buffer += sprintf(buffer, "ID       : %02X%02X\n", decodedPacket[2], decodedPacket[3]);
+  buffer += sprintf(buffer, "Command  : %02X\n", decodedPacket[4]);
+  buffer += sprintf(buffer, "Argument : %02X\n", decodedPacket[5]);
+  buffer += sprintf(buffer, "Sequence : %02X\n", decodedPacket[6]);
+  buffer += sprintf(buffer, "Group    : %02X\n", decodedPacket[7]);
+  buffer += sprintf(buffer, "Checksum : %02X", decodedPacket[8]);
+}

+ 40 - 0
lib/MiLight/RgbCctPacketFormatter.h

@@ -0,0 +1,40 @@
+#include <PacketFormatter.h>
+
+#define RGB_CCT_COMMAND_INDEX 4
+#define RGB_CCT_ARGUMENT_INDEX 5
+#define V2_OFFSET_JUMP_START 0x54
+
+#ifndef _RGB_CCT_PACKET_FORMATTER_H
+#define _RGB_CCT_PACKET_FORMATTER_H 
+
+class RgbCctPacketFormatter : public PacketFormatter {
+public:
+  static uint8_t const V2_OFFSETS[][4];
+    
+  RgbCctPacketFormatter()
+    : PacketFormatter(9)
+  { }
+  
+  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);
+  virtual void updateHue(uint16_t value);
+  virtual void updateColorRaw(uint8_t value);
+  virtual void updateColorWhite();
+  virtual void updateTemperature(uint8_t value);
+  virtual void updateSaturation(uint8_t value);
+  virtual void format(uint8_t const* packet, char* buffer);
+  virtual void unpair();
+  
+  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);
+  static uint8_t encodeByte(uint8_t byte, uint8_t s1, uint8_t xorKey, uint8_t s2);
+  static uint8_t decodeByte(uint8_t byte, uint8_t s1, uint8_t xorKey, uint8_t s2);
+};
+
+#endif

+ 68 - 0
lib/MiLight/RgbPacketFormatter.cpp

@@ -0,0 +1,68 @@
+#include <RgbPacketFormatter.h>
+
+void RgbPacketFormatter::initializePacket(uint8_t *packet) {
+  size_t packetPtr = 0;
+  
+  packet[packetPtr++] = RGB;
+  packet[packetPtr++] = deviceId >> 8;
+  packet[packetPtr++] = deviceId & 0xFF;
+  packet[packetPtr++] = 0;
+  packet[packetPtr++] = 0;
+  packet[packetPtr++] = sequenceNum++;
+}
+
+void RgbPacketFormatter::pair() { 
+  for (size_t i = 0; i < 5; i++) {
+    command(RGB_SPEED_UP, 0);
+  }
+}
+
+void RgbPacketFormatter::unpair() { 
+  for (size_t i = 0; i < 5; i++) {
+    command(RGB_SPEED_UP | 0x10, 0);
+  }
+}
+
+void RgbPacketFormatter::updateStatus(MiLightStatus status, uint8_t groupId) {
+  command(status == ON ? RGB_ON : RGB_OFF, 0);
+}
+
+void RgbPacketFormatter::command(uint8_t command, uint8_t arg) {
+  pushPacket();
+  currentPacket[RGB_COMMAND_INDEX] = command;
+}
+  
+void RgbPacketFormatter::updateHue(uint16_t value) {
+  const int16_t remappedColor = (value + 40) % 360;
+  updateColorRaw(rescale(remappedColor, 255, 360));
+}
+
+void RgbPacketFormatter::updateColorRaw(uint8_t value) {
+  currentPacket[RGB_COLOR_INDEX] = value;
+  command(0, 0);
+}
+
+void RgbPacketFormatter::updateBrightness(uint8_t value) {
+  valueByStepFunction(
+    &PacketFormatter::increaseBrightness,
+    &PacketFormatter::decreaseBrightness,
+    RGB_INTERVALS,
+    value / RGB_INTERVALS
+  );
+}
+
+void RgbPacketFormatter::increaseBrightness() {
+  command(RGB_BRIGHTNESS_UP, 0);
+}
+
+void RgbPacketFormatter::decreaseBrightness() {
+  command(RGB_BRIGHTNESS_DOWN, 0);
+}
+
+void RgbPacketFormatter::format(uint8_t const* packet, char* buffer) {
+  buffer += sprintf(buffer, "b0       : %02X\n", packet[0]);
+  buffer += sprintf(buffer, "ID       : %02X%02X\n", packet[1], packet[2]);
+  buffer += sprintf(buffer, "Color    : %02X\n", packet[3]);
+  buffer += sprintf(buffer, "Command  : %02X\n", packet[4]);
+  buffer += sprintf(buffer, "Sequence : %02X\n", packet[5]);
+}

+ 42 - 0
lib/MiLight/RgbPacketFormatter.h

@@ -0,0 +1,42 @@
+#include <PacketFormatter.h>
+
+#ifndef _RGB_PACKET_FORMATTER_H
+#define _RGB_PACKET_FORMATTER_H 
+
+#define RGB_COMMAND_INDEX 4
+#define RGB_COLOR_INDEX 3
+#define RGB_INTERVALS 10
+
+enum MiLightRgbButton {
+  RGB_OFF             = 0x01,
+  RGB_ON              = 0x02,
+  RGB_BRIGHTNESS_UP   = 0x03,
+  RGB_BRIGHTNESS_DOWN = 0x04,
+  RGB_SPEED_UP        = 0x05,
+  RGB_SPEED_DOWN      = 0x06,
+  RGB_MODE_UP         = 0x07,
+  RGB_MODE_DOWN       = 0x08,
+  RGB_PAIR            = RGB_SPEED_UP
+};
+
+class RgbPacketFormatter : public PacketFormatter {
+public:
+  RgbPacketFormatter()
+    : PacketFormatter(6, 20)
+  { }
+  
+  virtual void updateStatus(MiLightStatus status, uint8_t groupId);
+  virtual void updateBrightness(uint8_t value);
+  virtual void increaseBrightness();
+  virtual void decreaseBrightness();
+  virtual void command(uint8_t command, uint8_t arg);
+  virtual void updateHue(uint16_t value);
+  virtual void updateColorRaw(uint8_t value);
+  virtual void format(uint8_t const* packet, char* buffer);
+  virtual void pair();
+  virtual void unpair();
+  
+  virtual void initializePacket(uint8_t* packet);
+};
+
+#endif

+ 62 - 0
lib/MiLight/RgbwPacketFormatter.cpp

@@ -0,0 +1,62 @@
+#include <RgbwPacketFormatter.h>
+#include <MiLightButtons.h>
+
+void RgbwPacketFormatter::initializePacket(uint8_t* packet) {
+  size_t packetPtr = 0;
+  
+  packet[packetPtr++] = RGBW;
+  packet[packetPtr++] = deviceId >> 8;
+  packet[packetPtr++] = deviceId & 0xFF;
+  packet[packetPtr++] = 0;
+  packet[packetPtr++] = (groupId & 0x07);
+  packet[packetPtr++] = 0;
+  packet[packetPtr++] = sequenceNum++;
+}
+
+void RgbwPacketFormatter::unpair() { 
+  PacketFormatter::updateStatus(ON);
+  updateColorWhite();
+}
+
+void RgbwPacketFormatter::updateStatus(MiLightStatus status, uint8_t groupId) {
+  uint8_t button = RGBW_GROUP_1_ON + ((groupId - 1)*2) + status;
+  command(button, 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);
+
+  // The actual protocol uses a bizarre range where min is 16, max is 23:
+  // [16, 15, ..., 0, 31, ..., 23]
+  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();
+  currentPacket[RGBW_COMMAND_INDEX] = command;
+}
+  
+void RgbwPacketFormatter::updateHue(uint16_t value) {
+  const int16_t remappedColor = (value + 40) % 360;
+  updateColorRaw(rescale(remappedColor, 255, 360));
+}
+
+void RgbwPacketFormatter::updateColorRaw(uint8_t value) {
+  currentPacket[RGBW_COLOR_INDEX] = value;
+  command(RGBW_COLOR, 0);
+}
+
+void RgbwPacketFormatter::updateColorWhite() {
+  uint8_t button = RGBW_GROUP_1_MAX_LEVEL + ((groupId - 1)*2);
+  command(button, 0);
+}
+
+void RgbwPacketFormatter::format(uint8_t const* packet, char* buffer) {
+  PacketFormatter::formatV1Packet(packet, buffer);
+}

+ 28 - 0
lib/MiLight/RgbwPacketFormatter.h

@@ -0,0 +1,28 @@
+#include <PacketFormatter.h>
+
+#ifndef _RGBW_PACKET_FORMATTER_H
+#define _RGBW_PACKET_FORMATTER_H 
+
+#define RGBW_COMMAND_INDEX 5
+#define RGBW_BRIGHTNESS_GROUP_INDEX 4
+#define RGBW_COLOR_INDEX 3
+
+class RgbwPacketFormatter : public PacketFormatter {
+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);
+  virtual void updateHue(uint16_t value);
+  virtual void updateColorRaw(uint8_t value);
+  virtual void updateColorWhite();
+  virtual void format(uint8_t const* packet, char* buffer);
+  virtual void unpair();
+  
+  virtual void initializePacket(uint8_t* packet);
+};
+
+#endif

+ 1 - 1
lib/Settings/Settings.h

@@ -8,7 +8,7 @@
 #define SETTINGS_FILE  "/config.json"
 #define SETTINGS_TERMINATOR '\0'
 
-#define WEB_INDEX_FILENAME "/index.html"
+#define WEB_INDEX_FILENAME "/web/index.html"
 
 class GatewayConfig {
 public:

+ 51 - 0
lib/Udp/MiLightUdpServer.cpp

@@ -0,0 +1,51 @@
+#include <MiLightUdpServer.h>
+#include <V5MiLightUdpServer.h>
+#include <V6MiLightUdpServer.h>
+#include <ESP8266WiFi.h>
+
+MiLightUdpServer::MiLightUdpServer(MiLightClient*& client, uint16_t port, uint16_t deviceId)
+  : client(client), 
+    port(port),
+    deviceId(deviceId),
+    lastGroup(0)
+{ }
+
+MiLightUdpServer::~MiLightUdpServer() {
+  stop();
+}
+
+void MiLightUdpServer::begin() {
+  socket.begin(port);
+}
+
+void MiLightUdpServer::stop() {
+  socket.stop();
+}
+
+void MiLightUdpServer::handleClient() {
+  const size_t packetSize = socket.parsePacket();
+  
+  if (packetSize) {
+    socket.read(packetBuffer, packetSize);
+    
+#ifdef MILIGHT_UDP_DEBUG
+    printf("Handling packet: ");
+    for (size_t i = 0; i < packetSize; i++) {
+      printf("%02X ", packetBuffer[i]);
+    }
+    printf("\n");
+#endif
+    
+    handlePacket(packetBuffer, packetSize);
+  }
+}
+
+MiLightUdpServer* MiLightUdpServer::fromVersion(uint8_t version, MiLightClient*& client, uint16_t port, uint16_t deviceId) {
+  if (version == 0 || version == 5) {
+    return new V5MiLightUdpServer(client, port, deviceId);
+  } else if (version == 6) {
+    return new V6MiLightUdpServer(client, port, deviceId);
+  }
+  
+  return NULL;
+}

+ 40 - 0
lib/Udp/MiLightUdpServer.h

@@ -0,0 +1,40 @@
+#include <Arduino.h>
+#include <MiLightClient.h>
+#include <WiFiUdp.h>
+
+// This protocol is documented here:
+// http://www.limitlessled.com/dev/
+
+#define MILIGHT_PACKET_BUFFER_SIZE 30 
+
+// Uncomment to enable Serial printing of packets
+// #define MILIGHT_UDP_DEBUG
+
+#ifndef _MILIGHT_UDP_SERVER
+#define _MILIGHT_UDP_SERVER 
+
+class MiLightUdpServer {
+public:
+  MiLightUdpServer(MiLightClient*& client, uint16_t port, uint16_t deviceId);
+  ~MiLightUdpServer();
+    
+  void stop();
+  void begin();
+  void handleClient();
+  
+  static MiLightUdpServer* fromVersion(uint8_t version, MiLightClient*&, uint16_t port, uint16_t deviceId);
+    
+protected:
+  WiFiUDP socket;
+  MiLightClient*& client;
+  uint16_t port;
+  uint16_t deviceId;
+  uint8_t lastGroup;
+  uint8_t packetBuffer[MILIGHT_PACKET_BUFFER_SIZE];
+  uint8_t responseBuffer[MILIGHT_PACKET_BUFFER_SIZE];
+  
+  // Should return size of the response packet
+  virtual void handlePacket(uint8_t* packet, size_t packetSize) = 0;
+};
+
+#endif

+ 137 - 0
lib/Udp/V5MiLightUdpServer.cpp

@@ -0,0 +1,137 @@
+#include <V5MiLightUdpServer.h>
+
+void V5MiLightUdpServer::handlePacket(uint8_t* packet, size_t packetSize) {
+  if (packetSize == 2 || packetSize == 3) {
+    handleCommand(packet[0], packet[1]);
+  } else {
+    Serial.print("V5MilightUdpServer: unexpected packet length. Should always be 2-3, was: ");
+    Serial.println(packetSize);  
+  }
+}
+
+void V5MiLightUdpServer::handleCommand(uint8_t command, uint8_t commandArg) {
+  if (command >= UDP_RGBW_GROUP_1_ON && command <= UDP_RGBW_GROUP_4_OFF) {
+    const MiLightStatus status = (command % 2) == 1 ? ON : OFF;
+    const uint8_t groupId = (command - UDP_RGBW_GROUP_1_ON + 2)/2;
+    
+    client->prepare(MilightRgbwConfig, deviceId, groupId);
+    client->updateStatus(status);
+    
+    this->lastGroup = groupId;
+  } else if (command >= UDP_RGBW_GROUP_ALL_WHITE && command <= UDP_RGBW_GROUP_4_WHITE) {
+    const uint8_t groupId = (command - UDP_RGBW_GROUP_ALL_WHITE)/2;
+    client->prepare(MilightRgbwConfig, deviceId, groupId);
+    client->updateColorWhite();
+    this->lastGroup = groupId;
+  } else if (uint8_t cctGroup = cctCommandIdToGroup(command)) {
+    client->prepare(MilightCctConfig, deviceId, cctGroup);
+    client->updateStatus(cctCommandToStatus(command));
+    this->lastGroup = cctGroup;
+  }
+  else {
+    client->prepare(MilightRgbwConfig, deviceId, lastGroup);
+    bool handled = true;
+    
+    switch (command) {
+      case UDP_RGBW_ALL_ON:
+        client->updateStatus(ON, 0);
+        break;
+      
+      case UDP_RGBW_ALL_OFF:
+        client->updateStatus(OFF, 0);
+        break;
+      
+      case UDP_RGBW_COLOR:
+        // UDP color is shifted by 0xC8 from 2.4 GHz color, and the spectrum is
+        // flipped (R->B->G instead of R->G->B)
+        client->updateColorRaw(0xFF-(commandArg + 0x35));
+        break;
+        
+      case UDP_RGBW_DISCO_MODE:
+        pressButton(RGBW_DISCO_MODE);
+        break;
+        
+      case UDP_RGBW_SPEED_DOWN:
+        pressButton(RGBW_SPEED_DOWN);
+        break;
+        
+      case UDP_RGBW_SPEED_UP:
+        pressButton(RGBW_SPEED_UP);
+        break;
+        
+      case UDP_RGBW_BRIGHTNESS:
+        // map [2, 27] --> [0, 100]
+        client->updateBrightness(
+          round(((commandArg - 2) / 25.0)*100)
+        );
+        break;
+        
+      default:
+        handled = false;
+    }
+    
+    client->prepare(MilightCctConfig);
+    
+    switch(command) {
+      case UDP_CCT_BRIGHTNESS_DOWN:
+        client->decreaseBrightness();
+        break;
+        
+      case UDP_CCT_BRIGHTNESS_UP:
+        client->increaseBrightness();
+        break;
+        
+      case UDP_CCT_TEMPERATURE_DOWN:
+        client->decreaseTemperature();
+        break;
+        
+      case UDP_CCT_TEMPERATURE_UP:
+        client->increaseTemperature();
+        break;
+        
+      default:
+        if (!handled) {
+          Serial.print("V5MiLightUdpServer - Unhandled command: ");
+          Serial.println(command);
+        }
+    }
+  }
+}
+
+void V5MiLightUdpServer::pressButton(uint8_t button) {
+  client->command(button, 0);
+}  
+
+uint8_t V5MiLightUdpServer::cctCommandIdToGroup(uint8_t command) {
+  switch (command) {
+    case UDP_CCT_GROUP_1_ON:
+    case UDP_CCT_GROUP_1_OFF:
+      return 1;
+    case UDP_CCT_GROUP_2_ON:
+    case UDP_CCT_GROUP_2_OFF:
+      return 2;
+    case UDP_CCT_GROUP_3_ON:
+    case UDP_CCT_GROUP_3_OFF:
+      return 3;
+    case UDP_CCT_GROUP_4_ON:
+    case UDP_CCT_GROUP_4_OFF:
+      return 4;
+  }
+  
+  return 0;
+}  
+  
+MiLightStatus V5MiLightUdpServer::cctCommandToStatus(uint8_t command) {
+  switch (command) {
+    case UDP_CCT_GROUP_1_ON:
+    case UDP_CCT_GROUP_2_ON:
+    case UDP_CCT_GROUP_3_ON:
+    case UDP_CCT_GROUP_4_ON:
+      return ON;
+    case UDP_CCT_GROUP_1_OFF:
+    case UDP_CCT_GROUP_2_OFF:
+    case UDP_CCT_GROUP_3_OFF:
+    case UDP_CCT_GROUP_4_OFF:
+      return OFF;
+  }
+}

+ 15 - 26
lib/MiLight/MiLightUdpServer.h

@@ -1,17 +1,13 @@
-#include <Arduino.h>
-#include <MiLightClient.h>
-#include <WiFiUdp.h>
-
 // This protocol is documented here:
 // http://www.limitlessled.com/dev/
 
-#define MILIGHT_PACKET_BUFFER_SIZE 10
-
-// Uncomment to enable Serial printing of packets
-//#define MILIGHT_UDP_DEBUG
+#include <Arduino.h>
+#include <MiLightClient.h>
+#include <WiFiUdp.h>
+#include <MiLightUdpServer.h>
 
-#ifndef _MILIGHT_UDP_SERVER
-#define _MILIGHT_UDP_SERVER 
+#ifndef _V5_MILIGHT_UDP_SERVER
+#define _V5_MILIGHT_UDP_SERVER 
 
 enum MiLightUdpCommands {
   UDP_CCT_GROUP_1_ON         = 0x38,
@@ -49,27 +45,20 @@ enum MiLightUdpCommands {
   UDP_RGBW_COLOR             = 0x40
 };
 
-class MiLightUdpServer {
+class V5MiLightUdpServer : public MiLightUdpServer {
 public:
-  MiLightUdpServer(MiLightClient*& client, uint16_t port, uint16_t deviceId);
-  ~MiLightUdpServer();
-    
-  void stop();
-  void begin();
-  void handleClient();
+  V5MiLightUdpServer(MiLightClient*& client, uint16_t port, uint16_t deviceId)
+    : MiLightUdpServer(client, port, deviceId)
+  { }
+  
+  // Should return size of the response packet
+  virtual void handlePacket(uint8_t* packet, size_t packetSize);
     
 protected:
-  WiFiUDP socket;
-  MiLightClient*& client;
-  uint16_t port;
-  uint16_t deviceId;
-  uint8_t lastGroup;
-  char packetBuffer[MILIGHT_PACKET_BUFFER_SIZE];
-  
   void handleCommand(uint8_t command, uint8_t commandArg);
-  void pressButton(uint8_t group, uint8_t button);
+  void pressButton(uint8_t button);
   uint8_t cctCommandIdToGroup(uint8_t command);
   MiLightStatus cctCommandToStatus(uint8_t command);
 };
 
-#endif
+#endif

+ 250 - 0
lib/Udp/V6MiLightUdpServer.cpp

@@ -0,0 +1,250 @@
+#include <V6MiLightUdpServer.h>
+#include <ESP8266WiFi.h>
+#include <Arduino.h>
+  
+uint8_t V6MiLightUdpServer::START_SESSION_COMMAND[] = {
+  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, 
+   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
+   0x00
+};
+
+uint8_t V6MiLightUdpServer::COMMAND_HEADER[] = {
+  0x80, 0x00, 0x00, 0x00
+};
+
+uint8_t V6MiLightUdpServer::HEARTBEAT_HEADER[] = {
+  0xD0, 0x00, 0x00, 0x00, 0x02
+};
+
+uint8_t V6MiLightUdpServer::COMMAND_RESPONSE[] = {
+  0x88, 0x00, 0x00, 0x00, 0x03, 0x00, 0xFF, 0x00
+};
+
+template<typename T, size_t sz>
+size_t size(T(&)[sz]) {
+    return sz;
+}
+
+V6MiLightUdpServer::~V6MiLightUdpServer() {
+  V6Session* cur = firstSession;
+  
+  while (cur != NULL) {
+    V6Session* next = cur->next;
+    delete cur;
+    cur = next;
+  }
+}
+
+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;
+}
+
+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);
+  response[19] = sessionId >> 8;
+  response[20] = sessionId & 0xFF;
+  
+  sendResponse(sessionId, response, len);
+}
+  
+void 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;
+  }
+  
+#ifdef MILIGHT_UDP_DEBUG
+  printf("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();
+}
+
+bool V6MiLightUdpServer::handleV1BulbCommand(uint8_t group, uint32_t _cmd, uint32_t _arg) {
+  // Makes more sense to use V5 protocol for now.
+}
+
+bool V6MiLightUdpServer::handleV2BulbCommand(uint8_t group, uint32_t _cmd, uint32_t _arg) {
+  const uint8_t cmd = _cmd & 0xFF;
+  const uint8_t arg = _arg >> 24;
+  
+  client->prepare(MilightRgbCctConfig, deviceId, group);
+  
+  switch (cmd) {
+    case V2_STATUS:
+      if (arg == 0x01) {
+        client->updateStatus(ON);
+      } else if (arg == 0x02) {
+        client->updateStatus(OFF);
+      } else if (arg == 0x05) {
+        client->updateBrightness(0);
+      }
+      break;
+      
+    case V2_COLOR:
+      client->updateColorRaw(arg);
+      break;
+      
+    case V2_KELVIN:
+      client->updateTemperature(arg);
+      break;
+      
+    case V2_BRIGHTNESS:
+      client->updateBrightness(arg);
+      break;
+      
+    case V2_SATURATION:
+      client->updateSaturation(100 - arg);
+      break;
+      
+    default:
+      return false;
+  }
+  
+  return true;
+}
+  
+void V6MiLightUdpServer::handleCommand(
+  uint16_t sessionId,
+  uint8_t sequenceNum,
+  uint8_t* cmd,
+  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("Command type: %02X, command: %08X, arg: %08X\n", cmdType, cmdHeader, cmdArg);
+#endif
+  
+  bool handled = false;
+  
+  if ((cmdHeader & 0x0800) == 0x0800) {
+    handled = handleV2BulbCommand(group, cmdHeader, cmdArg);
+  } else if ((cmdHeader & 0x0700) == 0x0700) {
+    handled = handleV1BulbCommand(group, cmdHeader, cmdArg);
+  }
+  
+  if (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("V6MiLightUdpServer - Unhandled command: ");
+  for (size_t i = 0; i < V6_COMMAND_LEN; i++) {
+    printf("%02X ", cmd[i]);
+  }
+  printf("\n");
+#endif
+}
+
+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;
+  
+  sendResponse(sessionId, responseBuffer, 12);
+}
+
+void V6MiLightUdpServer::handlePacket(uint8_t* packet, size_t packetSize) {
+#ifdef MILIGHT_UDP_DEBUG
+  printf("Packet size: %d\n", packetSize);
+#endif
+  
+  if (packetSize >= size(START_SESSION_COMMAND) && memcmp(START_SESSION_COMMAND, packet, size(START_SESSION_COMMAND)) == 0) {
+    handleStartSession();
+  } else if (packetSize >= size(HEARTBEAT_HEADER) && memcmp(HEARTBEAT_HEADER, packet, size(HEARTBEAT_HEADER)) == 0) {
+    uint16_t sessionId = readInt<uint16_t>(packet+5);
+    handleHeartbeat(sessionId);
+  } else if (packetSize == 22 && memcmp(COMMAND_HEADER, packet, size(COMMAND_HEADER)) == 0) {
+    uint16_t sessionId = readInt<uint16_t>(packet+5);
+    uint8_t sequenceNum = packet[8];
+    uint8_t* cmd = packet+10;
+    uint8_t group = packet[19];
+    uint8_t checksum = packet[21];
+    
+#ifdef MILIGHT_UDP_DEBUG
+    printf("session: %04X, sequence: %d, group: %d, checksum: %d\n", sessionId, sequenceNum, group, checksum);
+#endif
+    
+    handleCommand(sessionId, sequenceNum, cmd, group, checksum);
+  } else {
+    Serial.println("Unhandled V6 packet");
+  }
+}

+ 97 - 0
lib/Udp/V6MiLightUdpServer.h

@@ -0,0 +1,97 @@
+// This protocol is documented here:
+// http://www.limitlessled.com/dev/
+
+#include <Arduino.h>
+#include <MiLightClient.h>
+#include <WiFiUdp.h>
+#include <MiLightUdpServer.h>
+#include <Vector.h>
+
+#define V6_COMMAND_LEN 8
+#define V6_MAX_SESSIONS 10
+
+#ifndef _V6_MILIGHT_UDP_SERVER
+#define _V6_MILIGHT_UDP_SERVER 
+
+enum V2CommandIds {
+  V2_COLOR = 0x01,
+  V2_SATURATION = 0x02,
+  V2_BRIGHTNESS = 0x03,
+  V2_STATUS = 0x04,
+  V2_KELVIN = 0x05
+};
+
+struct V6Session {
+  V6Session(IPAddress ipAddr, uint16_t port, uint16_t sessionId)
+    : ipAddr(ipAddr),
+      port(port),
+      sessionId(sessionId),
+      next(NULL)
+  { }
+  
+  IPAddress ipAddr;
+  uint16_t port;
+  uint16_t sessionId;
+  V6Session* next;
+};
+
+class V6MiLightUdpServer : public MiLightUdpServer {
+public:
+  V6MiLightUdpServer(MiLightClient*& client, uint16_t port, uint16_t deviceId)
+    : MiLightUdpServer(client, port, deviceId),
+      sessionId(0),
+      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 uint8_t START_SESSION_COMMAND[];
+  static uint8_t START_SESSION_RESPONSE[];
+  static uint8_t COMMAND_HEADER[];
+  static uint8_t COMMAND_RESPONSE[];
+  static uint8_t SEARCH_COMMAND[];
+  static uint8_t LOCAL_SEARCH_COMMAND[];
+  static uint8_t HEARTBEAT_HEADER[];
+  
+  V6Session* firstSession;
+  size_t numSessions;
+  uint16_t sessionId;
+  
+  uint16_t beginSession();
+  void sendResponse(uint16_t sessionId, uint8_t* responseBuffer, size_t responseSize);
+  
+  void handleStartSession();
+  void handleHeartbeat(uint16_t sessionId);
+  void handleCommand(
+    uint16_t sessionId,
+    uint8_t sequenceNum,
+    uint8_t* cmd,
+    uint8_t group,
+    uint8_t checksum
+  );
+  
+  bool handleV1BulbCommand(
+    uint8_t group,
+    uint32_t cmd,
+    uint32_t cmdArg
+  );
+  
+  bool handleV2BulbCommand(
+    uint8_t group,
+    uint32_t cmd,
+    uint32_t cmdArg
+  );
+};
+
+#endif

+ 101 - 53
lib/WebServer/MiLightHttpServer.cpp

@@ -12,9 +12,11 @@ void MiLightHttpServer::begin() {
   server.on("/settings", HTTP_GET, handleServeFile(SETTINGS_FILE, "application/json"));
   server.on("/settings", HTTP_PUT, [this]() { handleUpdateSettings(); });
   server.on("/settings", HTTP_POST, [this]() { server.send(200, "text/plain", "success"); }, handleUpdateFile(SETTINGS_FILE));
+  server.on("/radio_configs", HTTP_GET, [this]() { handleGetRadioConfigs(); });
   server.onPattern("/gateway_traffic/:type", HTTP_GET, [this](const UrlTokenBindings* b) { handleListenGateway(b); });
   server.onPattern("/gateways/:device_id/:type/:group_id", HTTP_PUT, [this](const UrlTokenBindings* b) { handleUpdateGroup(b); });
   server.onPattern("/gateways/:device_id/:type", HTTP_PUT, [this](const UrlTokenBindings* b) { handleUpdateGateway(b); });
+  server.onPattern("/send_raw/:type", HTTP_PUT, [this](const UrlTokenBindings* b) { handleSendRaw(b); });
   server.on("/web", HTTP_POST, [this]() { server.send(200, "text/plain", "success"); }, handleUpdateFile(WEB_INDEX_FILENAME));
   server.on("/firmware", HTTP_POST, 
     [this](){
@@ -66,6 +68,21 @@ void MiLightHttpServer::onSettingsSaved(SettingsSavedHandler handler) {
   this->settingsSavedHandler = handler;
 }
   
+void MiLightHttpServer::handleGetRadioConfigs() {
+  DynamicJsonBuffer buffer;
+  JsonArray& arr = buffer.createArray();
+  
+  for (size_t i = 0; i < MiLightRadioConfig::NUM_CONFIGS; i++) {
+    const MiLightRadioConfig* config = MiLightRadioConfig::ALL_CONFIGS[i];
+    arr.add(config->name);
+  }
+  
+  String body;
+  arr.printTo(body);
+  
+  server.send(200, "application/json", body);
+}
+  
 ESP8266WebServer::THandlerFunction MiLightHttpServer::handleServeFile(
   const char* filename, 
   const char* contentType, 
@@ -129,29 +146,39 @@ void MiLightHttpServer::handleUpdateSettings() {
 
 void MiLightHttpServer::handleListenGateway(const UrlTokenBindings* bindings) {
   bool available = false;
-  MiLightRadioConfig config = milightClient->getRadioConfig(bindings->get("type"));
+  MiLightRadioConfig* config = MiLightRadioConfig::fromString(bindings->get("type"));
+  
+  if (config == NULL) {
+    String body = "Unknown device type: ";
+    body += bindings->get("type");
+    
+    server.send(400, "text/plain", body);
+    return;
+  }
+  
+  milightClient->prepare(*config, 0, 0);
   
   while (!available) {
     if (!server.clientConnected()) {
       return;
     }
     
-    if (milightClient->available(config.type)) {
+    if (milightClient->available()) {
       available = true;
     }
     
     yield();
   }
   
-  uint8_t packet[config.packetLength];
-  milightClient->read(static_cast<MiLightRadioType>(config.type), packet);
+  uint8_t packet[config->getPacketLength()];
+  milightClient->read(packet);
   
   String response = "Packet received (";
   response += String(sizeof(packet)) + " bytes)";
   response += ":\n";
   
   char ppBuffer[200];
-  milightClient->formatPacket(config, packet, ppBuffer);
+  milightClient->formatPacket(packet, ppBuffer);
   response += String(ppBuffer);
   
   response += "\n\n";
@@ -170,9 +197,9 @@ void MiLightHttpServer::handleUpdateGroup(const UrlTokenBindings* urlBindings) {
   
   const uint16_t deviceId = parseInt<uint16_t>(urlBindings->get("device_id"));
   const uint8_t groupId = urlBindings->get("group_id").toInt();
-  const MiLightRadioType type = MiLightClient::getRadioType(urlBindings->get("type"));
+  MiLightRadioConfig* config = MiLightRadioConfig::fromString(urlBindings->get("type"));
   
-  if (type == UNKNOWN) {
+  if (config == NULL) {
     String body = "Unknown device type: ";
     body += urlBindings->get("type");
     
@@ -183,69 +210,59 @@ void MiLightHttpServer::handleUpdateGroup(const UrlTokenBindings* urlBindings) {
   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(type, deviceId, groupId, status);
+    milightClient->updateStatus(status);
   }
       
   if (request.containsKey("command")) {
     if (request["command"] == "unpair") {
-      milightClient->unpair(type, deviceId, groupId);
+      milightClient->unpair();
     }
     
     if (request["command"] == "pair") {
-      milightClient->pair(type, deviceId, groupId);
+      milightClient->pair();
     }
-  }
-  
-  if (type == RGBW) {
-    if (request.containsKey("hue")) {
-      milightClient->updateHue(deviceId, groupId, request["hue"]);
+    
+    if (request["command"] == "set_white") {
+      milightClient->updateColorWhite();
     }
     
-    if (request.containsKey("level")) {
-      milightClient->updateBrightness(deviceId, groupId, request["level"]);
+    if (request["command"] == "level_up") {
+      milightClient->increaseBrightness();
     }
     
-    if (request.containsKey("command")) {
-      if (request["command"] == "set_white") {
-        milightClient->updateColorWhite(deviceId, groupId);
-      }
+    if (request["command"] == "level_down") {
+      milightClient->decreaseBrightness();
     }
-  } else if (type == CCT) {
-    if (request.containsKey("temperature")) {
-      milightClient->updateTemperature(deviceId, groupId, request["temperature"]);
+    
+    if (request["command"] == "temperature_up") {
+      milightClient->increaseTemperature();
     }
     
-    if (request.containsKey("level")) {
-      milightClient->updateCctBrightness(deviceId, groupId, request["level"]);
+    if (request["command"] == "temperature_down") {
+      milightClient->decreaseTemperature();
     }
+  }
+  
+  if (request.containsKey("hue")) {
+    milightClient->updateHue(request["hue"]);
+  }
     
-    if (request.containsKey("command")) {
-      // CCT command work more effectively with a lower number of repeats it seems.
-      milightClient->setResendCount(MILIGHT_DEFAULT_RESEND_COUNT);
-      
-      if (request["command"] == "level_up") {
-        milightClient->increaseCctBrightness(deviceId, groupId);
-      }
-      
-      if (request["command"] == "level_down") {
-        milightClient->decreaseCctBrightness(deviceId, groupId);
-      }
-      
-      if (request["command"] == "temperature_up") {
-        milightClient->increaseTemperature(deviceId, groupId);
-      }
-      
-      if (request["command"] == "temperature_down") {
-        milightClient->decreaseTemperature(deviceId, groupId);
-      }
+  if (request.containsKey("level")) {
+    milightClient->updateBrightness(request["level"]);
+  }
+    
+  if (request.containsKey("temperature")) {
+    milightClient->updateTemperature(request["temperature"]);
+  }
   
-      milightClient->setResendCount(settings.packetRepeats);
-    }
-  } 
+  if (request.containsKey("saturation")) {
+    milightClient->updateSaturation(request["saturation"]);
+  }
   
   milightClient->setResendCount(settings.packetRepeats);
   
@@ -257,9 +274,9 @@ void MiLightHttpServer::handleUpdateGateway(const UrlTokenBindings* urlBindings)
   JsonObject& request = buffer.parse(server.arg("plain"));
   
   const uint16_t deviceId = parseInt<uint16_t>(urlBindings->get("device_id"));
-  const MiLightRadioType type = MiLightClient::getRadioType(urlBindings->get("type"));
+  MiLightRadioConfig* config = MiLightRadioConfig::fromString(urlBindings->get("type"));
   
-  if (type == UNKNOWN) {
+  if (config == NULL) {
     String body = "Unknown device type: ";
     body += urlBindings->get("type");
     
@@ -267,15 +284,46 @@ void MiLightHttpServer::handleUpdateGateway(const UrlTokenBindings* urlBindings)
     return;
   }
   
-  milightClient->setResendCount(MILIGHT_DEFAULT_RESEND_COUNT);
+  milightClient->prepare(*config, deviceId, 0);
   
   if (request.containsKey("status")) {
     if (request["status"] == "on") {
-      milightClient->allOn(type, deviceId);
+      milightClient->updateStatus(ON);
     } else if (request["status"] == "off") {
-      milightClient->allOff(type, deviceId);
+      milightClient->updateStatus(OFF);
     }
   }
   
   server.send(200, "application/json", "true");
+}
+
+void MiLightHttpServer::handleSendRaw(const UrlTokenBindings* bindings) {
+  DynamicJsonBuffer buffer;
+  JsonObject& request = buffer.parse(server.arg("plain"));
+  MiLightRadioConfig* config = MiLightRadioConfig::fromString(bindings->get("type"));
+  
+  if (config == NULL) {
+    String body = "Unknown device type: ";
+    body += bindings->get("type");
+    
+    server.send(400, "text/plain", body);
+    return;
+  }
+  
+  uint8_t packet[config->getPacketLength()];
+  const String& hexPacket = request["packet"];
+  hexStrToBytes<uint8_t>(hexPacket.c_str(), hexPacket.length(), packet, config->getPacketLength());
+  
+  size_t numRepeats = MILIGHT_DEFAULT_RESEND_COUNT;
+  if (request.containsKey("num_repeats")) {
+    numRepeats = request["num_repeats"];
+  }
+  
+  milightClient->prepare(*config, 0, 0);
+  
+  for (size_t i = 0; i < numRepeats; i++) {
+    milightClient->write(packet);
+  }
+  
+  server.send(200, "text/plain", "true");
 }

+ 2 - 0
lib/WebServer/MiLightHttpServer.h

@@ -32,7 +32,9 @@ protected:
   void applySettings(Settings& settings);
   
   void handleUpdateSettings();
+  void handleGetRadioConfigs();
   void handleListenGateway(const UrlTokenBindings* urlBindings);
+  void handleSendRaw(const UrlTokenBindings* urlBindings);
   void handleUpdateGroup(const UrlTokenBindings* urlBindings);
   void handleUpdateGateway(const UrlTokenBindings* urlBindings);
   

+ 36 - 0
lib/readme.txt

@@ -0,0 +1,36 @@
+
+This directory is intended for the project specific (private) libraries.
+PlatformIO will compile them to static libraries and link to executable file.
+
+The source code of each library should be placed in separate directory, like
+"lib/private_lib/[here are source files]".
+
+For example, see how can be organized `Foo` and `Bar` libraries:
+
+|--lib
+|  |--Bar
+|  |  |--docs
+|  |  |--examples
+|  |  |--src
+|  |     |- Bar.c
+|  |     |- Bar.h
+|  |--Foo
+|  |  |- Foo.c
+|  |  |- Foo.h
+|  |- readme.txt --> THIS FILE
+|- platformio.ini
+|--src
+   |- main.c
+
+Then in `src/main.c` you should use:
+
+#include <Foo.h>
+#include <Bar.h>
+
+// rest H/C/CPP code
+
+PlatformIO will find your libraries automatically, configure preprocessor's
+include paths and build them.
+
+More information about PlatformIO Library Dependency Finder
+- http://docs.platformio.org/page/librarymanager/ldf.html

+ 11 - 5
src/main.cpp

@@ -36,13 +36,19 @@ void initMilightUdpServers() {
   
   for (size_t i = 0; i < settings.numGatewayConfigs; i++) {
     GatewayConfig* config = settings.gatewayConfigs[i];
+    MiLightUdpServer* server = MiLightUdpServer::fromVersion(
+      config->protocolVersion,
+      milightClient,
+      config->port,
+      config->deviceId
+    );
     
-    if (config->protocolVersion == 0) {
-      udpServers[i] = new MiLightUdpServer(milightClient, config->port, config->deviceId);
-      udpServers[i]->begin();
-    } else {
-      Serial.print("Error initializing milight UDP server - Unsupported protocolVersion: ");
+    if (server == NULL) {
+      Serial.print("Error creating UDP server with protocol version: ");
       Serial.println(config->protocolVersion);
+    } else {
+      udpServers[i] = server;
+      udpServers[i]->begin();
     }
   }
 }