Ver código fonte

preliminary support for rgb+cct

Chris Mullins 8 anos atrás
pai
commit
ab67da90f7

+ 56 - 0
lib/MiLight/CctPacketFormatter.cpp

@@ -0,0 +1,56 @@
+#include <CctPacketFormatter.h>
+#include <MiLightButtons.h>
+
+void CctPacketFormatter::reset() {
+  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::status(MiLightStatus status, uint8_t groupId) {
+  packet[CCT_COMMAND_INDEX] = getCctStatusButton(groupId, status);
+}
+
+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;
+}

+ 21 - 0
lib/MiLight/CctPacketFormatter.h

@@ -0,0 +1,21 @@
+#include <PacketFormatter.h>
+
+#ifndef _CCT_PACKET_FORMATTER_H
+#define _CCT_PACKET_FORMATTER_H 
+
+#define CCT_COMMAND_INDEX 5
+
+class CctPacketFormatter : public PacketFormatter {
+public:
+  CctPacketFormatter(size_t packetLength)
+    : PacketFormatter(packetLength)
+  { }
+  
+  virtual void status(MiLightStatus status, uint8_t groupId);
+  
+  virtual void reset();
+  
+  static uint8_t getCctStatusButton(uint8_t groupId, MiLightStatus status);
+};
+
+#endif

+ 7 - 0
lib/MiLight/MiLightButtons.h

@@ -1,6 +1,13 @@
 #ifndef _MILIGHT_BUTTONS
 #define _MILIGHT_BUTTONS 
 
+enum MiLightRadioType {
+  UNKNOWN = 0,
+  RGBW  = 0xB8,
+  CCT   = 0x5A,
+  RGB_CCT = 0x20
+};
+
 enum MiLightRgbCctCommand {
   RGB_CCT_ON = 0x01,
   RGB_CCT_OFF = 0x01,

+ 90 - 312
lib/MiLight/MiLightClient.cpp

@@ -2,359 +2,170 @@
 #include <MiLightRadioConfig.h>
 #include <Arduino.h>
 
-#define V2_OFFSET(byte, key) ( V2_OFFSETS[byte-1][key%4] )
-
-uint8_t const MiLightClient::V2_OFFSETS[][4] = {
-  { 0x45, 0x1F, 0x14, 0x5C },
-  { 0xAB, 0x49, 0x63, 0x91 },
-  { 0x2D, 0x1F, 0x4A, 0xEB },
-  { 0xAF, 0x03, 0x1D, 0xF3 },
-  { 0x5A, 0x22, 0x30, 0x11 },
-  { 0x04, 0xD8, 0x71, 0x42 },
-  { 0xAF, 0x04, 0xDD, 0x07 },
-  { 0xE1, 0x93, 0xB8, 0xE4 }
-};
-
-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 < NUM_RADIOS; 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;
   }
   
   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 MiLightRadioConfig& radioConfig, 
-  uint8_t packet[]) {
-    
-  MiLightRadio* radio = getRadio(radioConfig.type);
-  
-  if (radio == NULL) {
+void MiLightClient::write(uint8_t packet[]) {
+  if (currentRadio == NULL) {
     return;
   }
   
   for (int i = 0; i < this->resendCount; i++) {
-    radio->write(packet, radioConfig.packetLength);
+    currentRadio->getRadio()->write(packet, currentRadio->config.packetLength);
   }
 }
-
-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(MilightRgbwConfig, packet);
+    
+void MiLightClient::updateColorRaw(const uint8_t color) {
+  formatter->updateColorRaw(color);
+  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(MilightCctConfig, packet);
+void MiLightClient::updateHue(const uint16_t hue) {
+  formatter->updateHue(hue);
+  flushPacket();
 }
 
-void MiLightClient::writeRgbCct(const uint16_t deviceId,
-  const uint8_t command,
-  const uint8_t arg,
-  const uint8_t group) {
-    
-  uint8_t packet[MilightRgbCctConfig.packetLength];
-  uint8_t sequenceNum = nextSequenceNum();
-  size_t packetPtr = 0;
-  
-  packet[packetPtr++] = 0x00;
-  packet[packetPtr++] = RGB_CCT;
-  packet[packetPtr++] = deviceId >> 8;
-  packet[packetPtr++] = deviceId & 0xFF;
-  packet[packetPtr++] = command;
-  packet[packetPtr++] = arg;
-  packet[packetPtr++] = sequenceNum;
-  packet[packetPtr++] = group;
-  packet[packetPtr++] = 0;
-  
-  printf("Constructed raw packet: ");
-  for (int i = 0; i < MilightRgbCctConfig.packetLength; i++) {
-    printf("%02X ", packet[i]);
-  }
-  printf("\n");
-  
-  encodeV2Packet(packet);
-  
-  write(MilightRgbCctConfig, 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 if (type == RGB_CCT) {
-    writeRgbCct(
-      deviceId,
-      RGB_CCT_ON,
-      0xC0 + groupId + (status == OFF ? 0x05 : 0x00),
-      groupId
-    );
-  } 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(const MiLightRadioType type, const uint16_t deviceId, const uint8_t groupId) {
-  if (type == RGBW || type == CCT) {
-    updateStatus(type, deviceId, groupId, ON);
-  } else if (type == RGB_CCT) {
-    updateStatus(type, deviceId, groupId, ON);
+void MiLightClient::pair() {
+  for (size_t i = 0; i < 5; i++) {
+    formatter->updateStatus(ON);
+    flushPacket();
     delay(1);
-    updateStatus(type, deviceId, groupId, ON);
   }
 }
 
-void MiLightClient::unpair(const MiLightRadioType type, const uint16_t deviceId, const uint8_t groupId) {
+void MiLightClient::unpair() {
+  MiLightRadioType type = currentRadio->config.type;
+  
   if (type == RGBW) {
-    updateStatus(type, deviceId, groupId, ON);
-    delay(1);
-    updateColorWhite(deviceId, groupId);
+    formatter->updateStatus(ON);
+    flushPacket();
+    yield();
+    formatter->updateColorWhite();
+    flushPacket();
   } else if (type == CCT) {
     for (int i = 0; i < 5; i++) {
-      updateStatus(type, deviceId, groupId, ON);
+      formatter->updateStatus(ON);
+      flushPacket();
       delay(1);
     }
   } else if (type == RGB_CCT) {
     for (int i = 0; i < 5; i++) {
-      updateStatus(type, deviceId, 0, ON);
+      formatter->updateStatus(ON, 0);
+      flushPacket();
       delay(1);
     }
   }
 }
     
-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->increaseTemperature();
+  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);
-  } else if (type == RGB_CCT) {
-    updateStatus(RGB_CCT, deviceId, 0, ON);
-  }
-}
-
-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);
-  } else if (type == RGB_CCT) {
-    updateStatus(RGB_CCT, deviceId, 0, OFF);
-  }
+void MiLightClient::decreaseBrightness() {
+  formatter->increaseTemperature();
+  flushPacket();
 }
 
-void MiLightClient::increaseCctBrightness(const uint16_t deviceId, const uint8_t groupId) {
-  writeCct(deviceId, groupId, CCT_BRIGHTNESS_UP);
+void MiLightClient::increaseTemperature() {
+  formatter->increaseTemperature();
+  flushPacket();
 }
 
-void MiLightClient::decreaseCctBrightness(const uint16_t deviceId, const uint8_t groupId) {
-  writeCct(deviceId, groupId, CCT_BRIGHTNESS_DOWN);
+void MiLightClient::decreaseTemperature() {
+  formatter->decreaseTemperature();
+  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::increaseTemperature(const uint16_t deviceId, const uint8_t groupId) {
-  writeCct(deviceId, groupId, CCT_TEMPERATURE_UP);
-}
-
-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;
-  
-  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 MiLightClient::updateTemperature(const uint8_t temperature) {
+  formatter->updateTemperature(temperature);
+  flushPacket();
 }
 
-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;
-  }
+void MiLightClient::command(uint8_t command, uint8_t arg) {
+  formatter->command(command, arg);
+  flushPacket();
 }
 
-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;
-  }
-}
-    
 void MiLightClient::formatPacket(MiLightRadioConfig& config, uint8_t* packet, char* buffer) {
   if (config.type == RGBW || config.type == CCT) {
     String format = String("Request type  : %02X\n") 
@@ -382,41 +193,8 @@ void MiLightClient::formatPacket(MiLightRadioConfig& config, uint8_t* packet, ch
     sprintf(buffer, "\n\n");
   }
 }
-
-uint8_t MiLightClient::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 MiLightClient::encodeByte(uint8_t byte, uint8_t s1, uint8_t xorKey, uint8_t s2) {
-  uint8_t value = (byte + s1) % 0x100;
-  value = value ^ xorKey;
-  value = (value + s2) % 0x100;
-  
-  return value;
+    
+void MiLightClient::flushPacket() {
+  write(formatter->buildPacket());
+  formatter->reset();
 }
-
-void MiLightClient::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]));
-  }
-  
-  packet[8] = encodeByte(sum, 2, key, V2_OFFSET(8, packet[0]));
-  
-  printf("encoded packet: ");
-  for (int i = 0; i < MilightRgbCctConfig.packetLength; i++) {
-    printf("%02X ", packet[i]);
-  }
-  printf("\n");
-}

+ 43 - 95
lib/MiLight/MiLightClient.h

@@ -3,135 +3,83 @@
 #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
-
-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 NUM_RADIOS 3
 
 class MiLightClient {
   public:
-    static uint8_t const V2_OFFSETS[][4];
-    
     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)
     {
-      rgbwRadio = new MiLightRadioStack(rf, MilightRgbwConfig);
-      cctRadio = new MiLightRadioStack(rf, MilightCctConfig);
-      rgbCctRadio = new MiLightRadioStack(rf, MilightRgbCctConfig);
+      size_t ix = 0;
+      radios = new RadioStack*[NUM_RADIOS];
+      radios[ix++] = new RadioStack(rf, MilightRgbwConfig);
+      radios[ix++] = new RadioStack(rf, MilightCctConfig);
+      radios[ix++] = new RadioStack(rf, MilightRgbCctConfig);
+      
+      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 < NUM_RADIOS; 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 MiLightRadioConfig& radioConfig, 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
-    );
-    
-    void writeRgbCct(const uint16_t deviceId,
-      const uint8_t command,
-      const uint8_t arg,
-      const uint8_t group
-    );
+    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);
-    
-    static uint8_t getCctStatusButton(uint8_t groupId, MiLightStatus status);
-    static MiLightRadioType getRadioType(const String& typeName);
-    static const MiLightRadioConfig& getRadioConfig(const String& typeName);
+    void updateSaturation(const uint8_t saturation);
     
     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;
     
-    uint8_t sequenceNum;
-    uint8_t nextSequenceNum();
     unsigned int resendCount;
     
-    static void encodeV2Packet(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);
+    MiLightRadio* switchRadio(const MiLightRadioType type);
+    void flushPacket();
 };
 
 #endif

+ 13 - 0
lib/MiLight/MiLightRadioConfig.cpp

@@ -0,0 +1,13 @@
+#include <MiLightRadioConfig.h>
+
+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;
+  }
+  
+  return NULL;
+}

+ 13 - 10
lib/MiLight/MiLightRadioConfig.h

@@ -1,15 +1,13 @@
 #include <Arduino.h>
+#include <PacketFormatter.h>
+#include <RgbCctPacketFormatter.h>
+#include <RgbwPacketFormatter.h>
+#include <CctPacketFormatter.h>
+#include <MiLightButtons.h>
 
 #ifndef _MILIGHT_RADIO_CONFIG
 #define _MILIGHT_RADIO_CONFIG 
 
-enum MiLightRadioType {
-  UNKNOWN = 0,
-  RGBW  = 0xB8,
-  CCT   = 0x5A,
-  RGB_CCT = 0x20
-};
-
 class MiLightRadioConfig {
 public:
   MiLightRadioConfig(const uint16_t syncword0,
@@ -17,12 +15,14 @@ public:
   const size_t packetLength,
   const uint8_t* channels,
   const size_t numChannels,
+  PacketFormatter* packetFormatter,
   const MiLightRadioType type) 
     : syncword0(syncword0),
       syncword3(syncword3),
       packetLength(packetLength),
       channels(channels),
       numChannels(numChannels),
+      packetFormatter(packetFormatter),
       type(type)
   {}
     
@@ -31,22 +31,25 @@ public:
   const size_t packetLength;
   const uint8_t* channels;
   const size_t numChannels;
+  PacketFormatter* packetFormatter;
   const MiLightRadioType type;
+  
+  static MiLightRadioConfig* fromString(const String& s);
 };
 
 const uint8_t RGBW_CHANNELS[] = {9, 40, 71};
 static MiLightRadioConfig MilightRgbwConfig(
-  0x147A, 0x258B, 7, RGBW_CHANNELS, 3, RGBW
+  0x147A, 0x258B, 7, RGBW_CHANNELS, 3, new RgbwPacketFormatter(8), RGBW
 );
 
 const uint8_t CCT_CHANNELS[] = {4, 39, 74};
 static MiLightRadioConfig MilightCctConfig(
-  0x050A, 0x55AA, 7, CCT_CHANNELS, 3, CCT
+  0x050A, 0x55AA, 7, CCT_CHANNELS, 3, new CctPacketFormatter(8), CCT
 );
 
 const uint8_t RGBCCT_CHANNELS[] = {70, 39, 8};
 static MiLightRadioConfig MilightRgbCctConfig(
-  0x7236, 0x1809, 9, RGBCCT_CHANNELS, 3, RGB_CCT
+  0x7236, 0x1809, 9, RGBCCT_CHANNELS, 3, new RgbCctPacketFormatter(9), RGB_CCT
 );
 
 #endif

+ 32 - 24
lib/MiLight/MiLightUdpServer.cpp

@@ -46,84 +46,92 @@ void MiLightUdpServer::handleCommand(uint8_t command, uint8_t commandArg) {
     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);
+    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->updateColorWhite(deviceId, groupId);
+    client->prepare(MilightRgbwConfig, deviceId, groupId);
+    client->updateColorWhite();
     this->lastGroup = groupId;
   } else if (uint8_t cctGroup = cctCommandIdToGroup(command)) {
-    client->updateStatus(
-      CCT,
-      deviceId,
-      cctGroup,
-      cctCommandToStatus(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->allOn(RGBW, deviceId);
+        client->updateStatus(ON, 0);
         break;
       
       case UDP_RGBW_ALL_OFF:
-        client->allOff(RGBW, deviceId);
+        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(deviceId, this->lastGroup, 0xFF-(commandArg + 0x35));
+        client->updateColorRaw(0xFF-(commandArg + 0x35));
         break;
         
       case UDP_RGBW_DISCO_MODE:
-        pressButton(this->lastGroup, RGBW_DISCO_MODE);
+        pressButton(RGBW_DISCO_MODE);
         break;
         
       case UDP_RGBW_SPEED_DOWN:
-        pressButton(this->lastGroup, RGBW_SPEED_DOWN);
+        pressButton(RGBW_SPEED_DOWN);
         break;
         
       case UDP_RGBW_SPEED_UP:
-        pressButton(this->lastGroup, RGBW_SPEED_UP);
+        pressButton(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;
         
+      default:
+        handled = false;
+    }
+    
+    client->prepare(MilightCctConfig);
+    
+    switch(command) {
       case UDP_CCT_BRIGHTNESS_DOWN:
-        client->decreaseCctBrightness(deviceId, this->lastGroup);
+        client->decreaseBrightness();
         break;
         
       case UDP_CCT_BRIGHTNESS_UP:
-        client->increaseCctBrightness(deviceId, this->lastGroup);
+        client->increaseBrightness();
         break;
         
       case UDP_CCT_TEMPERATURE_DOWN:
-        client->decreaseTemperature(deviceId, this->lastGroup);
+        client->decreaseTemperature();
         break;
         
       case UDP_CCT_TEMPERATURE_UP:
-        client->increaseTemperature(deviceId, this->lastGroup);
+        client->increaseTemperature();
         break;
         
       default:
-        Serial.print("MiLightUdpServer - Unhandled command: ");
-        Serial.println(command);
+        if (!handled) {
+          Serial.print("MiLightUdpServer - Unhandled command: ");
+          Serial.println(command);
+        }
     }
   }
 }
 
-void MiLightUdpServer::pressButton(uint8_t group, uint8_t button) {
-  client->writeRgbw(deviceId, 0, 0, group, button);
+void MiLightUdpServer::pressButton(uint8_t button) {
+  client->command(button, 0);
 }  
 
 uint8_t MiLightUdpServer::cctCommandIdToGroup(uint8_t command) {

+ 1 - 1
lib/MiLight/MiLightUdpServer.h

@@ -67,7 +67,7 @@ protected:
   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);
 };

+ 22 - 0
lib/MiLight/PacketFormatter.cpp

@@ -0,0 +1,22 @@
+#include <PacketFormatter.h>
+
+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::updateTemperature(uint8_t value) { }
+void PacketFormatter::updateSaturation(uint8_t value) { }

+ 16 - 7
lib/MiLight/PacketFormatter.h

@@ -16,19 +16,22 @@ public:
   }
   
   // all
-  virtual void updateStatus(MiLightStatus status);
+  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();
   
   // rgbw, rgb+cct
-  virtual void updateHue(uint8_t value);
+  virtual void updateHue(uint16_t value);
   virtual void updateColorRaw(uint8_t value);
+  virtual void updateColorWhite();
   
   // cct 
-  virtual void increaseTemperature(uint8_t value);
-  virtual void decreaseTemperature(uint8_t value);
+  virtual void increaseTemperature();
+  virtual void decreaseTemperature();
   
   // rgb+cct
   virtual void updateTemperature(uint8_t value);
@@ -36,7 +39,12 @@ public:
   
   virtual void reset() = 0;
   
-  uint8_t const* buildPacket() {
+  virtual uint8_t* buildPacket() {
+    printf("Packet: ");
+    for(size_t i = 0; i < 7; i++) {
+      printf("%02X ", this->packet[i]);
+    }
+    printf("\n");
     return this->packet;
   }
   
@@ -46,8 +54,9 @@ public:
     reset();
   }
   
-  static uint8_t rescale(uint8_t value, uint8_t max) {
-    return round(value * (max / 255.0));
+  template <typename T>
+  static T rescale(T value, uint8_t newMax, float oldMax = 255.0) {
+    return round(value * (newMax / oldMax));
   }
   
 protected:

+ 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, 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;
+  }
+  
+  MiLightRadioConfig& config;
+  
+private:
+  PL1167_nRF24 *nrf;
+  MiLightRadio *radio;
+};
+
+#endif

+ 86 - 6
lib/MiLight/RgbCctPacketFormatter.cpp

@@ -1,10 +1,23 @@
 #include <RgbCctPacketFormatter.h>
 
+#define V2_OFFSET(byte, key) ( V2_OFFSETS[byte-1][key%4] )
+
+uint8_t const RgbCctPacketFormatter::V2_OFFSETS[][4] = {
+  { 0x45, 0x1F, 0x14, 0x5C },
+  { 0xAB, 0x49, 0x63, 0x91 },
+  { 0x2D, 0x1F, 0x4A, 0xEB },
+  { 0xAF, 0x03, 0x1D, 0xF3 },
+  { 0x5A, 0x22, 0x30, 0x11 },
+  { 0x04, 0xD8, 0x71, 0x42 },
+  { 0xAF, 0x04, 0xDD, 0x07 },
+  { 0xE1, 0x93, 0xB8, 0xE4 }
+};
+
 void RgbCctPacketFormatter::reset() {
   size_t packetPtr = 0;
   
   packet[packetPtr++] = 0x00;
-  packet[packetPtr++] = RGB_CCT;
+  packet[packetPtr++] = 0x20;
   packet[packetPtr++] = deviceId >> 8;
   packet[packetPtr++] = deviceId & 0xFF;
   packet[packetPtr++] = 0;
@@ -13,13 +26,80 @@ void RgbCctPacketFormatter::reset() {
   packet[packetPtr++] = groupId;
   packet[packetPtr++] = 0;
 }
+  
+void RgbCctPacketFormatter::command(uint8_t command, uint8_t arg) {
+  packet[RGB_CCT_COMMAND_INDEX] = command;
+  packet[RGB_CCT_ARGUMENT_INDEX] = arg;
+}
 
-void RgbCctPacketFormatter::updateStatus(MiLightStatus status) {
-  packet[RGB_CCT_COMMAND_INDEX] = RGB_CCT_ON;
-  packet[RGB_CCT_ARGUMENT_INDEX] = 0xC0 + groupId + (status == OFF ? 5 : 0);
+void RgbCctPacketFormatter::updateStatus(MiLightStatus status, uint8_t groupId) {
+  command(RGB_CCT_ON, 0xC0 + groupId + (status == OFF ? 5 : 0));
 }
 
 void RgbCctPacketFormatter::updateBrightness(uint8_t brightness) {
-  packet[RGB_CCT_COMMAND_INDEX] = RGB_CCT_BRIGHTNESS;
-  packet[RGB_CCT_ARGUMENT_INDEX] = 0x4F + rescale(brightness, 0x60);
+  command(RGB_CCT_BRIGHTNESS, 0x4F + brightness);
+}
+  
+void RgbCctPacketFormatter::updateHue(uint16_t value) {
+  const int16_t remappedColor = (value + 20) % 360;
+  updateColorRaw(rescale(remappedColor, 255, 360));
+}
+
+void RgbCctPacketFormatter::updateColorRaw(uint8_t value) {
+  command(RGB_CCT_COLOR, 0x15 + value);
+}
+  
+void RgbCctPacketFormatter::updateTemperature(uint8_t value) {
+  command(RGB_CCT_KELVIN, (0x4C + value)*2);
+}
+
+void RgbCctPacketFormatter::updateSaturation(uint8_t value) {
+  command(RGB_CCT_SATURATION, value - 0x33);
+}
+  
+void RgbCctPacketFormatter::updateColorWhite() {
+  updateTemperature(0);
+}
+  
+uint8_t* RgbCctPacketFormatter::buildPacket() {
+  encodeV2Packet(packet);
+  return 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::encodeByte(uint8_t byte, uint8_t s1, uint8_t xorKey, uint8_t s2) {
+  uint8_t value = (byte + s1) % 0x100;
+  value = value ^ xorKey;
+  value = (value + s2) % 0x100;
+  
+  return value;
+}
+
+void RgbCctPacketFormatter::encodeV2Packet(uint8_t *packet) {
+  uint8_t key = xorKey(packet[0]);
+  uint8_t sum = key;
+  
+  printf("Packet: ");
+  for (size_t i = 0; i < 9; i++) {
+    printf("%02X ", packet[i]);
+  }
+  printf("\n");
+  
+  for (size_t i = 1; i <= 7; i++) {
+    sum += packet[i];
+    packet[i] = encodeByte(packet[i], 0, key, V2_OFFSET(i, packet[0]));
+  }
+  
+  packet[8] = encodeByte(sum, 2, key, V2_OFFSET(8, packet[0]));
 }

+ 17 - 4
lib/MiLight/RgbCctPacketFormatter.h

@@ -1,5 +1,4 @@
 #include <PacketFormatter.h>
-#include <MiLightRadioConfig.h>
 
 #define RGB_CCT_COMMAND_INDEX 4
 #define RGB_CCT_ARGUMENT_INDEX 5
@@ -9,14 +8,28 @@
 
 class RgbCctPacketFormatter : public PacketFormatter {
 public:
-  RgbCctPacketFormatter()
-    : PacketFormatter(MilightRgbCctConfig.packetLength)
+  static uint8_t const V2_OFFSETS[][4];
+    
+  RgbCctPacketFormatter(size_t packetLength)
+    : PacketFormatter(packetLength)
   { }
   
   virtual void reset();
   
-  virtual void updateStatus(MiLightStatus status);
+  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 uint8_t* buildPacket();
+    
+  static void encodeV2Packet(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);
 };
 
 #endif

+ 52 - 0
lib/MiLight/RgbwPacketFormatter.cpp

@@ -0,0 +1,52 @@
+#include <RgbwPacketFormatter.h>
+#include <MiLightButtons.h>
+
+void RgbwPacketFormatter::reset() {
+  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::updateStatus(MiLightStatus status, uint8_t groupId) {
+  uint8_t button = RGBW_GROUP_1_ON + ((groupId - 1)*2) + status;
+  packet[RGBW_COMMAND_INDEX] = button;
+}
+  
+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);
+  packet[RGBW_BRIGHTNESS_GROUP_INDEX] |= (packetBrightnessValue << 3);
+}
+
+void RgbwPacketFormatter::command(uint8_t command, uint8_t arg) {
+  packet[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) {
+  packet[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);
+}

+ 26 - 0
lib/MiLight/RgbwPacketFormatter.h

@@ -0,0 +1,26 @@
+#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(size_t packetLength)
+    : PacketFormatter(packetLength)
+  { }
+  
+  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 reset();
+};
+
+#endif

+ 72 - 57
lib/WebServer/MiLightHttpServer.cpp

@@ -130,29 +130,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->packetLength];
+  milightClient->read(packet);
   
   String response = "Packet received (";
   response += String(sizeof(packet)) + " bytes)";
   response += ":\n";
   
   char ppBuffer[200];
-  milightClient->formatPacket(config, packet, ppBuffer);
+  milightClient->formatPacket(*config, packet, ppBuffer);
   response += String(ppBuffer);
   
   response += "\n\n";
@@ -171,9 +181,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");
     
@@ -184,69 +194,64 @@ 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();
     }
+      
+    // CCT command work more effectively with a lower number of repeats it seems.
+    milightClient->setResendCount(MILIGHT_DEFAULT_RESEND_COUNT);
     
-    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();
     }
+
+    milightClient->setResendCount(settings.packetRepeats);
+  }
+  
+  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);
   
@@ -258,9 +263,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");
     
@@ -268,13 +273,13 @@ 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);
     }
   }
   
@@ -284,19 +289,29 @@ void MiLightHttpServer::handleUpdateGateway(const UrlTokenBindings* urlBindings)
 void MiLightHttpServer::handleSendRaw(const UrlTokenBindings* bindings) {
   DynamicJsonBuffer buffer;
   JsonObject& request = buffer.parse(server.arg("plain"));
-  MiLightRadioConfig config = milightClient->getRadioConfig(bindings->get("type"));
+  MiLightRadioConfig* config = MiLightRadioConfig::fromString(bindings->get("type"));
   
-  uint8_t packet[config.packetLength];
+  if (config == NULL) {
+    String body = "Unknown device type: ";
+    body += bindings->get("type");
+    
+    server.send(400, "text/plain", body);
+    return;
+  }
+  
+  uint8_t packet[config->packetLength];
   const String& hexPacket = request["packet"];
-  hexStrToBytes<uint8_t>(hexPacket.c_str(), hexPacket.length(), packet, config.packetLength);
+  hexStrToBytes<uint8_t>(hexPacket.c_str(), hexPacket.length(), packet, config->packetLength);
   
   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->getRadio(config.type)->write(packet, config.packetLength);
+    milightClient->write(packet);
   }
   
   server.send(200, "text/plain", "true");

+ 10 - 10
platformio.ini

@@ -22,13 +22,13 @@ platform = espressif8266
 framework = arduino
 board = nodemcuv2
 
-[env:d1_mini]
-platform = espressif8266
-framework = arduino
-board = d1_mini
-
-[env:esp12]
-platform = espressif8266
-board = esp12e
-framework = arduino
-build_flags = -Wl,-Tesp8266.flash.4m1m.ld
+; [env:d1_mini]
+; platform = espressif8266
+; framework = arduino
+; board = d1_mini
+; 
+; [env:esp12]
+; platform = espressif8266
+; board = esp12e
+; framework = arduino
+; build_flags = -Wl,-Tesp8266.flash.4m1m.ld

+ 208 - 81
scripts/tests.rb

@@ -1,94 +1,221 @@
-require 'pp'
-require 'set'
+require_relative 'helpers'
 
-CAPTURES_DIR = File.expand_path(File.join(__FILE__, "../../packet_captures"))
+COLOR_S2_BASES = [0x5A, 0x22, 0x30, 0x11]
+KELVIN_S2_BASES = COLOR_S2_BASES.map { |x| x + 0x80 }
+SATURATION_BRIGHTNESS_S2_BASES = [0x9A, 0x62, 0x70, 0x51]
 
-def first_byte_determines_next(capture, count)
-  capture
-    .group_by { |x| x[0] }
-    .map { |_, x| x.map { |packet| packet[1..count] } }
-    .map(&:uniq)
-    .map(&:length)
-    .all? { |x| x == 1 }
-end
+# saturation bases + 0x80
+COMMAND_BASES = [0x1A, 0xE2, 0xF0, 0xD1]
+
+GROUP_BASES = [0xAF, 0x04, 0xDD, 0x07]
 
-def parse_capture(filename)
-  File
-    .read(filename)
-    .split("\n")
-    .map { |x| x.split(' ').map { |b| b.to_i(16) } }
+OFFSETTING_S2_FN = ->(bases, s, e) do
+  ->(b0) do
+    value = bases[ (b0 % 4) ]
+    if b0 >= s && b0 <= e
+      value = (value + 0x80) % 0x100
+    end
+    
+    [value]
+  end
 end
 
-Dir.glob("#{CAPTURES_DIR}/**.txt").each do |filename|
-  puts File.basename(filename)
-  capture = parse_capture(filename)
-  
-  pp first_byte_determines_next(capture, 3)
-  
-  # a = capture
-  #   .group_by { |x| x[0] }
-  #   .sort
-  #   .map { |_, packets| [packets.first[0], packets.first[1]] }
-  # mappings = Hash[a]
-  # perms = []
-  # current_perm = []
-  # elem = a.first
-  # current_perm = [elem[0]]
-  # visited = Set.new([elem[0]])
-  # perm_visited = Set.new([elem[0]])
-  # 
-  # while visited.length < mappings.length
-  #   new_elem = nil
-  #   
-  #   puts visited.inspect
-  #   
-  #   if !mappings.include?(elem[1]) || perm_visited.include?(mappings[elem[1]])
-  #     new_elem = (Set.new(mappings.keys) - visited).first
-  #     perms << current_perm
-  #     current_perm = []
-  #     perm_visited = Set.new
-  #   else
-  #     new_elem = [elem[1], mappings[elem[1]]]
-  #   end
-  #   
-  #   visited << new_elem[0]
-  #   perm_visited << new_elem[0]
-  #   elem = new_elem
-  # end
-  # 
-  # perms << current_perm
-  # 
-  # pp perms
+extra_args = {
+  kelvin: {
+    increments: 2,
+    s1_range: [0x0C],
+    s2_range: OFFSETTING_S2_FN.call(KELVIN_S2_BASES, 0x14, 0x93)
+  },
+  color: {
+    s1_range: [0x15],
+    s2_range: OFFSETTING_S2_FN.call(COLOR_S2_BASES, 0x14, 0x93)
+  },
+  brightness: {
+    s1_range: [0x0F],
+    s2_range: OFFSETTING_S2_FN.call(SATURATION_BRIGHTNESS_S2_BASES, 0x54, 0xD3)
+  },
+  saturation: {
+    invert_key: true,
+    s1_range: [0x0E],
+    s2_range: OFFSETTING_S2_FN.call(SATURATION_BRIGHTNESS_S2_BASES, 0x54, 0xD3)
+  },
+  on_seq: {
+    s1_range: [0x00],
+  },
+  group: {
+    s1_range: [0x00],
+    s2_range: OFFSETTING_S2_FN.call(GROUP_BASES, 0x54, 0xD3)
+  },
+  checksum: {
+    s1_range: ->(x) do
+      lsn = (((x & 0xF)^2) + 7) % 0x10
+      
+      (0..0xF).map { |msn| (msn << 4) + lsn }
+    end,
+    s2_range: [ 0xA1, 0x53, 0x78, 0xA4 ]
+  }
+}
+
+type_overrides = {
+  checksum: 'on_seq'
+}
+
+lengths = {
+  color: 256,
+  brightness: 0x64,
+  kelvin: 0x64,
+  saturation: 0x64,
+  on_seq: 30,
+  checksum: 0xFF,
+  group: 40
+}
+
+columns = {
+  on_seq: 6,
+  group: 7,
+  checksum: 8
+}
+
+extract_fns = {
+  group: ->(b0) do
+    get_group_sequence(type: 'on', col: 7, key: b0, group_range: (0..100))
+  end
+}
+
+# PACKET_TYPES = %w(color brightness saturation kelvin on_seq checksum)
+PACKET_TYPES = %w(color brightness kelvin saturation group)
+
+(0x00..0xFF).each do |b0|
+  k = xor_key(b0)
+  results = {}
   
-  a = capture
-   .group_by { |x| x[0] }
-   .sort
-   .map { |_, packets| [packets.first[0], packets.first[1]] }
-   .sort { |x,y| x[1] <=> y[1] }
-  a = a
-   .each_with_index.map do |x, i|
-     [x[1],x[0],if a[i-1][1] == x[1]-1
-       (x[0]-a[i-1][0])
-     else
-       nil
-     end]
-   end
-   .to_a
-  #  # .group_by { |x| x[1] }
-  #  # .sort
-  #  # .to_a
+  PACKET_TYPES.each do |type|
+    type_args = extra_args[type.to_sym] || {}
+    file_type = type_overrides[type.to_sym] || type
+    
+    if extract_fn = extract_fns[type.to_sym]
+      packet_str = extract_fn.call(b0)
+    else
+      packet_str = get_sequence(file_type, b0, columns[type.to_sym] || 5)
+    end
+    
+    max_len = lengths[type.to_sym]*2
+    if packet_str.length > max_len
+      packet_str = packet_str[0...max_len]
+    end
+    
+    args = {}.merge(extra_args[type.to_sym] || {})
+    
+    
+    if args[:s1_range].is_a?(Proc)
+      args[:s1_range] = args[:s1_range].call(b0)
+    end
+    
+    if args[:s2_range].is_a?(Proc)
+      args[:s2_range] = args[:s2_range].call(b0)
+    end
+    
+    args = {
+      seq: packet_str,
+      xor_range: [k]
+    }.merge(args)
+    
+    results[type] = search_sequence(args)
+  end
   
-  pp a
+  num_fns = results.values.map { |x| x[:scramble_fns].length }.max
+  printf "0x%02X", b0
   
-  # a = capture
-  #   .group_by { |x| x[0] }
-  #   .map { |_, packets| packets.first }
-  #   .sort #{ |x, y| x[1] <=> y[1] }
-  #   .map { |x| x }
-  #   .to_a
-  #   
+  (0...num_fns).each do |i|
+    printf "\t\t"
+    
+    PACKET_TYPES.each do |type|
+      result = results[type]
+      
+      fns = result[:scramble_fns]
+      
+      if i < fns.length
+        fn = result[:scramble_fns][i]
+        printf "0x%02X\t0x%02X\t0x%02X\t%d\t", fn[:a], fn[:x], fn[:b], result[:num_misses]
+      else
+        printf "\t\t\t\t"
+      end
+    end
+      
+    printf "\n"
+  end
 end
 
+# Dir.glob("#{CAPTURES_DIR}/**.txt").each do |filename|
+#   puts File.basename(filename)
+#   capture = parse_capture(filename)
+#   
+#   pp first_byte_determines_next(capture, 3)
+#   
+#   # a = capture
+#   #   .group_by { |x| x[0] }
+#   #   .sort
+#   #   .map { |_, packets| [packets.first[0], packets.first[1]] }
+#   # mappings = Hash[a]
+#   # perms = []
+#   # current_perm = []
+#   # elem = a.first
+#   # current_perm = [elem[0]]
+#   # visited = Set.new([elem[0]])
+#   # perm_visited = Set.new([elem[0]])
+#   # 
+#   # while visited.length < mappings.length
+#   #   new_elem = nil
+#   #   
+#   #   puts visited.inspect
+#   #   
+#   #   if !mappings.include?(elem[1]) || perm_visited.include?(mappings[elem[1]])
+#   #     new_elem = (Set.new(mappings.keys) - visited).first
+#   #     perms << current_perm
+#   #     current_perm = []
+#   #     perm_visited = Set.new
+#   #   else
+#   #     new_elem = [elem[1], mappings[elem[1]]]
+#   #   end
+#   #   
+#   #   visited << new_elem[0]
+#   #   perm_visited << new_elem[0]
+#   #   elem = new_elem
+#   # end
+#   # 
+#   # perms << current_perm
+#   # 
+#   # pp perms
+#   
+#   a = capture
+#    .group_by { |x| x[0] }
+#    .sort
+#    .map { |_, packets| [packets.first[0], packets.first[1]] }
+#    .sort { |x,y| x[1] <=> y[1] }
+#   a = a
+#    .each_with_index.map do |x, i|
+#      [x[1],x[0],if a[i-1][1] == x[1]-1
+#        (x[0]-a[i-1][0])
+#      else
+#        nil
+#      end]
+#    end
+#    .to_a
+#   #  # .group_by { |x| x[1] }
+#   #  # .sort
+#   #  # .to_a
+#   
+#   pp a
+#   
+#   # a = capture
+#   #   .group_by { |x| x[0] }
+#   #   .map { |_, packets| packets.first }
+#   #   .sort #{ |x, y| x[1] <=> y[1] }
+#   #   .map { |x| x }
+#   #   .to_a
+#   #   
+# end
+
 # on1 = parse_capture("#{CAPTURES_DIR}/rgbwcct_group1_on.txt")
 # on2 = parse_capture("#{CAPTURES_DIR}/rgbwcct_group2_on.txt")
 # 

+ 4 - 0
src/main.cpp

@@ -57,7 +57,9 @@ void initMilightClient() {
 }
 
 void applySettings() {
+  Serial.println(" Init client");
   initMilightClient();
+  Serial.println(" Init UDP servers");
   initMilightUdpServers();
 }
 
@@ -66,7 +68,9 @@ void setup() {
   wifiManager.autoConnect();
   SPIFFS.begin();
   Settings::load(settings);
+  Serial.println("Applying settings");
   applySettings();
+  Serial.println("Done");
   
   httpServer = new MiLightHttpServer(settings, milightClient);
   httpServer->onSettingsSaved(applySettings);

+ 28 - 8
web/index.html

@@ -230,10 +230,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();
         }
       });
     };
@@ -440,12 +440,15 @@
           <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>
         </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">
       <div class="row">
         <div class="col-sm-12">
           <h5>Hue</h5>
@@ -462,8 +465,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 +493,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 +528,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>