소스 검색

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',