浏览代码

Merge branch 'v1.6.0' of github.com:sidoh/esp8266_milight_hub into v1.6.0

Chris Mullins 8 年之前
父节点
当前提交
bb282249dd
共有 48 个文件被更改,包括 891 次插入518 次删除
  1. 1 0
      .gitignore
  2. 1 1
      .travis.yml
  3. 1 1
      README.md
  4. 2 2
      dist/index.html.gz.h
  5. 6 1
      lib/Helpers/Size.h
  6. 6 5
      lib/MQTT/MqttClient.cpp
  7. 2 1
      lib/MQTT/MqttClient.h
  8. 1 1
      lib/MiLight/CctPacketFormatter.cpp
  9. 97 0
      lib/MiLight/FUT089PacketFormatter.cpp
  10. 45 0
      lib/MiLight/FUT089PacketFormatter.h
  11. 73 68
      lib/MiLight/MiLightClient.cpp
  12. 12 9
      lib/MiLight/MiLightClient.h
  13. 64 0
      lib/MiLight/MiLightRemoteConfig.cpp
  14. 75 0
      lib/MiLight/MiLightRemoteConfig.h
  15. 4 0
      lib/MiLight/PacketFormatter.cpp
  16. 4 2
      lib/MiLight/PacketFormatter.h
  17. 7 134
      lib/MiLight/RgbCctPacketFormatter.cpp
  18. 6 25
      lib/MiLight/RgbCctPacketFormatter.h
  19. 1 1
      lib/MiLight/RgbPacketFormatter.cpp
  20. 3 3
      lib/MiLight/RgbwPacketFormatter.cpp
  21. 2 0
      lib/MiLight/RgbwPacketFormatter.h
  22. 82 0
      lib/MiLight/V2PacketFormatter.cpp
  23. 34 0
      lib/MiLight/V2PacketFormatter.h
  24. 66 0
      lib/MiLight/V2RFEncoding.cpp
  25. 21 0
      lib/MiLight/V2RFEncoding.h
  26. 48 0
      lib/MiLightState/GroupState.h
  27. 15 0
      lib/MiLightState/GroupStateStore.h
  28. 46 106
      lib/Radio/LT8900MiLightRadio.cpp
  29. 1 1
      lib/Radio/LT8900MiLightRadio.h
  30. 12 11
      lib/Radio/MiLightButtons.h
  31. 5 29
      lib/Radio/MiLightRadioConfig.cpp
  32. 14 40
      lib/Radio/MiLightRadioConfig.h
  33. 1 1
      lib/Radio/MiLightRadioFactory.h
  34. 1 1
      lib/Radio/NRF24MiLightRadio.cpp
  35. 5 5
      lib/Udp/V5MiLightUdpServer.cpp
  36. 1 1
      lib/Udp/V6CctCommandHandler.h
  37. 1 1
      lib/Udp/V6ComamndHandler.cpp
  38. 4 4
      lib/Udp/V6CommandHandler.h
  39. 1 1
      lib/Udp/V6RgbCctCommandHandler.h
  40. 1 1
      lib/Udp/V6RgbCommandHandler.h
  41. 1 1
      lib/Udp/V6RgbwCommandHandler.h
  42. 39 36
      lib/WebServer/MiLightHttpServer.cpp
  43. 1 1
      lib/WebServer/MiLightHttpServer.h
  44. 3 3
      platformio.ini
  45. 25 10
      src/main.cpp
  46. 3 1
      web/src/css/style.css
  47. 37 10
      web/src/index.html
  48. 10 0
      web/src/js/script.js

+ 1 - 0
.gitignore

@@ -4,3 +4,4 @@
 .gcc-flags.json
 /web/node_modules
 /web/build
+/dist/*.bin

文件差异内容过多而无法显示
+ 1 - 1
.travis.yml


+ 1 - 1
README.md

@@ -20,7 +20,7 @@ Support has been added for the following [bulb types](http://futlight.com/produc
 1. RGBW bulbs: FUT014, FUT016, FUT103
 1. Dual-White (CCT) bulbs: FUT019
 1. RGB LED strips: FUT025
-1. RGB + Dual White (RGB+CCT) bulbs: FUT015
+1. RGB + Dual White (RGB+CCT) bulbs: FUT015, FUT105
 
 Other bulb types might work, but have not been tested. It is also relatively easy to add support for new bulb types.
 

文件差异内容过多而无法显示
+ 2 - 2
dist/index.html.gz.h


+ 6 - 1
lib/Helpers/Size.h

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

+ 6 - 5
lib/MQTT/MqttClient.cpp

@@ -4,6 +4,7 @@
 #include <IntParsing.h>
 #include <ArduinoJson.h>
 #include <WiFiClient.h>
+#include <MiLightRadioConfig.h>
 
 MqttClient::MqttClient(Settings& settings, MiLightClient*& milightClient)
   : milightClient(milightClient),
@@ -85,7 +86,7 @@ void MqttClient::handleClient() {
   mqttClient->loop();
 }
 
-void MqttClient::sendUpdate(MiLightRadioType type, uint16_t deviceId, uint16_t groupId, const char* update) {
+void MqttClient::sendUpdate(const MiLightRemoteConfig& remoteConfig, uint16_t deviceId, uint16_t groupId, const char* update) {
   String topic = settings.mqttUpdateTopicPattern;
 
   if (topic.length() == 0) {
@@ -97,7 +98,7 @@ void MqttClient::sendUpdate(MiLightRadioType type, uint16_t deviceId, uint16_t g
 
   topic.replace(":device_id", String("0x") + deviceIdStr);
   topic.replace(":group_id", String(groupId));
-  topic.replace(":device_type", MiLightRadioConfig::fromType(type)->name);
+  topic.replace(":device_type", remoteConfig.name);
 
 #ifdef MQTT_DEBUG
   printf_P(PSTR("MqttClient - publishing update to %s: %s\n"), topic.c_str(), update);
@@ -123,7 +124,7 @@ void MqttClient::subscribe() {
 void MqttClient::publishCallback(char* topic, byte* payload, int length) {
   uint16_t deviceId = 0;
   uint8_t groupId = 0;
-  MiLightRadioConfig* config = &MilightRgbCctConfig;
+  const MiLightRemoteConfig* config = &FUT092Config;
   char cstrPayload[length + 1];
   cstrPayload[length] = 0;
   memcpy(cstrPayload, payload, sizeof(byte)*length);
@@ -148,7 +149,7 @@ void MqttClient::publishCallback(char* topic, byte* payload, int length) {
   }
 
   if (tokenBindings.hasBinding("device_type")) {
-    config = MiLightRadioConfig::fromString(tokenBindings.get("device_type"));
+    config = MiLightRemoteConfig::fromType(tokenBindings.get("device_type"));
   }
 
   StaticJsonBuffer<400> buffer;
@@ -158,6 +159,6 @@ void MqttClient::publishCallback(char* topic, byte* payload, int length) {
   printf_P(PSTR("MqttClient - device %04X, group %u\n"), deviceId, groupId);
 #endif
 
-  milightClient->prepare(*config, deviceId, groupId);
+  milightClient->prepare(config, deviceId, groupId);
   milightClient->update(obj);
 }

+ 2 - 1
lib/MQTT/MqttClient.h

@@ -2,6 +2,7 @@
 #include <Settings.h>
 #include <PubSubClient.h>
 #include <WiFiClient.h>
+#include <MiLightRadioConfig.h>
 
 #ifndef MQTT_CONNECTION_ATTEMPT_FREQUENCY
 #define MQTT_CONNECTION_ATTEMPT_FREQUENCY 5000
@@ -18,7 +19,7 @@ public:
   void begin();
   void handleClient();
   void reconnect();
-  void sendUpdate(MiLightRadioType type, uint16_t deviceId, uint16_t groupId, const char* update);
+  void sendUpdate(const MiLightRemoteConfig& remoteConfig, uint16_t deviceId, uint16_t groupId, const char* update);
 
 private:
   WiFiClient tcpClient;

+ 1 - 1
lib/MiLight/CctPacketFormatter.cpp

@@ -4,7 +4,7 @@
 void CctPacketFormatter::initializePacket(uint8_t* packet) {
   size_t packetPtr = 0;
 
-  packet[packetPtr++] = CCT;
+  packet[packetPtr++] = 0x5A;
   packet[packetPtr++] = deviceId >> 8;
   packet[packetPtr++] = deviceId & 0xFF;
   packet[packetPtr++] = groupId;

+ 97 - 0
lib/MiLight/FUT089PacketFormatter.cpp

@@ -0,0 +1,97 @@
+#include <FUT089PacketFormatter.h>
+#include <V2RFEncoding.h>
+#include <Units.h>
+
+void FUT089PacketFormatter::modeSpeedDown() {
+  command(FUT089_ON, FUT089_MODE_SPEED_DOWN);
+}
+
+void FUT089PacketFormatter::modeSpeedUp() {
+  command(FUT089_ON, FUT089_MODE_SPEED_UP);
+}
+
+void FUT089PacketFormatter::updateMode(uint8_t mode) {
+  command(FUT089_MODE, mode);
+}
+
+void FUT089PacketFormatter::updateBrightness(uint8_t brightness) {
+  command(FUT089_BRIGHTNESS, brightness);
+}
+
+void FUT089PacketFormatter::updateHue(uint16_t value) {
+  uint8_t remapped = Units::rescale(value, 255, 360);
+  updateColorRaw(remapped);
+}
+
+void FUT089PacketFormatter::updateColorRaw(uint8_t value) {
+  command(FUT089_COLOR, FUT089_COLOR_OFFSET + value);
+}
+
+void FUT089PacketFormatter::updateTemperature(uint8_t value) {
+  updateColorWhite();
+  command(FUT089_KELVIN, 100 - value);
+}
+
+void FUT089PacketFormatter::updateSaturation(uint8_t value) {
+  command(FUT089_SATURATION, value);
+}
+
+void FUT089PacketFormatter::updateColorWhite() {
+  command(FUT089_ON, FUT089_WHITE_MODE);
+}
+
+void FUT089PacketFormatter::enableNightMode() {
+  uint8_t arg = groupCommandArg(OFF, groupId);
+  command(FUT089_ON | 0x80, arg);
+}
+
+void FUT089PacketFormatter::parsePacket(const uint8_t *packet, JsonObject& result) {
+  uint8_t packetCopy[V2_PACKET_LEN];
+  memcpy(packetCopy, packet, V2_PACKET_LEN);
+  V2RFEncoding::decodeV2Packet(packetCopy);
+
+  result["device_id"] = (packetCopy[2] << 8) | packetCopy[3];
+  result["group_id"] = packetCopy[7];
+  result["device_type"] = "fut089";
+
+  uint8_t command = (packetCopy[V2_COMMAND_INDEX] & 0x7F);
+  uint8_t arg = packetCopy[V2_ARGUMENT_INDEX];
+
+  if (command == FUT089_ON) {
+    if (arg == FUT089_MODE_SPEED_DOWN) {
+      result["command"] = "mode_speed_down";
+    } else if (arg == FUT089_MODE_SPEED_UP) {
+      result["command"] = "mode_speed_up";
+    } else if (arg == FUT089_WHITE_MODE) {
+      result["command"] = "white_mode";
+    } else if (arg <= 8) { // Group is not reliably encoded in group byte. Extract from arg byte
+      result["state"] = "ON";
+      result["group_id"] = arg;
+    } else if (arg >= 9 && arg <= 17) {
+      result["state"] = "OFF";
+      result["group_id"] = arg-9;
+    }
+  } else if (command == FUT089_COLOR) {
+    uint8_t rescaledColor = (arg - FUT089_COLOR_OFFSET) % 0x100;
+    uint16_t hue = Units::rescale<uint16_t, uint16_t>(rescaledColor, 360, 255.0);
+    result["hue"] = hue;
+  } else if (command == FUT089_BRIGHTNESS) {
+    uint8_t level = constrain(arg, 0, 100);
+    result["brightness"] = Units::rescale<uint8_t, uint8_t>(level, 255, 100);
+  // saturation == kelvin. arg ranges are the same, so won't be able to parse
+  // both unless state is persisted
+  // } else if (command == FUT089_SATURATION) {
+  //   result["saturation"] = constrain(arg, 0, 100);
+  // } else if (command == FUT089_KELVIN) {
+  //   result["color_temp"] = Units::whiteValToMireds(arg, 100);
+  } else if (command == FUT089_MODE) {
+    result["mode"] = arg;
+  } else {
+    result["button_id"] = command;
+    result["argument"] = arg;
+  }
+
+  if (! result.containsKey("state")) {
+    result["state"] = "ON";
+  }
+}

+ 45 - 0
lib/MiLight/FUT089PacketFormatter.h

@@ -0,0 +1,45 @@
+#include <V2PacketFormatter.h>
+
+#ifndef _FUT089_PACKET_FORMATTER_H
+#define _FUT089_PACKET_FORMATTER_H
+
+#define FUT089_COLOR_OFFSET 0
+
+enum MiLightFUT089Command {
+  FUT089_ON = 0x01,
+  FUT089_OFF = 0x01,
+  FUT089_COLOR = 0x02,
+  FUT089_BRIGHTNESS = 0x05,
+  FUT089_MODE = 0x06,
+  FUT089_KELVIN = 0x07,
+  FUT089_SATURATION = 0x07
+};
+
+enum MiLightFUT089Arguments {
+  FUT089_MODE_SPEED_UP   = 0x12,
+  FUT089_MODE_SPEED_DOWN = 0x13,
+  FUT089_WHITE_MODE = 0x14
+};
+
+class FUT089PacketFormatter : public V2PacketFormatter {
+public:
+  FUT089PacketFormatter()
+    : V2PacketFormatter(0x25, 8)
+  { }
+
+  virtual void updateBrightness(uint8_t value);
+  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 enableNightMode();
+
+  virtual void modeSpeedDown();
+  virtual void modeSpeedUp();
+  virtual void updateMode(uint8_t mode);
+
+  virtual void parsePacket(const uint8_t* packet, JsonObject& result);
+};
+
+#endif

+ 73 - 68
lib/MiLight/MiLightClient.cpp

@@ -7,13 +7,14 @@
 MiLightClient::MiLightClient(MiLightRadioFactory* radioFactory)
   : resendCount(MILIGHT_DEFAULT_RESEND_COUNT),
     currentRadio(NULL),
+    currentRemote(NULL),
     numRadios(MiLightRadioConfig::NUM_CONFIGS),
     packetSentHandler(NULL)
 {
   radios = new MiLightRadio*[numRadios];
 
   for (size_t i = 0; i < numRadios; i++) {
-    radios[i] = radioFactory->create(*MiLightRadioConfig::ALL_CONFIGS[i]);
+    radios[i] = radioFactory->create(MiLightRadioConfig::ALL_CONFIGS[i]);
   }
 }
 
@@ -22,65 +23,66 @@ void MiLightClient::begin() {
     radios[i]->begin();
   }
 
-  this->currentRadio = radios[0];
-  this->currentRadio->configure();
+  switchRadio(static_cast<size_t>(0));
 }
 
 void MiLightClient::setHeld(bool held) {
-  formatter->setHeld(held);
+  currentRemote->packetFormatter->setHeld(held);
 }
 
-MiLightRadio* MiLightClient::switchRadio(const MiLightRadioType type) {
-  MiLightRadio* radio = NULL;
+size_t MiLightClient::getNumRadios() const {
+  return numRadios;
+}
 
-  for (int i = 0; i < numRadios; i++) {
-    if (this->radios[i]->config().type == type) {
-      radio = radios[i];
-      break;
-    }
+MiLightRadio* MiLightClient::switchRadio(size_t radioIx) {
+  if (radioIx >= getNumRadios()) {
+    return NULL;
   }
 
-  if (radio != NULL) {
-    if (currentRadio != radio) {
-      radio->configure();
-    }
-
-    this->currentRadio = radio;
-    this->formatter = radio->config().packetFormatter;
-
-    return radio;
-  } else {
-    Serial.print(F("MiLightClient - tried to get radio for unknown type: "));
-    Serial.println(type);
+  if (this->currentRadio != radios[radioIx]) {
+    this->currentRadio = radios[radioIx];
+    this->currentRadio->configure();
   }
 
-  return NULL;
+  return this->currentRadio;
 }
 
+MiLightRadio* MiLightClient::switchRadio(const MiLightRemoteConfig* remoteConfig) {
+  MiLightRadio* radio;
 
-void MiLightClient::prepare(MiLightRadioConfig& config,
-  const uint16_t deviceId,
-  const uint8_t groupId) {
+  for (int i = 0; i < numRadios; i++) {
+    if (&this->radios[i]->config() == &remoteConfig->radioConfig) {
+      radio = switchRadio(i);
+      break;
+    }
+  }
 
-  prepare(config.type, deviceId, groupId);
+  return radio;
 }
 
-void MiLightClient::prepare(MiLightRadioType type,
+void MiLightClient::prepare(const MiLightRemoteConfig* config,
   const uint16_t deviceId,
-  const uint8_t groupId) {
-
-  switchRadio(type);
+  const uint8_t groupId
+) {
+  switchRadio(config);
+  this->currentRemote = config;
 
   if (deviceId >= 0 && groupId >= 0) {
-    formatter->prepare(deviceId, groupId);
+    currentRemote->packetFormatter->prepare(deviceId, groupId);
   }
 }
 
+void MiLightClient::prepare(const MiLightRemoteType type,
+  const uint16_t deviceId,
+  const uint8_t groupId
+) {
+  prepare(MiLightRemoteConfig::fromType(type));
+}
+
 void MiLightClient::setResendCount(const unsigned int resendCount) {
   this->resendCount = resendCount;
 }
 
-
 bool MiLightClient::available() {
   if (currentRadio == NULL) {
     return false;
@@ -88,14 +90,16 @@ bool MiLightClient::available() {
 
   return currentRadio->available();
 }
-void MiLightClient::read(uint8_t packet[]) {
+
+size_t MiLightClient::read(uint8_t packet[]) {
   if (currentRadio == NULL) {
-    return;
+    return 0;
   }
 
-  size_t length = currentRadio->config().getPacketLength();
-
+  size_t length;
   currentRadio->read(packet, length);
+
+  return length;
 }
 
 void MiLightClient::write(uint8_t packet[]) {
@@ -105,7 +109,7 @@ void MiLightClient::write(uint8_t packet[]) {
 
 #ifdef DEBUG_PRINTF
   printf("Sending packet: ");
-  for (int i = 0; i < currentRadio->config().getPacketLength(); i++) {
+  for (int i = 0; i < currentRemote->packetFormatter->getPacketLength(); i++) {
     printf("%02X", packet[i]);
   }
   printf("\n");
@@ -113,11 +117,11 @@ void MiLightClient::write(uint8_t packet[]) {
 #endif
 
   for (int i = 0; i < this->resendCount; i++) {
-    currentRadio->write(packet, currentRadio->config().getPacketLength());
+    currentRadio->write(packet, currentRemote->packetFormatter->getPacketLength());
   }
 
   if (this->packetSentHandler) {
-    this->packetSentHandler(packet, currentRadio->config());
+    this->packetSentHandler(packet, *currentRemote);
   }
 
 #ifdef DEBUG_PRINTF
@@ -128,106 +132,106 @@ void MiLightClient::write(uint8_t packet[]) {
 }
 
 void MiLightClient::updateColorRaw(const uint8_t color) {
-  formatter->updateColorRaw(color);
+  currentRemote->packetFormatter->updateColorRaw(color);
   flushPacket();
 }
 
 void MiLightClient::updateHue(const uint16_t hue) {
-  formatter->updateHue(hue);
+  currentRemote->packetFormatter->updateHue(hue);
   flushPacket();
 }
 
 void MiLightClient::updateBrightness(const uint8_t brightness) {
-  formatter->updateBrightness(brightness);
+  currentRemote->packetFormatter->updateBrightness(brightness);
   flushPacket();
 }
 
 void MiLightClient::updateMode(uint8_t mode) {
-  formatter->updateMode(mode);
+  currentRemote->packetFormatter->updateMode(mode);
   flushPacket();
 }
 
 void MiLightClient::nextMode() {
-  formatter->nextMode();
+  currentRemote->packetFormatter->nextMode();
   flushPacket();
 }
 
 void MiLightClient::previousMode() {
-  formatter->previousMode();
+  currentRemote->packetFormatter->previousMode();
   flushPacket();
 }
 
 void MiLightClient::modeSpeedDown() {
-  formatter->modeSpeedDown();
+  currentRemote->packetFormatter->modeSpeedDown();
   flushPacket();
 }
 void MiLightClient::modeSpeedUp() {
-  formatter->modeSpeedUp();
+  currentRemote->packetFormatter->modeSpeedUp();
   flushPacket();
 }
 
 void MiLightClient::updateStatus(MiLightStatus status, uint8_t groupId) {
-  formatter->updateStatus(status, groupId);
+  currentRemote->packetFormatter->updateStatus(status, groupId);
   flushPacket();
 }
 
 void MiLightClient::updateStatus(MiLightStatus status) {
-  formatter->updateStatus(status);
+  currentRemote->packetFormatter->updateStatus(status);
   flushPacket();
 }
 
 void MiLightClient::updateSaturation(const uint8_t value) {
-  formatter->updateSaturation(value);
+  currentRemote->packetFormatter->updateSaturation(value);
   flushPacket();
 }
 
 void MiLightClient::updateColorWhite() {
-  formatter->updateColorWhite();
+  currentRemote->packetFormatter->updateColorWhite();
   flushPacket();
 }
 
 void MiLightClient::enableNightMode() {
-  formatter->enableNightMode();
+  currentRemote->packetFormatter->enableNightMode();
   flushPacket();
 }
 
 void MiLightClient::pair() {
-  formatter->pair();
+  currentRemote->packetFormatter->pair();
   flushPacket();
 }
 
 void MiLightClient::unpair() {
-  formatter->unpair();
+  currentRemote->packetFormatter->unpair();
   flushPacket();
 }
 
 void MiLightClient::increaseBrightness() {
-  formatter->increaseBrightness();
+  currentRemote->packetFormatter->increaseBrightness();
   flushPacket();
 }
 
 void MiLightClient::decreaseBrightness() {
-  formatter->decreaseBrightness();
+  currentRemote->packetFormatter->decreaseBrightness();
   flushPacket();
 }
 
 void MiLightClient::increaseTemperature() {
-  formatter->increaseTemperature();
+  currentRemote->packetFormatter->increaseTemperature();
   flushPacket();
 }
 
 void MiLightClient::decreaseTemperature() {
-  formatter->decreaseTemperature();
+  currentRemote->packetFormatter->decreaseTemperature();
   flushPacket();
 }
 
 void MiLightClient::updateTemperature(const uint8_t temperature) {
-  formatter->updateTemperature(temperature);
+  currentRemote->packetFormatter->updateTemperature(temperature);
   flushPacket();
 }
 
 void MiLightClient::command(uint8_t command, uint8_t arg) {
-  formatter->command(command, arg);
+  currentRemote->packetFormatter->command(command, arg);
   flushPacket();
 }
 
@@ -311,6 +315,11 @@ void MiLightClient::update(const JsonObject& request) {
     this->updateMode(request["mode"]);
   }
 
+  // Raw packet command/args
+  if (request.containsKey("button_id") && request.containsKey("argument")) {
+    this->command(request["button_id"], request["argument"]);
+  }
+
   // Always turn off last
   if (parsedStatus == OFF) {
     this->updateStatus(OFF);
@@ -367,12 +376,8 @@ uint8_t MiLightClient::parseStatus(const JsonObject& object) {
   return (strStatus.equalsIgnoreCase("on") || strStatus.equalsIgnoreCase("true")) ? ON : OFF;
 }
 
-void MiLightClient::formatPacket(uint8_t* packet, char* buffer) {
-  formatter->format(packet, buffer);
-}
-
 void MiLightClient::flushPacket() {
-  PacketStream& stream = formatter->buildPackets();
+  PacketStream& stream = currentRemote->packetFormatter->buildPackets();
   const size_t prevNumRepeats = this->resendCount;
 
   // When sending multiple packets, normalize the number of repeats
@@ -389,7 +394,7 @@ void MiLightClient::flushPacket() {
   }
 
   setResendCount(prevNumRepeats);
-  formatter->reset();
+  currentRemote->packetFormatter->reset();
 }
 
 void MiLightClient::onPacketSent(PacketSentHandler handler) {

+ 12 - 9
lib/MiLight/MiLightClient.h

@@ -3,6 +3,7 @@
 #include <MiLightRadio.h>
 #include <MiLightRadioFactory.h>
 #include <MiLightButtons.h>
+#include <MiLightRemoteConfig.h>
 #include <Settings.h>
 
 #ifndef _MILIGHTCLIENT_H
@@ -22,15 +23,15 @@ public:
     delete[] radios;
   }
 
-  typedef std::function<void(uint8_t* packet, const MiLightRadioConfig& config)> PacketSentHandler;
+  typedef std::function<void(uint8_t* packet, const MiLightRemoteConfig& config)> PacketSentHandler;
 
   void begin();
-  void prepare(MiLightRadioConfig& config, const uint16_t deviceId = -1, const uint8_t groupId = -1);
-  void prepare(MiLightRadioType config, const uint16_t deviceId = -1, const uint8_t groupId = -1);
+  void prepare(const MiLightRemoteConfig* remoteConfig, const uint16_t deviceId = -1, const uint8_t groupId = -1);
+  void prepare(const MiLightRemoteType type, const uint16_t deviceId = -1, const uint8_t groupId = -1);
 
   void setResendCount(const unsigned int resendCount);
   bool available();
-  void read(uint8_t packet[]);
+  size_t read(uint8_t packet[]);
   void write(uint8_t packet[]);
 
   void setHeld(bool held);
@@ -63,24 +64,26 @@ public:
 
   void updateSaturation(const uint8_t saturation);
 
-  void formatPacket(uint8_t* packet, char* buffer);
-
   void update(const JsonObject& object);
   void handleCommand(const String& command);
   void handleEffect(const String& effect);
-  
+
   void onPacketSent(PacketSentHandler handler);
 
+  size_t getNumRadios() const;
+  MiLightRadio* switchRadio(size_t radioIx);
+  MiLightRemoteConfig& currentRemoteConfig() const;
+
 protected:
 
   MiLightRadio** radios;
   MiLightRadio* currentRadio;
-  PacketFormatter* formatter;
+  const MiLightRemoteConfig* currentRemote;
   const size_t numRadios;
   unsigned int resendCount;
   PacketSentHandler packetSentHandler;
 
-  MiLightRadio* switchRadio(const MiLightRadioType type);
+  MiLightRadio* switchRadio(const MiLightRemoteConfig* remoteConfig);
   uint8_t parseStatus(const JsonObject& object);
 
   void flushPacket();

+ 64 - 0
lib/MiLight/MiLightRemoteConfig.cpp

@@ -0,0 +1,64 @@
+#include <MiLightRemoteConfig.h>
+
+const MiLightRemoteConfig* MiLightRemoteConfig::ALL_REMOTES[] = {
+  &FUT096Config,
+  &FUT091Config,
+  &FUT092Config,
+  &FUT089Config,
+  &FUT098Config
+};
+
+const MiLightRemoteConfig* MiLightRemoteConfig::fromType(const String& type) {
+  if (type.equalsIgnoreCase("rgbw") || type.equalsIgnoreCase("fut096")) {
+    return &FUT096Config;
+  }
+
+  if (type.equalsIgnoreCase("cct") || type.equalsIgnoreCase("fut091")) {
+    return &FUT091Config;
+  }
+
+  if (type.equalsIgnoreCase("rgb_cct") || type.equalsIgnoreCase("fut092")) {
+    return &FUT092Config;
+  }
+
+  if (type.equalsIgnoreCase("fut089")) {
+    return &FUT089Config;
+  }
+
+  if (type.equalsIgnoreCase("rgb") || type.equalsIgnoreCase("fut098")) {
+    return &FUT098Config;
+  }
+
+  return NULL;
+}
+
+const MiLightRemoteConfig* MiLightRemoteConfig::fromType(MiLightRemoteType type) {
+  switch (type) {
+    case REMOTE_TYPE_RGB:
+      return &FUT096Config;
+    case REMOTE_TYPE_CCT:
+      return &FUT091Config;
+    case REMOTE_TYPE_RGB_CCT:
+      return &FUT092Config;
+    case REMOTE_TYPE_FUT089:
+      return &FUT089Config;
+    default:
+      return NULL;
+  }
+}
+
+const MiLightRemoteConfig* MiLightRemoteConfig::fromReceivedPacket(
+  const MiLightRadioConfig& radioConfig,
+  const uint8_t* packet,
+  const size_t len
+) {
+  for (size_t i = 0; i < MiLightRemoteConfig::NUM_REMOTES; i++) {
+    const MiLightRemoteConfig* config = MiLightRemoteConfig::ALL_REMOTES[i];
+    if (&config->radioConfig == &radioConfig
+      && config->packetFormatter->canHandle(packet, len)) {
+      return config;
+    }
+  }
+
+  return NULL;
+}

+ 75 - 0
lib/MiLight/MiLightRemoteConfig.h

@@ -0,0 +1,75 @@
+#include <MiLightRadioConfig.h>
+#include <PacketFormatter.h>
+
+#include <RgbwPacketFormatter.h>
+#include <RgbPacketFormatter.h>
+#include <RgbCctPacketFormatter.h>
+#include <CctPacketFormatter.h>
+#include <FUT089PacketFormatter.h>
+#include <PacketFormatter.h>
+
+#ifndef _MILIGHT_REMOTE_CONFIG_H
+#define _MILIGHT_REMOTE_CONFIG_H
+
+class MiLightRemoteConfig {
+public:
+  MiLightRemoteConfig(
+    PacketFormatter* packetFormatter,
+    MiLightRadioConfig& radioConfig,
+    const MiLightRemoteType type,
+    const String name
+  ) : packetFormatter(packetFormatter),
+      radioConfig(radioConfig),
+      type(type),
+      name(name)
+  { }
+
+  PacketFormatter* const packetFormatter;
+  const MiLightRadioConfig& radioConfig;
+  const MiLightRemoteType type;
+  const String name;
+
+  static const MiLightRemoteConfig* fromType(MiLightRemoteType type);
+  static const MiLightRemoteConfig* fromType(const String& type);
+  static const MiLightRemoteConfig* fromReceivedPacket(const MiLightRadioConfig& radioConfig, const uint8_t* packet, const size_t len);
+
+  static const size_t NUM_REMOTES = 5;
+  static const MiLightRemoteConfig* ALL_REMOTES[NUM_REMOTES];
+};
+
+static const MiLightRemoteConfig FUT096Config( //rgbw
+  new RgbwPacketFormatter(),
+  MiLightRadioConfig::ALL_CONFIGS[0],
+  REMOTE_TYPE_RGBW,
+  "rgbw"
+);
+
+static const MiLightRemoteConfig FUT091Config( //cct
+  new CctPacketFormatter(),
+  MiLightRadioConfig::ALL_CONFIGS[1],
+  REMOTE_TYPE_CCT,
+  "cct"
+);
+
+static const MiLightRemoteConfig FUT092Config( //rgb+cct
+  new RgbCctPacketFormatter(),
+  MiLightRadioConfig::ALL_CONFIGS[2],
+  REMOTE_TYPE_RGB_CCT,
+  "rgb_cct"
+);
+
+static const MiLightRemoteConfig FUT089Config( //rgb+cct B8 / FUT089
+  new FUT089PacketFormatter(),
+  MiLightRadioConfig::ALL_CONFIGS[2],
+  REMOTE_TYPE_FUT089,
+  "fut089"
+);
+
+static const MiLightRemoteConfig FUT098Config( //rgb
+  new RgbPacketFormatter(),
+  MiLightRadioConfig::ALL_CONFIGS[3],
+  REMOTE_TYPE_RGB,
+  "rgb"
+);
+
+#endif

+ 4 - 0
lib/MiLight/PacketFormatter.cpp

@@ -29,6 +29,10 @@ PacketFormatter::PacketFormatter(const size_t packetLength, const size_t maxPack
   packetStream.packetStream = PACKET_BUFFER;
 }
 
+bool PacketFormatter::canHandle(const uint8_t *packet, const size_t len) {
+  return len == packetLength;
+}
+
 void PacketFormatter::finalizePacket(uint8_t* packet) { }
 
 void PacketFormatter::updateStatus(MiLightStatus status) {

+ 4 - 2
lib/MiLight/PacketFormatter.h

@@ -4,11 +4,11 @@
 #include <MiLightButtons.h>
 #include <ArduinoJson.h>
 
-#define PACKET_FORMATTER_BUFFER_SIZE 48
-
 #ifndef _PACKET_FORMATTER_H
 #define _PACKET_FORMATTER_H
 
+#define PACKET_FORMATTER_BUFFER_SIZE 48
+
 struct PacketStream {
   PacketStream();
 
@@ -27,6 +27,8 @@ public:
 
   typedef void (PacketFormatter::*StepFunction)();
 
+  virtual bool canHandle(const uint8_t* packet, const size_t len);
+
   void updateStatus(MiLightStatus status);
   virtual void updateStatus(MiLightStatus status, uint8_t groupId);
   virtual void command(uint8_t command, uint8_t arg);

+ 7 - 134
lib/MiLight/RgbCctPacketFormatter.cpp

@@ -1,60 +1,7 @@
 #include <RgbCctPacketFormatter.h>
+#include <V2RFEncoding.h>
 #include <Units.h>
 
-#define V2_OFFSET(byte, key, jumpStart) ( \
-  pgm_read_byte(&V2_OFFSETS[byte-1][key%4]) \
-    + \
-  ((jumpStart > 0 && key >= jumpStart && key < jumpStart+0x80) ? 0x80 : 0) \
-)
-
-#define GROUP_COMMAND_ARG(status, groupId) ( groupId + (status == OFF ? 5 : 0) )
-
-uint8_t const RgbCctPacketFormatter::V2_OFFSETS[][4] = {
-  { 0x45, 0x1F, 0x14, 0x5C }, // request type
-  { 0x2B, 0xC9, 0xE3, 0x11 }, // id 1
-  { 0x6D, 0x5F, 0x8A, 0x2B }, // id 2
-  { 0xAF, 0x03, 0x1D, 0xF3 }, // command
-  { 0x1A, 0xE2, 0xF0, 0xD1 }, // argument
-  { 0x04, 0xD8, 0x71, 0x42 }, // sequence
-  { 0xAF, 0x04, 0xDD, 0x07 }, // group
-  { 0x61, 0x13, 0x38, 0x64 }  // checksum
-};
-
-void RgbCctPacketFormatter::initializePacket(uint8_t* packet) {
-  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();
-  if (held) {
-    command |= 0x80;
-  }
-  currentPacket[RGB_CCT_COMMAND_INDEX] = command;
-  currentPacket[RGB_CCT_ARGUMENT_INDEX] = arg;
-}
-
-void RgbCctPacketFormatter::updateStatus(MiLightStatus status, uint8_t groupId) {
-  command(RGB_CCT_ON, GROUP_COMMAND_ARG(status, groupId));
-}
-
 void RgbCctPacketFormatter::modeSpeedDown() {
   command(RGB_CCT_ON, RGB_CCT_MODE_SPEED_DOWN);
 }
@@ -103,25 +50,21 @@ void RgbCctPacketFormatter::updateColorWhite() {
 }
 
 void RgbCctPacketFormatter::enableNightMode() {
-  uint8_t arg = GROUP_COMMAND_ARG(OFF, groupId);
+  uint8_t arg = groupCommandArg(OFF, groupId);
   command(RGB_CCT_ON | 0x80, arg);
 }
 
-void RgbCctPacketFormatter::finalizePacket(uint8_t* packet) {
-  encodeV2Packet(packet);
-}
-
 void RgbCctPacketFormatter::parsePacket(const uint8_t *packet, JsonObject& result) {
-  uint8_t packetCopy[RGB_CCT_PACKET_LEN];
-  memcpy(packetCopy, packet, RGB_CCT_PACKET_LEN);
-  decodeV2Packet(packetCopy);
+  uint8_t packetCopy[V2_PACKET_LEN];
+  memcpy(packetCopy, packet, V2_PACKET_LEN);
+  V2RFEncoding::decodeV2Packet(packetCopy);
 
   result["device_id"] = (packetCopy[2] << 8) | packetCopy[3];
   result["group_id"] = packetCopy[7];
   result["device_type"] = "rgb_cct";
 
-  uint8_t command = (packetCopy[RGB_CCT_COMMAND_INDEX] & 0x7F);
-  uint8_t arg = packetCopy[RGB_CCT_ARGUMENT_INDEX];
+  uint8_t command = (packetCopy[V2_COMMAND_INDEX] & 0x7F);
+  uint8_t arg = packetCopy[V2_ARGUMENT_INDEX];
 
   if (command == RGB_CCT_ON) {
     if (arg == RGB_CCT_MODE_SPEED_DOWN) {
@@ -172,73 +115,3 @@ void RgbCctPacketFormatter::parsePacket(const uint8_t *packet, JsonObject& resul
     result["state"] = "ON";
   }
 }
-
-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, 2, key, V2_OFFSET(8, packet[0], 0));
-}
-
-void RgbCctPacketFormatter::format(uint8_t const* packet, char* buffer) {
-  buffer += sprintf_P(buffer, PSTR("Raw packet: "));
-  for (int i = 0; i < packetLength; i++) {
-    buffer += sprintf_P(buffer, PSTR("%02X "), packet[i]);
-  }
-
-  uint8_t decodedPacket[packetLength];
-  memcpy(decodedPacket, packet, packetLength);
-
-  decodeV2Packet(decodedPacket);
-
-  buffer += sprintf_P(buffer, PSTR("\n\nDecoded:\n"));
-  buffer += sprintf_P(buffer, PSTR("Key      : %02X\n"), decodedPacket[0]);
-  buffer += sprintf_P(buffer, PSTR("b1       : %02X\n"), decodedPacket[1]);
-  buffer += sprintf_P(buffer, PSTR("ID       : %02X%02X\n"), decodedPacket[2], decodedPacket[3]);
-  buffer += sprintf_P(buffer, PSTR("Command  : %02X\n"), decodedPacket[4]);
-  buffer += sprintf_P(buffer, PSTR("Argument : %02X\n"), decodedPacket[5]);
-  buffer += sprintf_P(buffer, PSTR("Sequence : %02X\n"), decodedPacket[6]);
-  buffer += sprintf_P(buffer, PSTR("Group    : %02X\n"), decodedPacket[7]);
-  buffer += sprintf_P(buffer, PSTR("Checksum : %02X"), decodedPacket[8]);
-}

+ 6 - 25
lib/MiLight/RgbCctPacketFormatter.h

@@ -1,10 +1,9 @@
-#include <PacketFormatter.h>
+#include <V2PacketFormatter.h>
+
+#ifndef _RGB_CCT_PACKET_FORMATTER_H
+#define _RGB_CCT_PACKET_FORMATTER_H
 
-#define RGB_CCT_COMMAND_INDEX 4
-#define RGB_CCT_ARGUMENT_INDEX 5
 #define RGB_CCT_NUM_MODES 9
-#define V2_OFFSET_JUMP_START 0x54
-#define RGB_CCT_PACKET_LEN 9
 
 #define RGB_CCT_COLOR_OFFSET 0x5F
 #define RGB_CCT_BRIGHTNESS_OFFSET 0x8F
@@ -15,9 +14,6 @@
 #define RGB_CCT_KELVIN_REMOTE_OFFSET 0x4C
 #define RGB_CCT_KELVIN_REMOTE_START  0xE8
 
-#ifndef _RGB_CCT_PACKET_FORMATTER_H
-#define _RGB_CCT_PACKET_FORMATTER_H
-
 enum MiLightRgbCctCommand {
   RGB_CCT_ON = 0x01,
   RGB_CCT_OFF = 0x01,
@@ -33,27 +29,19 @@ enum MiLightRgbCctArguments {
   RGB_CCT_MODE_SPEED_DOWN = 0x0B
 };
 
-class RgbCctPacketFormatter : public PacketFormatter {
+class RgbCctPacketFormatter : public V2PacketFormatter {
 public:
-  static uint8_t const V2_OFFSETS[][4];
-
   RgbCctPacketFormatter()
-    : PacketFormatter(RGB_CCT_PACKET_LEN),
+    : V2PacketFormatter(0x20, 4),
       lastMode(0)
   { }
 
-  virtual void initializePacket(uint8_t* packet);
-
-  virtual void updateStatus(MiLightStatus status, uint8_t group);
   virtual void updateBrightness(uint8_t value);
-  virtual void command(uint8_t command, uint8_t arg);
   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 enableNightMode();
 
   virtual void modeSpeedDown();
@@ -62,15 +50,8 @@ public:
   virtual void nextMode();
   virtual void previousMode();
 
-  virtual void finalizePacket(uint8_t* packet);
   virtual void parsePacket(const uint8_t* packet, JsonObject& result);
 
-  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);
-
 protected:
 
   uint8_t lastMode;

+ 1 - 1
lib/MiLight/RgbPacketFormatter.cpp

@@ -4,7 +4,7 @@
 void RgbPacketFormatter::initializePacket(uint8_t *packet) {
   size_t packetPtr = 0;
 
-  packet[packetPtr++] = RGB;
+  packet[packetPtr++] = 0xA4;
   packet[packetPtr++] = deviceId >> 8;
   packet[packetPtr++] = deviceId & 0xFF;
   packet[packetPtr++] = 0;

+ 3 - 3
lib/MiLight/RgbwPacketFormatter.cpp

@@ -8,7 +8,7 @@
 void RgbwPacketFormatter::initializePacket(uint8_t* packet) {
   size_t packetPtr = 0;
 
-  packet[packetPtr++] = RGBW;
+  packet[packetPtr++] = RGBW_PROTOCOL_ID_BYTE;
   packet[packetPtr++] = deviceId >> 8;
   packet[packetPtr++] = deviceId & 0xFF;
   packet[packetPtr++] = 0;
@@ -42,7 +42,7 @@ void RgbwPacketFormatter::previousMode() {
 
 void RgbwPacketFormatter::updateMode(uint8_t mode) {
   command(RGBW_DISCO_MODE, 0);
-  currentPacket[0] = RGBW | mode;
+  currentPacket[0] = RGBW_PROTOCOL_ID_BYTE | mode;
 }
 
 void RgbwPacketFormatter::updateStatus(MiLightStatus status, uint8_t groupId) {
@@ -121,7 +121,7 @@ void RgbwPacketFormatter::parsePacket(const uint8_t* packet, JsonObject& result)
   } else if (command == RGBW_SPEED_UP) {
     result["command"] = "mode_speed_up";
   } else if (command == RGBW_DISCO_MODE) {
-    result["mode"] = packet[0] & ~RGBW;
+    result["mode"] = packet[0] & ~RGBW_PROTOCOL_ID_BYTE;
   } else {
     result["button_id"] = command;
   }

+ 2 - 0
lib/MiLight/RgbwPacketFormatter.h

@@ -3,6 +3,8 @@
 #ifndef _RGBW_PACKET_FORMATTER_H
 #define _RGBW_PACKET_FORMATTER_H
 
+#define RGBW_PROTOCOL_ID_BYTE 0xB0
+
 enum MiLightRgbwButton {
   RGBW_ALL_ON            = 0x01,
   RGBW_ALL_OFF           = 0x02,

+ 82 - 0
lib/MiLight/V2PacketFormatter.cpp

@@ -0,0 +1,82 @@
+#include <V2PacketFormatter.h>
+#include <V2RFEncoding.h>
+
+#define GROUP_COMMAND_ARG(status, groupId, numGroups) ( groupId + (status == OFF ? (numGroups + 1) : 0) )
+
+V2PacketFormatter::V2PacketFormatter(uint8_t protocolId, uint8_t numGroups)
+  : PacketFormatter(9),
+    protocolId(protocolId),
+    numGroups(numGroups)
+{ }
+
+bool V2PacketFormatter::canHandle(const uint8_t *packet, const size_t packetLen) {
+  uint8_t packetCopy[V2_PACKET_LEN];
+  memcpy(packetCopy, packet, V2_PACKET_LEN);
+  V2RFEncoding::decodeV2Packet(packetCopy);
+  return packetCopy[V2_PROTOCOL_ID_INDEX] == protocolId;
+}
+
+void V2PacketFormatter::initializePacket(uint8_t* packet) {
+  size_t packetPtr = 0;
+
+  // Always encode with 0x00 key. No utility in varying it.
+  packet[packetPtr++] = 0x00;
+
+  packet[packetPtr++] = protocolId;
+  packet[packetPtr++] = deviceId >> 8;
+  packet[packetPtr++] = deviceId & 0xFF;
+  packet[packetPtr++] = 0;
+  packet[packetPtr++] = 0;
+  packet[packetPtr++] = sequenceNum++;
+  packet[packetPtr++] = groupId;
+  packet[packetPtr++] = 0;
+}
+
+void V2PacketFormatter::command(uint8_t command, uint8_t arg) {
+  pushPacket();
+  if (held) {
+    command |= 0x80;
+  }
+  currentPacket[V2_COMMAND_INDEX] = command;
+  currentPacket[V2_ARGUMENT_INDEX] = arg;
+}
+
+void V2PacketFormatter::updateStatus(MiLightStatus status, uint8_t groupId) {
+  command(0x01, GROUP_COMMAND_ARG(status, groupId, numGroups));
+}
+
+void V2PacketFormatter::unpair() {
+  for (size_t i = 0; i < 5; i++) {
+    updateStatus(ON, 0);
+  }
+}
+
+void V2PacketFormatter::finalizePacket(uint8_t* packet) {
+  V2RFEncoding::encodeV2Packet(packet);
+}
+
+void V2PacketFormatter::format(uint8_t const* packet, char* buffer) {
+  buffer += sprintf_P(buffer, PSTR("Raw packet: "));
+  for (int i = 0; i < packetLength; i++) {
+    buffer += sprintf_P(buffer, PSTR("%02X "), packet[i]);
+  }
+
+  uint8_t decodedPacket[packetLength];
+  memcpy(decodedPacket, packet, packetLength);
+
+  V2RFEncoding::decodeV2Packet(decodedPacket);
+
+  buffer += sprintf_P(buffer, PSTR("\n\nDecoded:\n"));
+  buffer += sprintf_P(buffer, PSTR("Key      : %02X\n"), decodedPacket[0]);
+  buffer += sprintf_P(buffer, PSTR("b1       : %02X\n"), decodedPacket[1]);
+  buffer += sprintf_P(buffer, PSTR("ID       : %02X%02X\n"), decodedPacket[2], decodedPacket[3]);
+  buffer += sprintf_P(buffer, PSTR("Command  : %02X\n"), decodedPacket[4]);
+  buffer += sprintf_P(buffer, PSTR("Argument : %02X\n"), decodedPacket[5]);
+  buffer += sprintf_P(buffer, PSTR("Sequence : %02X\n"), decodedPacket[6]);
+  buffer += sprintf_P(buffer, PSTR("Group    : %02X\n"), decodedPacket[7]);
+  buffer += sprintf_P(buffer, PSTR("Checksum : %02X"), decodedPacket[8]);
+}
+
+uint8_t V2PacketFormatter::groupCommandArg(MiLightStatus status, uint8_t groupId) {
+  return GROUP_COMMAND_ARG(status, groupId, numGroups);
+}

+ 34 - 0
lib/MiLight/V2PacketFormatter.h

@@ -0,0 +1,34 @@
+#include <inttypes.h>
+#include <PacketFormatter.h>
+
+#ifndef _V2_PACKET_FORMATTER
+#define _V2_PACKET_FORMATTER
+
+#define V2_PACKET_LEN 9
+
+#define V2_PROTOCOL_ID_INDEX 1
+#define V2_COMMAND_INDEX 4
+#define V2_ARGUMENT_INDEX 5
+
+class V2PacketFormatter : public PacketFormatter {
+public:
+  V2PacketFormatter(uint8_t protocolId, uint8_t numGroups);
+
+  virtual bool canHandle(const uint8_t* packet, const size_t packetLen);
+  virtual void initializePacket(uint8_t* packet);
+
+  virtual void updateStatus(MiLightStatus status, uint8_t group);
+  virtual void command(uint8_t command, uint8_t arg);
+  virtual void format(uint8_t const* packet, char* buffer);
+  virtual void unpair();
+
+  virtual void finalizePacket(uint8_t* packet);
+
+  uint8_t groupCommandArg(MiLightStatus status, uint8_t groupId);
+
+protected:
+  const uint8_t protocolId;
+  const uint8_t numGroups;
+};
+
+#endif

+ 66 - 0
lib/MiLight/V2RFEncoding.cpp

@@ -0,0 +1,66 @@
+#include <V2RFEncoding.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 V2RFEncoding::V2_OFFSETS[][4] = {
+  { 0x45, 0x1F, 0x14, 0x5C }, // request type
+  { 0x2B, 0xC9, 0xE3, 0x11 }, // id 1
+  { 0x6D, 0x5F, 0x8A, 0x2B }, // id 2
+  { 0xAF, 0x03, 0x1D, 0xF3 }, // command
+  { 0x1A, 0xE2, 0xF0, 0xD1 }, // argument
+  { 0x04, 0xD8, 0x71, 0x42 }, // sequence
+  { 0xAF, 0x04, 0xDD, 0x07 }, // group
+  { 0x61, 0x13, 0x38, 0x64 }  // checksum
+};
+
+uint8_t V2RFEncoding::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 V2RFEncoding::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 V2RFEncoding::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 V2RFEncoding::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 V2RFEncoding::encodeV2Packet(uint8_t *packet) {
+  uint8_t key = xorKey(packet[0]);
+  uint8_t sum = key;
+
+  for (size_t i = 1; i <= 7; i++) {
+    sum += packet[i];
+    packet[i] = encodeByte(packet[i], 0, key, V2_OFFSET(i, packet[0], V2_OFFSET_JUMP_START));
+  }
+
+  packet[8] = encodeByte(sum, 2, key, V2_OFFSET(8, packet[0], 0));
+}

+ 21 - 0
lib/MiLight/V2RFEncoding.h

@@ -0,0 +1,21 @@
+#include <Arduino.h>
+#include <inttypes.h>
+
+#ifndef _V2_RF_ENCODING_H
+#define _V2_RF_ENCODING_H
+
+#define V2_OFFSET_JUMP_START 0x54
+
+class V2RFEncoding {
+public:
+  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);
+
+private:
+  static uint8_t const V2_OFFSETS[][4];
+};
+
+#endif

+ 48 - 0
lib/MiLightState/GroupState.h

@@ -0,0 +1,48 @@
+#include <inttypes.h>
+#include <Arduino.h>
+
+#ifndef _GROUP_STATE_H
+#define _GROUP_STATE_H
+
+struct GroupId {
+  uint16_t deviceId;
+  uint8_t groupId;
+};
+
+struct GroupState {
+  // xxxx xxxx  xxxx xxxx  xxxx xxxx  xxxx xxxx
+  // ^..
+  uint32_t data;
+
+  // 1 bit
+  bool isOn();
+  void setOn(bool on);
+
+  // 7 bits
+  uint8_t getBrightness();
+  void setBrightness(uint8_t brightness);
+
+  // 8 bits
+  uint8_t getHue();
+  void setHue(uint8_t hue);
+
+  // 7 bits
+  uint8_t getSaturation();
+  void setSaturation(uint8_t saturation);
+
+  // 5 bits
+  uint8_t getMode();
+  void setMode(uint8_t mode);
+
+  // 7 bits
+  uint8_t getKelvin();
+  void setKelvin(uint8_t kelvin);
+};
+
+struct GroupStateNode {
+  GroupState state;
+  GroupId nextNode;
+  GroupId prevNode;
+};
+
+#endif

+ 15 - 0
lib/MiLightState/GroupStateStore.h

@@ -0,0 +1,15 @@
+#include <GroupState.h>
+
+#ifndef _STATE_CACHE_H
+#define _STATE_CACHE_H
+
+class GroupStateStore {
+public:
+  bool get(const GroupId& id, GroupState& state);
+  void set(const GroupId& id, const GroupState& state);
+
+private:
+  void evictOldest(GroupState& state);
+};
+
+#endif

+ 46 - 106
lib/Radio/LT8900MiLightRadio.cpp

@@ -47,7 +47,7 @@ LT8900MiLightRadio::LT8900MiLightRadio(byte byCSPin, byte byResetPin, byte byPkt
   SPI.setBitOrder(MSBFIRST);
 
   //Initialize transceiver with correct settings
-  vInitRadioModule(config.type);
+  vInitRadioModule();
   delay(50);
 
   // Check if HW is connected
@@ -90,108 +90,48 @@ bool LT8900MiLightRadio::bCheckRadioConnection(void)
 /**************************************************************************/
 // Initialize radio module
 /**************************************************************************/
-void LT8900MiLightRadio::vInitRadioModule(MiLightRadioType type) {
-	if (type == RGB_CCT) {
-		bool bWriteDefaultDefault = true;  // Is it okay to use the default power up values, without setting them
-
-		regWrite16(0x00, 0x6F, 0xE0, 7);  // Recommended value by PMmicro
-		regWrite16(0x02, 0x66, 0x17, 7);  // Recommended value by PMmicro
-		regWrite16(0x04, 0x9C, 0xC9, 7);  // Recommended value by PMmicro
-
-		regWrite16(0x05, 0x66, 0x37, 7);  // Recommended value by PMmicro
-		regWrite16(0x07, 0x00, 0x4C, 7);  // PL1167's TX/RX Enable and Channel Register, Default channel 76
-		regWrite16(0x08, 0x6C, 0x90, 7);  // Recommended value by PMmicro
-		regWrite16(0x09, 0x48, 0x00, 7);  // PA Control register
-
-		regWrite16(0x0B, 0x00, 0x08, 7);  // Recommended value by PMmicro
-		regWrite16(0x0D, 0x48, 0xBD, 7);  // Recommended value by PMmicro
-		regWrite16(0x16, 0x00, 0xFF, 7);  // Recommended value by PMmicro
-		regWrite16(0x18, 0x00, 0x67, 7);  // Recommended value by PMmicro
-
-		regWrite16(0x1A, 0x19, 0xE0, 7);  // Recommended value by PMmicro
-		regWrite16(0x1B, 0x13, 0x00, 7);  // Recommended value by PMmicro
-
-		regWrite16(0x20, 0x48, 0x00, 7);  // Recommended value by PMmicro
-		regWrite16(0x21, 0x3F, 0xC7, 7);  // Recommended value by PMmicro
-		regWrite16(0x22, 0x20, 0x00, 7);  // Recommended value by PMmicro
-		regWrite16(0x23, 0x03, 0x00, 7);  // Recommended value by PMmicro
-
-		regWrite16(0x24, 0x72, 0x36, 7);  // Sync R0
-		regWrite16(0x27, 0x18, 0x09, 7);  // Sync R3
-		regWrite16(0x28, 0x44, 0x02, 7);  // Recommended value by PMmicro
-		regWrite16(0x29, 0xB0, 0x00, 7);  // Recommended value by PMmicro
-		regWrite16(0x2A, 0xFD, 0xB0, 7);  // Recommended value by PMmicro
-
-		if (bWriteDefaultDefault == true) {
-			regWrite16(0x01, 0x56, 0x81, 7);  // Recommended value by PMmicro
-			regWrite16(0x0A, 0x7F, 0xFD, 7);  // Recommended value by PMmicro
-			regWrite16(0x0C, 0x00, 0x00, 7);  // Recommended value by PMmicro
-			regWrite16(0x17, 0x80, 0x05, 7);  // Recommended value by PMmicro
-			regWrite16(0x19, 0x16, 0x59, 7);  // Recommended value by PMmicro
-			regWrite16(0x1C, 0x18, 0x00, 7);  // Recommended value by PMmicro
-
-			regWrite16(0x25, 0x00, 0x00, 7);  // Recommended value by PMmicro
-			regWrite16(0x26, 0x00, 0x00, 7);  // Recommended value by PMmicro
-			regWrite16(0x2B, 0x00, 0x0F, 7);  // Recommended value by PMmicro
-		}
-	} else if( (type == RGBW) || (type == CCT) || (type == RGB) ) {
-		regWrite16(0x00, 0x6F, 0xE0, 7);  // Recommended value by PMmicro
-		regWrite16(0x01, 0x56, 0x81, 7);   // Recommended value by PMmicro
-		regWrite16(0x02, 0x66, 0x17, 7);   // Recommended value by PMmicro
-		regWrite16(0x04, 0x9C, 0xC9, 7);  // Recommended value by PMmicro
-		regWrite16(0x05, 0x66, 0x37, 7);   // Recommended value by PMmicro
-		regWrite16(0x07, 0x00, 0x4C, 7);     // PL1167's TX/RX Enable and Channel Register
-		regWrite16(0x08, 0x6C, 0x90, 7);  // Recommended value by PMmicro
-		regWrite16(0x09, 0x48, 0x00, 7);     // PL1167's PA Control Register
-		regWrite16(0x0A, 0x7F, 0xFD, 7); // Recommended value by PMmicro
-		regWrite16(0x0B, 0x00, 0x08, 7);     // PL1167's RSSI OFF Control Register -- ???
-		regWrite16(0x0C, 0x00, 0x00, 7);     // Recommended value by PMmicro
-		regWrite16(0x0D, 0x48, 0xBD, 7);  // Recommended value by PMmicro
-		regWrite16(0x16, 0x00, 0xFF, 7);   // Recommended value by PMmicro
-		regWrite16(0x17, 0x80, 0x05, 7);   // PL1167's VCO Calibration Enable Register
-		regWrite16(0x18, 0x00, 0x67, 7);   // Recommended value by PMmicro
-		regWrite16(0x19, 0x16, 0x59, 7);   // Recommended value by PMmicro
-		regWrite16(0x1A, 0x19, 0xE0, 7);  // Recommended value by PMmicro
-		regWrite16(0x1B, 0x13, 0x00, 7);    // Recommended value by PMmicro
-		regWrite16(0x1C, 0x18, 0x00, 7);    // Recommended value by PMmicro
-		regWrite16(0x20, 0x48, 0x00, 7);    // PL1167's Data Configure Register: LEN_PREAMBLE = 010 -> (0xAAAAAA) 3 bytes, LEN_SYNCWORD = 01 -> 32 bits, LEN_TRAILER = 000 -> (0x05) 4 bits, TYPE_PKT_DAT = 00 -> NRZ law data, TYPE_FEC = 00 -> No FEC
-		regWrite16(0x21, 0x3F, 0xC7, 7);  // PL1167's Delay Time Control Register 0
-		regWrite16(0x22, 0x20, 0x00, 7);    // PL1167's Delay Time Control Register 1
-		regWrite16(0x23, 0x03, 0x00, 7);     // PL1167's Power Management and Miscellaneous Register
-
-		regWrite16(0x28, 0x44, 0x02, 7);    // PL1167's FIFO and SYNCWORD Threshold Register
-		regWrite16(0x29, 0xB0, 0x00, 7);   // PL1167's Miscellaneous Register: CRC_ON = 1 -> ON, SCR_ON = 0 -> OFF, EN_PACK_LEN = 1 -> ON, FW_TERM_TX = 1 -> ON, AUTO_ACK = 0 -> OFF, PKT_LEVEL = 0 -> PKT active high, CRC_INIT_DAT = 0
-		regWrite16(0x2A, 0xFD, 0xB0, 7); // PL1167's SCAN RSSI Register 0
-		regWrite16(0x2B, 0x00, 0x0F, 7);    // PL1167's SCAN RSSI Register 1
-		delay(200);
-		regWrite16(0x80, 0x00, 0x00, 7);
-		regWrite16(0x81, 0xFF, 0xFF, 7);
-		regWrite16(0x82, 0x00, 0x00, 7);
-		regWrite16(0x84, 0x00, 0x00, 7);
-		regWrite16(0x85, 0xFF, 0xFF, 7);
-		regWrite16(0x87, 0xFF, 0xFF, 7);
-		regWrite16(0x88, 0x00, 0x00, 7);
-		regWrite16(0x89, 0xFF, 0xFF, 7);
-		regWrite16(0x8A, 0x00, 0x00, 7);
-		regWrite16(0x8B, 0xFF, 0xFF, 7);
-		regWrite16(0x8C, 0x00, 0x00, 7);
-		regWrite16(0x8D, 0xFF, 0xFF, 7);
-		regWrite16(0x96, 0x00, 0x00, 7);
-		regWrite16(0x97, 0xFF, 0xFF, 7);
-		regWrite16(0x98, 0x00, 0x00, 7);
-		regWrite16(0x99, 0xFF, 0xFF, 7);
-		regWrite16(0x9A, 0x00, 0x00, 7);
-		regWrite16(0x9B, 0xFF, 0xFF, 7);
-		regWrite16(0x9C, 0x00, 0x00, 7);
-		regWrite16(0xA0, 0x00, 0x00, 7);
-		regWrite16(0xA1, 0xFF, 0xFF, 7);
-		regWrite16(0xA2, 0x00, 0x00, 7);
-		regWrite16(0xA3, 0xFF, 0xFF, 7);
-		regWrite16(0xA8, 0x00, 0x00, 7);
-		regWrite16(0xA9, 0xFF, 0xFF, 7);
-		regWrite16(0xAA, 0x00, 0x00, 7);
-		regWrite16(0xAB, 0xFF, 0xFF, 7);
-		regWrite16(0x07, 0x00, 0x00, 7);       // Disable TX/RX and set radio channel to 0
+void LT8900MiLightRadio::vInitRadioModule() {
+	bool bWriteDefaultDefault = true;  // Is it okay to use the default power up values, without setting them
+
+	regWrite16(0x00, 0x6F, 0xE0, 7);  // Recommended value by PMmicro
+	regWrite16(0x02, 0x66, 0x17, 7);  // Recommended value by PMmicro
+	regWrite16(0x04, 0x9C, 0xC9, 7);  // Recommended value by PMmicro
+
+	regWrite16(0x05, 0x66, 0x37, 7);  // Recommended value by PMmicro
+	regWrite16(0x07, 0x00, 0x4C, 7);  // PL1167's TX/RX Enable and Channel Register, Default channel 76
+	regWrite16(0x08, 0x6C, 0x90, 7);  // Recommended value by PMmicro
+	regWrite16(0x09, 0x48, 0x00, 7);  // PA Control register
+
+	regWrite16(0x0B, 0x00, 0x08, 7);  // Recommended value by PMmicro
+	regWrite16(0x0D, 0x48, 0xBD, 7);  // Recommended value by PMmicro
+	regWrite16(0x16, 0x00, 0xFF, 7);  // Recommended value by PMmicro
+	regWrite16(0x18, 0x00, 0x67, 7);  // Recommended value by PMmicro
+
+	regWrite16(0x1A, 0x19, 0xE0, 7);  // Recommended value by PMmicro
+	regWrite16(0x1B, 0x13, 0x00, 7);  // Recommended value by PMmicro
+
+	regWrite16(0x20, 0x48, 0x00, 7);  // Recommended value by PMmicro
+	regWrite16(0x21, 0x3F, 0xC7, 7);  // Recommended value by PMmicro
+	regWrite16(0x22, 0x20, 0x00, 7);  // Recommended value by PMmicro
+	regWrite16(0x23, 0x03, 0x00, 7);  // Recommended value by PMmicro
+
+	regWrite16(0x24, 0x72, 0x36, 7);  // Sync R0
+	regWrite16(0x27, 0x18, 0x09, 7);  // Sync R3
+	regWrite16(0x28, 0x44, 0x02, 7);  // Recommended value by PMmicro
+	regWrite16(0x29, 0xB0, 0x00, 7);  // Recommended value by PMmicro
+	regWrite16(0x2A, 0xFD, 0xB0, 7);  // Recommended value by PMmicro
+
+	if (bWriteDefaultDefault == true) {
+		regWrite16(0x01, 0x56, 0x81, 7);  // Recommended value by PMmicro
+		regWrite16(0x0A, 0x7F, 0xFD, 7);  // Recommended value by PMmicro
+		regWrite16(0x0C, 0x00, 0x00, 7);  // Recommended value by PMmicro
+		regWrite16(0x17, 0x80, 0x05, 7);  // Recommended value by PMmicro
+		regWrite16(0x19, 0x16, 0x59, 7);  // Recommended value by PMmicro
+		regWrite16(0x1C, 0x18, 0x00, 7);  // Recommended value by PMmicro
+
+		regWrite16(0x25, 0x00, 0x00, 7);  // Recommended value by PMmicro
+		regWrite16(0x26, 0x00, 0x00, 7);  // Recommended value by PMmicro
+		regWrite16(0x2B, 0x00, 0x0F, 7);  // Recommended value by PMmicro
 	}
 }
 
@@ -380,7 +320,7 @@ int LT8900MiLightRadio::begin()
 /**************************************************************************/
 int LT8900MiLightRadio::configure()
 {
-  vInitRadioModule(_config.type);
+  vInitRadioModule();
   vSetSyncWord(_config.syncword3, 0,0,_config.syncword0);
   vStartListening(_config.channels[0]);
   return 0;
@@ -412,8 +352,8 @@ int LT8900MiLightRadio::read(uint8_t frame[], size_t &frame_length)
   Serial.println(F("LT8900: Radio was available, reading packet..."));
   #endif
 
-  uint8_t buf[_config.getPacketLength()];
-  int packetSize = iReadRXBuffer(buf, _config.getPacketLength());
+  uint8_t buf[MILIGHT_MAX_PACKET_LENGTH];
+  int packetSize = iReadRXBuffer(buf, MILIGHT_MAX_PACKET_LENGTH);
 
   if (packetSize > 0) {
     frame_length = packetSize;

+ 1 - 1
lib/Radio/LT8900MiLightRadio.h

@@ -55,7 +55,7 @@ class LT8900MiLightRadio : public MiLightRadio {
 
   private:
 
-    void vInitRadioModule(MiLightRadioType type);
+    void vInitRadioModule();
     void vSetSyncWord(uint16_t syncWord3, uint16_t syncWord2, uint16_t syncWord1, uint16_t syncWord0);
     uint16_t uiReadRegister(uint8_t reg);
     void regWrite16(byte ADDR, byte V1, byte V2, byte WAIT);

+ 12 - 11
lib/Radio/MiLightButtons.h

@@ -1,17 +1,18 @@
 #ifndef _MILIGHT_BUTTONS
-#define _MILIGHT_BUTTONS 
+#define _MILIGHT_BUTTONS
 
-enum MiLightRadioType {
-  UNKNOWN = 0,
-  RGBW  = 0xB0,
-  CCT   = 0x5A,
-  RGB_CCT = 0x20,
-  RGB = 0xA4
+enum MiLightRemoteType {
+  REMOTE_TYPE_UNKNOWN,
+  REMOTE_TYPE_RGBW,
+  REMOTE_TYPE_CCT,
+  REMOTE_TYPE_RGB_CCT,
+  REMOTE_TYPE_RGB,
+  REMOTE_TYPE_FUT089
 };
 
-enum MiLightStatus { 
-  ON = 0, 
-  OFF = 1 
+enum MiLightStatus {
+  ON = 0,
+  OFF = 1
 };
 
-#endif
+#endif

+ 5 - 29
lib/Radio/MiLightRadioConfig.cpp

@@ -1,32 +1,8 @@
 #include <MiLightRadioConfig.h>
 
-MiLightRadioConfig* MiLightRadioConfig::ALL_CONFIGS[] = {
-  &MilightRgbwConfig,
-  &MilightCctConfig,
-  &MilightRgbCctConfig,
-  &MilightRgbConfig
+MiLightRadioConfig MiLightRadioConfig::ALL_CONFIGS[] = {
+  MiLightRadioConfig(0x147A, 0x258B, 7, 9, 40, 71), // rgbw
+  MiLightRadioConfig(0x050A, 0x55AA, 7, 4, 39, 74), // cct
+  MiLightRadioConfig(0x7236, 0x1809, 9, 8, 39, 70), // rgb+cct, fut089
+  MiLightRadioConfig(0x9AAB, 0xBCCD, 6, 3, 38, 73)  // rgb
 };
-
-MiLightRadioConfig* MiLightRadioConfig::fromString(const String& s) {
-  for (size_t i = 0; i < MiLightRadioConfig::NUM_CONFIGS; i++) {
-    MiLightRadioConfig* config = MiLightRadioConfig::ALL_CONFIGS[i];
-    if (s.equalsIgnoreCase(config->name)) {
-      return config;
-    }
-  }
-  return NULL;
-}
-
-MiLightRadioConfig* MiLightRadioConfig::fromType(MiLightRadioType type) {
-  for (size_t i = 0; i < MiLightRadioConfig::NUM_CONFIGS; i++) {
-    MiLightRadioConfig* config = MiLightRadioConfig::ALL_CONFIGS[i];
-    if (config->type == type) {
-      return config;
-    }
-  }
-  return NULL;
-}
-
-size_t MiLightRadioConfig::getPacketLength() const {
-  return packetFormatter->getPacketLength();
-}

+ 14 - 40
lib/Radio/MiLightRadioConfig.h

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

+ 1 - 1
lib/Radio/MiLightRadioFactory.h

@@ -15,7 +15,7 @@ public:
   virtual MiLightRadio* create(const MiLightRadioConfig& config) = 0;
 
   static MiLightRadioFactory* fromSettings(const Settings& settings);
-  
+
 };
 
 class NRF24Factory : public MiLightRadioFactory {

+ 1 - 1
lib/Radio/NRF24MiLightRadio.cpp

@@ -49,7 +49,7 @@ int NRF24MiLightRadio::configure() {
   }
 
   // +1 to be able to buffer the length
-  retval = _pl1167.setMaxPacketLength(_config.getPacketLength() + 1);
+  retval = _pl1167.setMaxPacketLength(_config.packetLength + 1);
   if (retval < 0) {
     return retval;
   }

+ 5 - 5
lib/Udp/V5MiLightUdpServer.cpp

@@ -16,20 +16,20 @@ void V5MiLightUdpServer::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->prepare(MilightRgbwConfig, deviceId, groupId);
+    client->prepare(&FUT098Config, deviceId, groupId);
     client->updateStatus(status);
 
     this->lastGroup = groupId;
   // Command set_white for RGBW
   } 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->prepare(&FUT096Config, deviceId, groupId);
     client->updateColorWhite();
     this->lastGroup = groupId;
     // On/off for CCT
   } else if (CctPacketFormatter::cctCommandIdToGroup(command) != 255) {
     uint8_t cctGroup = CctPacketFormatter::cctCommandIdToGroup(command);
-    client->prepare(MilightCctConfig, deviceId, cctGroup);
+    client->prepare(&FUT091Config, deviceId, cctGroup);
     this->lastGroup = cctGroup;
 
     // Night mode commands are same as off commands with MSB set
@@ -39,7 +39,7 @@ void V5MiLightUdpServer::handleCommand(uint8_t command, uint8_t commandArg) {
       client->updateStatus(CctPacketFormatter::cctCommandToStatus(command));
     }
   } else {
-    client->prepare(MilightRgbwConfig, deviceId, lastGroup);
+    client->prepare(&FUT096Config, deviceId, lastGroup);
     bool handled = true;
 
     switch (command) {
@@ -84,7 +84,7 @@ void V5MiLightUdpServer::handleCommand(uint8_t command, uint8_t commandArg) {
       return;
     }
 
-    client->prepare(MilightCctConfig, deviceId, lastGroup);
+    client->prepare(&FUT091Config, deviceId, lastGroup);
 
     switch(command) {
       case UDP_CCT_BRIGHTNESS_DOWN:

+ 1 - 1
lib/Udp/V6CctCommandHandler.h

@@ -18,7 +18,7 @@ enum CctCommandIds {
 class V6CctCommandHandler : public V6CommandHandler {
 public:
   V6CctCommandHandler()
-    : V6CommandHandler(0x0100, MilightCctConfig)
+    : V6CommandHandler(0x0100, FUT091Config)
   { }
 
   virtual bool handleCommand(

+ 1 - 1
lib/Udp/V6ComamndHandler.cpp

@@ -21,7 +21,7 @@ bool V6CommandHandler::handleCommand(MiLightClient* client,
   uint32_t command,
   uint32_t commandArg)
 {
-  client->prepare(radioConfig, deviceId, group);
+  client->prepare(&remoteConfig, deviceId, group);
 
   if (commandType == V6_PAIR) {
     client->pair();

+ 4 - 4
lib/Udp/V6CommandHandler.h

@@ -16,9 +16,9 @@ public:
   static V6CommandHandler* ALL_HANDLERS[];
   static const size_t NUM_HANDLERS;
 
-  V6CommandHandler(uint16_t commandId, MiLightRadioConfig& radioConfig)
+  V6CommandHandler(uint16_t commandId, const MiLightRemoteConfig& remoteConfig)
     : commandId(commandId),
-      radioConfig(radioConfig)
+      remoteConfig(remoteConfig)
   { }
 
   virtual bool handleCommand(
@@ -31,7 +31,7 @@ public:
   );
 
   const uint16_t commandId;
-  MiLightRadioConfig& radioConfig;
+  const MiLightRemoteConfig& remoteConfig;
 
 protected:
 
@@ -51,7 +51,7 @@ protected:
 class V6CommandDemuxer : public V6CommandHandler {
 public:
   V6CommandDemuxer(V6CommandHandler* handlers[], size_t numHandlers)
-    : V6CommandHandler(0, MilightRgbwConfig),
+    : V6CommandHandler(0, FUT096Config),
       handlers(handlers),
       numHandlers(numHandlers)
   { }

+ 1 - 1
lib/Udp/V6RgbCctCommandHandler.h

@@ -23,7 +23,7 @@ enum V2CommandArgIds {
 class V6RgbCctCommandHandler : public V6CommandHandler {
 public:
   V6RgbCctCommandHandler()
-    : V6CommandHandler(0x0800, MilightRgbCctConfig)
+    : V6CommandHandler(0x0800, FUT092Config)
   { }
 
   virtual bool handleCommand(

+ 1 - 1
lib/Udp/V6RgbCommandHandler.h

@@ -19,7 +19,7 @@ enum RgbCommandIds {
 class V6RgbCommandHandler : public V6CommandHandler {
 public:
   V6RgbCommandHandler()
-    : V6CommandHandler(0x0500, MilightRgbConfig)
+    : V6CommandHandler(0x0500, FUT098Config)
   { }
 
   virtual bool handleCommand(

+ 1 - 1
lib/Udp/V6RgbwCommandHandler.h

@@ -20,7 +20,7 @@ enum RgbwCommandIds {
 class V6RgbwCommandHandler : public V6CommandHandler {
 public:
   V6RgbwCommandHandler()
-    : V6CommandHandler(0x0700, MilightRgbwConfig)
+    : V6CommandHandler(0x0700, FUT096Config)
   { }
 
   virtual bool handleCommand(

+ 39 - 36
lib/WebServer/MiLightHttpServer.cpp

@@ -161,7 +161,7 @@ void MiLightHttpServer::handleGetRadioConfigs() {
   JsonArray& arr = buffer.createArray();
 
   for (size_t i = 0; i < MiLightRadioConfig::NUM_CONFIGS; i++) {
-    const MiLightRadioConfig* config = MiLightRadioConfig::ALL_CONFIGS[i];
+    const MiLightRemoteConfig* config = MiLightRemoteConfig::ALL_REMOTES[i];
     arr.add(config->name);
   }
 
@@ -235,17 +235,18 @@ void MiLightHttpServer::handleUpdateSettings() {
 void MiLightHttpServer::handleListenGateway(const UrlTokenBindings* bindings) {
   bool available = false;
   bool listenAll = bindings == NULL;
-  uint8_t configIx = 0;
-  MiLightRadioConfig* currentConfig =
-    listenAll
-      ? MiLightRadioConfig::ALL_CONFIGS[0]
-      : MiLightRadioConfig::fromString(bindings->get("type"));
-
-  if (currentConfig == NULL && bindings != NULL) {
-    String body = "Unknown device type: ";
-    body += bindings->get("type");
+  size_t configIx = 0;
+  const MiLightRadioConfig* radioConfig = NULL;
+
+  if (bindings != NULL) {
+    String strType(bindings->get("type"));
+    const MiLightRemoteConfig* remoteConfig = MiLightRemoteConfig::fromType(strType);
+    milightClient->prepare(remoteConfig, 0, 0);
+    radioConfig = &remoteConfig->radioConfig;
+  }
 
-    server.send(400, "text/plain", body);
+  if (radioConfig == NULL && !listenAll) {
+    server.send_P(400, TEXT_PLAIN, PSTR("Unknown device type supplied."));
     return;
   }
 
@@ -255,11 +256,8 @@ void MiLightHttpServer::handleListenGateway(const UrlTokenBindings* bindings) {
     }
 
     if (listenAll) {
-      currentConfig = MiLightRadioConfig::ALL_CONFIGS[
-        configIx++ % MiLightRadioConfig::NUM_CONFIGS
-      ];
+      radioConfig = &milightClient->switchRadio(configIx++ % milightClient->getNumRadios())->config();
     }
-    milightClient->prepare(*currentConfig, 0, 0);
 
     if (milightClient->available()) {
       available = true;
@@ -268,8 +266,13 @@ void MiLightHttpServer::handleListenGateway(const UrlTokenBindings* bindings) {
     yield();
   }
 
-  uint8_t packet[currentConfig->getPacketLength()];
-  milightClient->read(packet);
+  uint8_t packet[MILIGHT_MAX_PACKET_LENGTH];
+  size_t packetLen = milightClient->read(packet);
+  const MiLightRemoteConfig* remoteConfig = MiLightRemoteConfig::fromReceivedPacket(
+    *radioConfig,
+    packet,
+    packetLen
+  );
 
   char response[200];
   char* responseBuffer = response;
@@ -277,10 +280,10 @@ void MiLightHttpServer::handleListenGateway(const UrlTokenBindings* bindings) {
   responseBuffer += sprintf_P(
     responseBuffer,
     PSTR("\n%s packet received (%d bytes):\n"),
-    currentConfig->name,
-    sizeof(packet)
+    remoteConfig->name.c_str(),
+    packetLen
   );
-  milightClient->formatPacket(packet, responseBuffer);
+  remoteConfig->packetFormatter->format(packet, responseBuffer);
 
   server.send(200, "text/plain", response);
 }
@@ -300,25 +303,25 @@ void MiLightHttpServer::handleUpdateGroup(const UrlTokenBindings* urlBindings) {
 
   String _deviceIds = urlBindings->get("device_id");
   String _groupIds = urlBindings->get("group_id");
-  String _radioTypes = urlBindings->get("type");
+  String _remoteTypes = urlBindings->get("type");
   char deviceIds[_deviceIds.length()];
   char groupIds[_groupIds.length()];
-  char radioTypes[_radioTypes.length()];
-  strcpy(radioTypes, _radioTypes.c_str());
+  char remoteTypes[_remoteTypes.length()];
+  strcpy(remoteTypes, _remoteTypes.c_str());
   strcpy(groupIds, _groupIds.c_str());
   strcpy(deviceIds, _deviceIds.c_str());
 
   TokenIterator deviceIdItr(deviceIds, _deviceIds.length());
   TokenIterator groupIdItr(groupIds, _groupIds.length());
-  TokenIterator radioTypesItr(radioTypes, _radioTypes.length());
+  TokenIterator remoteTypesItr(remoteTypes, _remoteTypes.length());
 
-  while (radioTypesItr.hasNext()) {
-    const char* _radioType = radioTypesItr.nextToken();
-    MiLightRadioConfig* config = MiLightRadioConfig::fromString(_radioType);
+  while (remoteTypesItr.hasNext()) {
+    const char* _remoteType = remoteTypesItr.nextToken();
+    const MiLightRemoteConfig* config = MiLightRemoteConfig::fromType(_remoteType);
 
     if (config == NULL) {
       char buffer[40];
-      sprintf_P(buffer, PSTR("Unknown device type: %s"), _radioType);
+      sprintf_P(buffer, PSTR("Unknown device type: %s"), _remoteType);
       server.send(400, "text/plain", buffer);
       return;
     }
@@ -331,7 +334,7 @@ void MiLightHttpServer::handleUpdateGroup(const UrlTokenBindings* urlBindings) {
       while (groupIdItr.hasNext()) {
         const uint8_t groupId = atoi(groupIdItr.nextToken());
 
-        milightClient->prepare(*config, deviceId, groupId);
+        milightClient->prepare(config, deviceId, groupId);
         handleRequest(request);
       }
     }
@@ -347,7 +350,7 @@ void MiLightHttpServer::handleRequest(const JsonObject& request) {
 void MiLightHttpServer::handleSendRaw(const UrlTokenBindings* bindings) {
   DynamicJsonBuffer buffer;
   JsonObject& request = buffer.parse(server.arg("plain"));
-  MiLightRadioConfig* config = MiLightRadioConfig::fromString(bindings->get("type"));
+  const MiLightRemoteConfig* config = MiLightRemoteConfig::fromType(bindings->get("type"));
 
   if (config == NULL) {
     char buffer[50];
@@ -356,16 +359,16 @@ void MiLightHttpServer::handleSendRaw(const UrlTokenBindings* bindings) {
     return;
   }
 
-  uint8_t packet[config->getPacketLength()];
+  uint8_t packet[MILIGHT_MAX_PACKET_LENGTH];
   const String& hexPacket = request["packet"];
-  hexStrToBytes<uint8_t>(hexPacket.c_str(), hexPacket.length(), packet, config->getPacketLength());
+  hexStrToBytes<uint8_t>(hexPacket.c_str(), hexPacket.length(), packet, MILIGHT_MAX_PACKET_LENGTH);
 
   size_t numRepeats = MILIGHT_DEFAULT_RESEND_COUNT;
   if (request.containsKey("num_repeats")) {
     numRepeats = request["num_repeats"];
   }
 
-  milightClient->prepare(*config, 0, 0);
+  milightClient->prepare(config, 0, 0);
 
   for (size_t i = 0; i < numRepeats; i++) {
     milightClient->write(packet);
@@ -388,7 +391,7 @@ void MiLightHttpServer::handleWsEvent(uint8_t num, WStype_t type, uint8_t *paylo
   }
 }
 
-void MiLightHttpServer::handlePacketSent(uint8_t *packet, const MiLightRadioConfig& config) {
+void MiLightHttpServer::handlePacketSent(uint8_t *packet, const MiLightRemoteConfig& config) {
   if (numWsClients > 0) {
     size_t packetLen = config.packetFormatter->getPacketLength();
     char buffer[packetLen*3];
@@ -401,8 +404,8 @@ void MiLightHttpServer::handlePacketSent(uint8_t *packet, const MiLightRadioConf
     sprintf_P(
       responseBuffer,
       PSTR("\n%s packet received (%d bytes):\n%s"),
-      config.name,
-      sizeof(packet),
+      config.name.c_str(),
+      packetLen,
       formattedPacket
     );
 

+ 1 - 1
lib/WebServer/MiLightHttpServer.h

@@ -29,7 +29,7 @@ public:
   void handleClient();
   void onSettingsSaved(SettingsSavedHandler handler);
   void on(const char* path, HTTPMethod method, ESP8266WebServer::THandlerFunction handler);
-  void handlePacketSent(uint8_t* packet, const MiLightRadioConfig& config);
+  void handlePacketSent(uint8_t* packet, const MiLightRemoteConfig& config);
   WiFiClient client();
 
 protected:

+ 3 - 3
platformio.ini

@@ -20,18 +20,18 @@ lib_deps_external =
   https://github.com/ratkins/RGBConverter
   Hash
   WebSockets
-build_flags = !python .get_version.py -DMQTT_MAX_PACKET_SIZE=200 -Idist
 extra_script =
   .build_web.py
+build_flags = !python .get_version.py -DMQTT_MAX_PACKET_SIZE=200 -Idist
+# -D DEBUG_PRINTF
 # -D MQTT_DEBUG
 # -D MILIGHT_UDP_DEBUG
-# -D DEBUG_PRINTF
 
 [env:nodemcuv2]
 platform = espressif8266
 framework = arduino
 board = nodemcuv2
-; upload_speed = 115200
+upload_speed = 115200
 build_flags = ${common.build_flags} -Wl,-Tesp8266.flash.4m1m.ld -D FIRMWARE_VARIANT=nodemcuv2
 extra_script = ${common.extra_script}
 lib_deps =

+ 25 - 10
src/main.cpp

@@ -6,6 +6,7 @@
 #include <IntParsing.h>
 #include <Size.h>
 #include <MiLightRadioConfig.h>
+#include <MiLightRemoteConfig.h>
 #include <MiLightHttpServer.h>
 #include <Settings.h>
 #include <MiLightUdpServer.h>
@@ -64,20 +65,26 @@ void initMilightUdpServers() {
   }
 }
 
-void onPacketSentHandler(uint8_t* packet, const MiLightRadioConfig& config) {
+void onPacketSentHandler(uint8_t* packet, const MiLightRemoteConfig& config) {
   StaticJsonBuffer<200> buffer;
   JsonObject& result = buffer.createObject();
   config.packetFormatter->parsePacket(packet, result);
 
+  if (!result.containsKey("device_id")
+    ||!result.containsKey("group_id")
+    ||!result.containsKey("device_type")) {
+    Serial.println(F("Skipping update because packet formatter didn't supply necessary information."));
+    return;
+  }
+
   uint16_t deviceId = result["device_id"];
   uint16_t groupId = result["group_id"];
-  MiLightRadioType type = MiLightRadioConfig::fromString(result["device_type"])->type;
 
   char output[200];
   result.printTo(output);
 
   if (mqttClient) {
-    mqttClient->sendUpdate(type, deviceId, groupId, output);
+    mqttClient->sendUpdate(config, deviceId, groupId, output);
   }
   httpServer->handlePacketSent(packet, config);
 }
@@ -87,17 +94,25 @@ void handleListen() {
     return;
   }
 
-  MiLightRadioConfig* config = MiLightRadioConfig::ALL_CONFIGS[
-    currentRadioType++ % MiLightRadioConfig::NUM_CONFIGS
-  ];
-  milightClient->prepare(*config);
+  MiLightRadio* radio = milightClient->switchRadio(currentRadioType++ % milightClient->getNumRadios());
 
   for (size_t i = 0; i < settings.listenRepeats; i++) {
     if (milightClient->available()) {
-      uint8_t readPacket[9];
-      milightClient->read(readPacket);
+      uint8_t readPacket[MILIGHT_MAX_PACKET_LENGTH];
+      size_t packetLen = milightClient->read(readPacket);
+
+      const MiLightRemoteConfig* remoteConfig = MiLightRemoteConfig::fromReceivedPacket(
+        radio->config(),
+        readPacket,
+        packetLen
+      );
+
+      if (remoteConfig == NULL) {
+        Serial.println(F("ERROR: Couldn't find remote for received packet!"));
+        return;
+      }
 
-      onPacketSentHandler(readPacket, *config);
+      onPacketSentHandler(readPacket, *remoteConfig);
     }
   }
 }

+ 3 - 1
web/src/css/style.css

@@ -2,7 +2,7 @@
 label { display: block; }
 .radio-option { padding: 0 5px; cursor: pointer; }
 .command-buttons { list-style: none; margin: 0; padding: 0; }
-.command-buttons li { display: inline-block; margin-right: 1em; }
+.command-buttons li { display: inline-block; margin-right: 1em; overflow: auto; }
 .form-entry { margin: 0 0 20px 0; }
 .form-entry .form-control { width: 20em; }
 .form-entry label { display: inline-block; }
@@ -11,6 +11,8 @@ label:not(.error) .error-info { display: none; }
 .error-info:before { content: '('; }
 .error-info:after { content: ')'; }
 .header-btn { margin: 20px; }
+.dropdown { position: initial; overflow: auto; }
+.dropdown-menu li { display: block; }
 #sniffed-traffic { max-height: 50em; overflow-y: auto; }
 .btn-secondary {
   background-color: #fff;

+ 37 - 10
web/src/index.html

@@ -81,7 +81,7 @@
 
          <div id="latest-version">
            <h4>Latest Version</h4>
-           
+
            <div class="status"></div>
            <div id="latest-version-info">
              <label>Version</label>
@@ -146,7 +146,7 @@
     <div>&nbsp;</div>
 
     <div class="row">
-      <div class="col-sm-4">
+      <div class="col-sm-3">
         <label for="deviceId" id="device-id-label">
           Device Id
           <span class="error-info"></span>
@@ -155,8 +155,8 @@
 				</select>
       </div>
 
-      <div class="col-sm-3">
-        <div class="mode-option" id="group-option" data-for="cct,rgbw,rgb_cct">
+      <div class="col-sm-4">
+        <div class="mode-option" id="group-option" data-for="cct,rgbw,rgb_cct,fut089">
           <label for="groupId">Group</label>
 
           <div class="btn-group" id="groupId" data-toggle="buttons">
@@ -172,6 +172,18 @@
             <label class="btn btn-secondary">
               <input type="radio" name="options" autocomplete="off" data-value="4"> 4
             </label>
+            <label class="btn btn-secondary mode-option" data-for="fut089">
+              <input type="radio" name="options" autocomplete="off" data-value="5"> 5
+            </label>
+            <label class="btn btn-secondary mode-option" data-for="fut089">
+              <input type="radio" name="options" autocomplete="off" data-value="6"> 6
+            </label>
+            <label class="btn btn-secondary mode-option" data-for="fut089">
+              <input type="radio" name="options" autocomplete="off" data-value="7"> 7
+            </label>
+            <label class="btn btn-secondary mode-option" data-for="fut089">
+              <input type="radio" name="options" autocomplete="off" data-value="8"> 8
+            </label>
             <label class="btn btn-secondary">
               <input type="radio" name="options" autocomplete="off" data-value="0"> All
             </label>
@@ -195,12 +207,15 @@
           <label class="btn btn-secondary">
             <input type="radio" name="mode" autocomplete="off" data-value="rgb"> RGB
           </label>
+          <label class="btn btn-secondary">
+            <input type="radio" name="mode" autocomplete="off" data-value="fut089"> FUT089
+          </label>
         </div>
       </div>
     </div>
 
     <div class="row"><div class="col-sm-12">
-    <div class="mode-option" data-for="rgbw,rgb_cct,rgb">
+    <div class="mode-option" data-for="rgbw,rgb_cct,rgb,fut089">
       <div class="row">
         <div class="col-sm-12">
           <h5>Hue</h5>
@@ -218,7 +233,7 @@
     </div>
     </div></div>
 
-    <div class="mode-option" data-for="rgb_cct">
+    <div class="mode-option" data-for="rgb_cct,fut089">
       <div class="row">
         <div class="col-sm-12">
           <h5>Saturation</h5>
@@ -235,7 +250,7 @@
       </div>
     </div>
 
-    <div class="mode-option" data-for="cct,rgb_cct">
+    <div class="mode-option" data-for="cct,rgb_cct,fut089">
       <div class="row">
         <div class="col-sm-12">
           <h5>Color Temperature</h5>
@@ -282,7 +297,7 @@
           <li>
             <input type="checkbox" name="status" class="raw-update" data-toggle="toggle" checked/>
           </li>
-          <div class="mode-option inline" data-for="rgbw,rgb_cct">
+          <div class="mode-option inline" data-for="rgbw,rgb_cct,fut089">
             <li>
               <button type="button" class="btn btn-secondary command-btn" data-command="set_white">White</button>
             </li>
@@ -302,7 +317,7 @@
         </ul>
         <p></p>
         <ul class="command-buttons">
-          <div class="mode-option inline" data-for="rgb,rgbw,rgb_cct">
+          <div class="mode-option inline" data-for="rgb">
             <li>
               <div class="plus-minus-group">
                 <button type="button" class="btn btn-default btn-number command-btn" data-command="previous_mode">
@@ -316,7 +331,19 @@
               </div>
             </li>
           </div>
-          <div class="mode-option inline" data-for="rgb,rgbw,rgb_cct">
+          <div class="mode-option inline" data-for="rgbw,rgb_cct,fut089">
+            <li>
+              <div class="dropdown">
+                <button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">
+                  Mode
+                  <span class="caret"></span>
+                </button>
+                <ul class="dropdown-menu mode-dropdown">
+                </ul>
+              </div>
+            </li>
+          </div>
+          <div class="mode-option inline" data-for="rgb,rgbw,rgb_cct,fut089">
             <li>
               <div class="plus-minus-group">
                 <button type="button" class="btn btn-default btn-number command-btn" data-command="mode_speed_down">

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

@@ -399,6 +399,16 @@ $(function() {
     $(this).closest('tr').remove();
   });
 
+  for (var i = 0; i < 9; i++) {
+    $('.mode-dropdown').append('<li><a href="#" data-mode-value="' + i + '">' + i + '</a></li>');
+  }
+
+  $('body').on('click', '.mode-dropdown li a', function(e) {
+    updateGroup({mode: $(this).data('mode-value')});
+    e.preventDefault();
+    return false;
+  });
+
   selectize = $('#deviceId').selectize({
     create: true,
     sortField: 'text',