Bladeren bron

Internal improvements: upgrade to ArduinoJson v6, use upstream HTTP lib, etc... (#458)

* Use upstream library for webserver

* Use std::vector instead of raw arrays

* Fix memory leak when recreating MQTT client

* Fix memory leak when applying new settings

* Use upstream lib for routes

* Upgrade to ArduinoJson v6

* don't use custom method -- client() was exposed in upstream library

* add tests

* true/flase -> on/off

* use builtin OTA handler

* add passthrough authprovider

* remove unused class def

* stick with SDK 2.4 for now
Chris Mullins 6 jaren geleden
bovenliggende
commit
ba9aa46f9d
42 gewijzigde bestanden met toevoegingen van 561 en 705 verwijderingen
  1. 2 2
      dist/index.html.gz.h
  2. 19 4
      lib/Helpers/JsonHelpers.h
  3. 5 4
      lib/MQTT/BulbStateUpdater.cpp
  4. 22 23
      lib/MQTT/MqttClient.cpp
  5. 1 1
      lib/MQTT/MqttClient.h
  6. 1 1
      lib/MiLight/CctPacketFormatter.cpp
  7. 1 1
      lib/MiLight/CctPacketFormatter.h
  8. 1 1
      lib/MiLight/FUT089PacketFormatter.cpp
  9. 1 1
      lib/MiLight/FUT089PacketFormatter.h
  10. 1 1
      lib/MiLight/FUT091PacketFormatter.cpp
  11. 1 1
      lib/MiLight/FUT091PacketFormatter.h
  12. 18 21
      lib/MiLight/MiLightClient.cpp
  13. 8 11
      lib/MiLight/MiLightClient.h
  14. 1 1
      lib/MiLight/PacketFormatter.cpp
  15. 1 1
      lib/MiLight/PacketFormatter.h
  16. 1 1
      lib/MiLight/RgbCctPacketFormatter.cpp
  17. 1 1
      lib/MiLight/RgbCctPacketFormatter.h
  18. 1 1
      lib/MiLight/RgbPacketFormatter.cpp
  19. 1 1
      lib/MiLight/RgbPacketFormatter.h
  20. 1 1
      lib/MiLight/RgbwPacketFormatter.cpp
  21. 1 1
      lib/MiLight/RgbwPacketFormatter.h
  22. 18 21
      lib/MiLightState/GroupState.cpp
  23. 7 7
      lib/MiLightState/GroupState.h
  24. 14 14
      lib/Radio/MiLightRadioFactory.cpp
  25. 5 4
      lib/Radio/MiLightRadioFactory.h
  26. 4 5
      lib/Settings/AboutHelper.cpp
  27. 1 1
      lib/Settings/AboutHelper.h
  28. 126 151
      lib/Settings/Settings.cpp
  29. 24 41
      lib/Settings/Settings.h
  30. 6 6
      lib/Udp/MiLightDiscoveryServer.cpp
  31. 3 3
      lib/Udp/MiLightUdpServer.cpp
  32. 3 1
      lib/Udp/MiLightUdpServer.h
  33. 150 164
      lib/WebServer/MiLightHttpServer.cpp
  34. 31 31
      lib/WebServer/MiLightHttpServer.h
  35. 0 68
      lib/WebServer/WebServer.cpp
  36. 0 42
      lib/WebServer/WebServer.h
  37. 13 5
      platformio.ini
  38. 20 32
      src/main.cpp
  39. 19 0
      test/remote/spec/rest_spec.rb
  40. 23 3
      test/remote/spec/settings_spec.rb
  41. 4 25
      web/src/index.html
  42. 1 1
      web/src/js/script.js

File diff suppressed because it is too large
+ 2 - 2
dist/index.html.gz.h


+ 19 - 4
lib/Helpers/JsonHelpers.h

@@ -9,11 +9,26 @@
 class JsonHelpers {
 public:
   template<typename T>
-  static std::vector<T> jsonArrToVector(JsonArray& arr, std::function<T (const String&)> converter, const bool unique = true) {
+  static void copyFrom(JsonArray arr, std::vector<T> vec) {
+    for (typename std::vector<T>::const_iterator it = vec.begin(); it != vec.end(); ++it) {
+      arr.add(*it);
+    }
+  }
+
+  template<typename T>
+  static void copyTo(JsonArray arr, std::vector<T> vec) {
+    for (size_t i = 0; i < arr.size(); ++i) {
+      JsonVariant val = arr[i];
+      vec.push_back(val.as<T>());
+    }
+  }
+
+  template<typename T, typename StrType>
+  static std::vector<T> jsonArrToVector(JsonArray& arr, std::function<T (const StrType)> converter, const bool unique = true) {
     std::vector<T> vec;
 
     for (size_t i = 0; i < arr.size(); ++i) {
-      String strVal = arr.get<const char*>(i);
+      StrType strVal = arr[i];
       T convertedVal = converter(strVal);
 
       // inefficient, but everything using this is tiny, so doesn't matter
@@ -25,8 +40,8 @@ public:
     return vec;
   }
 
-  template<typename T>
-  static void vectorToJsonArr(JsonArray& arr, const std::vector<T>& vec, std::function<String (const T&)> converter) {
+  template<typename T, typename StrType>
+  static void vectorToJsonArr(JsonArray& arr, const std::vector<T>& vec, std::function<StrType (const T&)> converter) {
     for (typename std::vector<T>::const_iterator it = vec.begin(); it != vec.end(); ++it) {
       arr.add(converter(*it));
     }

+ 5 - 4
lib/MQTT/BulbStateUpdater.cpp

@@ -39,10 +39,11 @@ void BulbStateUpdater::loop() {
 
 inline void BulbStateUpdater::flushGroup(BulbId bulbId, GroupState& state) {
   char buffer[200];
-  StaticJsonBuffer<200> jsonBuffer;
-  JsonObject& message = jsonBuffer.createObject();
-  state.applyState(message, bulbId, settings.groupStateFields, settings.numGroupStateFields);
-  message.printTo(buffer);
+  StaticJsonDocument<200> json;
+  JsonObject message = json.to<JsonObject>();
+
+  state.applyState(message, bulbId, settings.groupStateFields);
+  serializeJson(json, buffer);
 
   mqttClient.sendState(
     *MiLightRemoteConfig::fromType(bulbId.deviceType),

+ 22 - 23
lib/MQTT/MqttClient.cpp

@@ -13,21 +13,20 @@ static const char* STATUS_DISCONNECTED = "disconnected_clean";
 static const char* STATUS_LWT_DISCONNECTED = "disconnected_unclean";
 
 MqttClient::MqttClient(Settings& settings, MiLightClient*& milightClient)
-  : milightClient(milightClient),
+  : mqttClient(tcpClient),
+    milightClient(milightClient),
     settings(settings),
     lastConnectAttempt(0)
 {
   String strDomain = settings.mqttServer();
   this->domain = new char[strDomain.length() + 1];
   strcpy(this->domain, strDomain.c_str());
-
-  this->mqttClient = new PubSubClient(tcpClient);
 }
 
 MqttClient::~MqttClient() {
   String aboutStr = generateConnectionStatusMessage(STATUS_DISCONNECTED);
-  mqttClient->publish(settings.mqttClientStatusTopic.c_str(), aboutStr.c_str(), true);
-  mqttClient->disconnect();
+  mqttClient.publish(settings.mqttClientStatusTopic.c_str(), aboutStr.c_str(), true);
+  mqttClient.disconnect();
   delete this->domain;
 }
 
@@ -41,8 +40,8 @@ void MqttClient::begin() {
   );
 #endif
 
-  mqttClient->setServer(this->domain, settings.mqttPort());
-  mqttClient->setCallback(
+  mqttClient.setServer(this->domain, settings.mqttPort());
+  mqttClient.setCallback(
     [this](char* topic, byte* payload, int length) {
       this->publishCallback(topic, payload, length);
     }
@@ -59,7 +58,7 @@ bool MqttClient::connect() {
 #endif
 
   if (settings.mqttUsername.length() > 0 && settings.mqttClientStatusTopic.length() > 0) {
-    return mqttClient->connect(
+    return mqttClient.connect(
       nameBuffer,
       settings.mqttUsername.c_str(),
       settings.mqttPassword.c_str(),
@@ -69,13 +68,13 @@ bool MqttClient::connect() {
       generateConnectionStatusMessage(STATUS_LWT_DISCONNECTED).c_str()
     );
   } else if (settings.mqttUsername.length() > 0) {
-    return mqttClient->connect(
+    return mqttClient.connect(
       nameBuffer,
       settings.mqttUsername.c_str(),
       settings.mqttPassword.c_str()
     );
   } else if (settings.mqttClientStatusTopic.length() > 0) {
-    return mqttClient->connect(
+    return mqttClient.connect(
       nameBuffer,
       settings.mqttClientStatusTopic.c_str(),
       2,
@@ -83,14 +82,14 @@ bool MqttClient::connect() {
       generateConnectionStatusMessage(STATUS_LWT_DISCONNECTED).c_str()
     );
   } else {
-    return mqttClient->connect(nameBuffer);
+    return mqttClient.connect(nameBuffer);
   }
 }
 
 void MqttClient::sendBirthMessage() {
   if (settings.mqttClientStatusTopic.length() > 0) {
     String aboutStr = generateConnectionStatusMessage(STATUS_CONNECTED);
-    mqttClient->publish(settings.mqttClientStatusTopic.c_str(), aboutStr.c_str(), true);
+    mqttClient.publish(settings.mqttClientStatusTopic.c_str(), aboutStr.c_str(), true);
   }
 }
 
@@ -99,7 +98,7 @@ void MqttClient::reconnect() {
     return;
   }
 
-  if (! mqttClient->connected()) {
+  if (! mqttClient.connected()) {
     if (connect()) {
       subscribe();
       sendBirthMessage();
@@ -117,7 +116,7 @@ void MqttClient::reconnect() {
 
 void MqttClient::handleClient() {
   reconnect();
-  mqttClient->loop();
+  mqttClient.loop();
 }
 
 void MqttClient::sendUpdate(const MiLightRemoteConfig& remoteConfig, uint16_t deviceId, uint16_t groupId, const char* update) {
@@ -141,7 +140,7 @@ void MqttClient::subscribe() {
   printf_P(PSTR("MqttClient - subscribing to topic: %s\n"), topic.c_str());
 #endif
 
-  mqttClient->subscribe(topic.c_str());
+  mqttClient.subscribe(topic.c_str());
 }
 
 void MqttClient::publish(
@@ -163,7 +162,7 @@ void MqttClient::publish(
   printf("MqttClient - publishing update to %s\n", topic.c_str());
 #endif
 
-  mqttClient->publish(topic.c_str(), message, retain);
+  mqttClient.publish(topic.c_str(), message, retain);
 }
 
 void MqttClient::publishCallback(char* topic, byte* payload, int length) {
@@ -208,8 +207,9 @@ void MqttClient::publishCallback(char* topic, byte* payload, int length) {
     Serial.println(F("MqttClient - WARNING: could not find device_type token.  Defaulting to FUT092.\n"));
   }
 
-  StaticJsonBuffer<400> buffer;
-  JsonObject& obj = buffer.parseObject(cstrPayload);
+  StaticJsonDocument<400> buffer;
+  deserializeJson(buffer, cstrPayload);
+  JsonObject obj = buffer.as<JsonObject>();
 
 #ifdef MQTT_DEBUG
   printf("MqttClient - device %04X, group %u\n", deviceId, groupId);
@@ -237,15 +237,14 @@ inline void MqttClient::bindTopicString(
 }
 
 String MqttClient::generateConnectionStatusMessage(const char* connectionStatus) {
-  DynamicJsonBuffer buffer;
-  JsonObject& status = buffer.createObject();
-  status["status"] = connectionStatus;
+  StaticJsonDocument<1024> json;
+  json["status"] = connectionStatus;
 
   // Fill other fields
-  AboutHelper::generateAboutObject(status, true);
+  AboutHelper::generateAboutObject(json, true);
 
   String response;
-  status.printTo(response);
+  serializeJson(json, response);
 
   return response;
 }

+ 1 - 1
lib/MQTT/MqttClient.h

@@ -24,7 +24,7 @@ public:
 
 private:
   WiFiClient tcpClient;
-  PubSubClient* mqttClient;
+  PubSubClient mqttClient;
   MiLightClient*& milightClient;
   Settings& settings;
   char* domain;

+ 1 - 1
lib/MiLight/CctPacketFormatter.cpp

@@ -189,7 +189,7 @@ MiLightStatus CctPacketFormatter::cctCommandToStatus(uint8_t command) {
   }
 }
 
-BulbId CctPacketFormatter::parsePacket(const uint8_t* packet, JsonObject& result) {
+BulbId CctPacketFormatter::parsePacket(const uint8_t* packet, JsonObject result) {
   uint8_t command = packet[CCT_COMMAND_INDEX] & 0x7F;
 
   uint8_t onOffGroupId = cctCommandIdToGroup(command);

+ 1 - 1
lib/MiLight/CctPacketFormatter.h

@@ -46,7 +46,7 @@ public:
   virtual void format(uint8_t const* packet, char* buffer);
   virtual void initializePacket(uint8_t* packet);
   virtual void finalizePacket(uint8_t* packet);
-  virtual BulbId parsePacket(const uint8_t* packet, JsonObject& result);
+  virtual BulbId parsePacket(const uint8_t* packet, JsonObject result);
 
   static uint8_t getCctStatusButton(uint8_t groupId, MiLightStatus status);
   static uint8_t cctCommandIdToGroup(uint8_t command);

+ 1 - 1
lib/MiLight/FUT089PacketFormatter.cpp

@@ -91,7 +91,7 @@ void FUT089PacketFormatter::enableNightMode() {
   command(FUT089_ON | 0x80, arg);
 }
 
-BulbId FUT089PacketFormatter::parsePacket(const uint8_t *packet, JsonObject& result) {
+BulbId FUT089PacketFormatter::parsePacket(const uint8_t *packet, JsonObject result) {
   if (stateStore == NULL) {
     Serial.println(F("ERROR: stateStore not set.  Prepare was not called!  **THIS IS A BUG**"));
     BulbId fakeId(0, 0, REMOTE_TYPE_FUT089);

+ 1 - 1
lib/MiLight/FUT089PacketFormatter.h

@@ -39,7 +39,7 @@ public:
   virtual void modeSpeedUp();
   virtual void updateMode(uint8_t mode);
 
-  virtual BulbId parsePacket(const uint8_t* packet, JsonObject& result);
+  virtual BulbId parsePacket(const uint8_t* packet, JsonObject result);
 };
 
 #endif

+ 1 - 1
lib/MiLight/FUT091PacketFormatter.cpp

@@ -18,7 +18,7 @@ void FUT091PacketFormatter::enableNightMode() {
   command(static_cast<uint8_t>(FUT091Command::ON_OFF) | 0x80, arg);
 }
 
-BulbId FUT091PacketFormatter::parsePacket(const uint8_t *packet, JsonObject& result) {
+BulbId FUT091PacketFormatter::parsePacket(const uint8_t *packet, JsonObject result) {
   uint8_t packetCopy[V2_PACKET_LEN];
   memcpy(packetCopy, packet, V2_PACKET_LEN);
   V2RFEncoding::decodeV2Packet(packetCopy);

+ 1 - 1
lib/MiLight/FUT091PacketFormatter.h

@@ -19,7 +19,7 @@ public:
   virtual void updateTemperature(uint8_t value);
   virtual void enableNightMode();
 
-  virtual BulbId parsePacket(const uint8_t* packet, JsonObject& result);
+  virtual BulbId parsePacket(const uint8_t* packet, JsonObject result);
 };
 
 #endif

+ 18 - 21
lib/MiLight/MiLightClient.cpp

@@ -6,13 +6,12 @@
 #include <TokenIterator.h>
 
 MiLightClient::MiLightClient(
-  MiLightRadioFactory* radioFactory,
+  std::shared_ptr<MiLightRadioFactory> radioFactory,
   GroupStateStore* stateStore,
   Settings* settings
 )
   : currentRadio(NULL),
     currentRemote(NULL),
-    numRadios(MiLightRadioConfig::NUM_CONFIGS),
     packetSentHandler(NULL),
     updateBeginHandler(NULL),
     updateEndHandler(NULL),
@@ -21,15 +20,13 @@ MiLightClient::MiLightClient(
     lastSend(0),
     baseResendCount(MILIGHT_DEFAULT_RESEND_COUNT)
 {
-  radios = new MiLightRadio*[numRadios];
-
-  for (size_t i = 0; i < numRadios; i++) {
-    radios[i] = radioFactory->create(MiLightRadioConfig::ALL_CONFIGS[i]);
+  for (size_t i = 0; i < MiLightRadioConfig::NUM_CONFIGS; i++) {
+    radios.push_back(radioFactory->create(MiLightRadioConfig::ALL_CONFIGS[i]));
   }
 }
 
 void MiLightClient::begin() {
-  for (size_t i = 0; i < numRadios; i++) {
+  for (size_t i = 0; i < radios.size(); i++) {
     radios[i]->begin();
   }
 
@@ -49,10 +46,10 @@ void MiLightClient::setHeld(bool held) {
 }
 
 size_t MiLightClient::getNumRadios() const {
-  return numRadios;
+  return radios.size();
 }
 
-MiLightRadio* MiLightClient::switchRadio(size_t radioIx) {
+std::shared_ptr<MiLightRadio> MiLightClient::switchRadio(size_t radioIx) {
   if (radioIx >= getNumRadios()) {
     return NULL;
   }
@@ -65,10 +62,10 @@ MiLightRadio* MiLightClient::switchRadio(size_t radioIx) {
   return this->currentRadio;
 }
 
-MiLightRadio* MiLightClient::switchRadio(const MiLightRemoteConfig* remoteConfig) {
-  MiLightRadio* radio = NULL;
+std::shared_ptr<MiLightRadio> MiLightClient::switchRadio(const MiLightRemoteConfig* remoteConfig) {
+  std::shared_ptr<MiLightRadio> radio = NULL;
 
-  for (size_t i = 0; i < numRadios; i++) {
+  for (size_t i = 0; i < radios.size(); i++) {
     if (&this->radios[i]->config() == &remoteConfig->radioConfig) {
       radio = switchRadio(i);
       break;
@@ -330,7 +327,7 @@ void MiLightClient::toggleStatus() {
   flushPacket();
 }
 
-void MiLightClient::update(const JsonObject& request) {
+void MiLightClient::update(JsonObject request) {
   if (this->updateBeginHandler) {
     this->updateBeginHandler();
   }
@@ -347,11 +344,11 @@ void MiLightClient::update(const JsonObject& request) {
   }
 
   if (request.containsKey("commands")) {
-    JsonArray& commands = request["commands"];
+    JsonArray commands = request["commands"];
 
-    if (commands.success()) {
+    if (! commands.isNull()) {
       for (size_t i = 0; i < commands.size(); i++) {
-        this->handleCommand(commands.get<String>(i));
+        this->handleCommand(commands[i].as<const char*>());
       }
     }
   }
@@ -373,7 +370,7 @@ void MiLightClient::update(const JsonObject& request) {
     uint16_t r, g, b;
 
     if (request["color"].is<JsonObject>()) {
-      JsonObject& color = request["color"];
+      JsonObject color = request["color"];
 
       r = color["r"];
       g = color["g"];
@@ -422,7 +419,7 @@ void MiLightClient::update(const JsonObject& request) {
   }
   // HomeAssistant
   if (request.containsKey("brightness")) {
-    uint8_t scaledBrightness = Units::rescale(request.get<uint8_t>("brightness"), 100, 255);
+    uint8_t scaledBrightness = Units::rescale(request["brightness"].as<uint8_t>(), 100, 255);
     this->updateBrightness(scaledBrightness);
   }
 
@@ -498,13 +495,13 @@ void MiLightClient::handleEffect(const String& effect) {
   }
 }
 
-uint8_t MiLightClient::parseStatus(const JsonObject& object) {
+uint8_t MiLightClient::parseStatus(JsonObject object) {
   String strStatus;
 
   if (object.containsKey("status")) {
-    strStatus = object.get<char*>("status");
+    strStatus = object["status"].as<char*>();
   } else if (object.containsKey("state")) {
-    strStatus = object.get<char*>("state");
+    strStatus = object["state"].as<char*>();
   } else {
     return 255;
   }

+ 8 - 11
lib/MiLight/MiLightClient.h

@@ -20,14 +20,12 @@
 class MiLightClient {
 public:
   MiLightClient(
-    MiLightRadioFactory* radioFactory,
+    std::shared_ptr<MiLightRadioFactory> radioFactory,
     GroupStateStore* stateStore,
     Settings* settings
   );
 
-  ~MiLightClient() {
-    delete[] radios;
-  }
+  ~MiLightClient() { }
 
   typedef std::function<void(uint8_t* packet, const MiLightRemoteConfig& config)> PacketSentHandler;
   typedef std::function<void(void)> EventHandler;
@@ -72,7 +70,7 @@ public:
 
   void updateSaturation(const uint8_t saturation);
 
-  void update(const JsonObject& object);
+  void update(JsonObject object);
   void handleCommand(const String& command);
   void handleEffect(const String& effect);
 
@@ -81,15 +79,15 @@ public:
   void onUpdateEnd(EventHandler handler);
 
   size_t getNumRadios() const;
-  MiLightRadio* switchRadio(size_t radioIx);
+  std::shared_ptr<MiLightRadio> switchRadio(size_t radioIx);
+  std::shared_ptr<MiLightRadio> switchRadio(const MiLightRemoteConfig* remoteConfig);
   MiLightRemoteConfig& currentRemoteConfig() const;
 
 protected:
 
-  MiLightRadio** radios;
-  MiLightRadio* currentRadio;
+  std::vector<std::shared_ptr<MiLightRadio>> radios;
+  std::shared_ptr<MiLightRadio> currentRadio;
   const MiLightRemoteConfig* currentRemote;
-  const size_t numRadios;
 
   PacketSentHandler packetSentHandler;
   EventHandler updateBeginHandler;
@@ -122,8 +120,7 @@ protected:
    */
   void updateResendCount();
 
-  MiLightRadio* switchRadio(const MiLightRemoteConfig* remoteConfig);
-  uint8_t parseStatus(const JsonObject& object);
+  uint8_t parseStatus(JsonObject object);
 
   void flushPacket();
 };

+ 1 - 1
lib/MiLight/PacketFormatter.cpp

@@ -80,7 +80,7 @@ void PacketFormatter::enableNightMode() { }
 void PacketFormatter::updateTemperature(uint8_t value) { }
 void PacketFormatter::updateSaturation(uint8_t value) { }
 
-BulbId PacketFormatter::parsePacket(const uint8_t *packet, JsonObject &result) {
+BulbId PacketFormatter::parsePacket(const uint8_t *packet, JsonObject result) {
   return DEFAULT_BULB_ID;
 }
 

+ 1 - 1
lib/MiLight/PacketFormatter.h

@@ -83,7 +83,7 @@ public:
   virtual void prepare(uint16_t deviceId, uint8_t groupId);
   virtual void format(uint8_t const* packet, char* buffer);
 
-  virtual BulbId parsePacket(const uint8_t* packet, JsonObject& result);
+  virtual BulbId parsePacket(const uint8_t* packet, JsonObject result);
 
   static void formatV1Packet(uint8_t const* packet, char* buffer);
 

+ 1 - 1
lib/MiLight/RgbCctPacketFormatter.cpp

@@ -106,7 +106,7 @@ void RgbCctPacketFormatter::enableNightMode() {
   command(RGB_CCT_ON | 0x80, arg);
 }
 
-BulbId RgbCctPacketFormatter::parsePacket(const uint8_t *packet, JsonObject& result) {
+BulbId RgbCctPacketFormatter::parsePacket(const uint8_t *packet, JsonObject result) {
   uint8_t packetCopy[V2_PACKET_LEN];
   memcpy(packetCopy, packet, V2_PACKET_LEN);
   V2RFEncoding::decodeV2Packet(packetCopy);

+ 1 - 1
lib/MiLight/RgbCctPacketFormatter.h

@@ -50,7 +50,7 @@ public:
   virtual void nextMode();
   virtual void previousMode();
 
-  virtual BulbId parsePacket(const uint8_t* packet, JsonObject& result);
+  virtual BulbId parsePacket(const uint8_t* packet, JsonObject result);
 
 protected:
 

+ 1 - 1
lib/MiLight/RgbPacketFormatter.cpp

@@ -83,7 +83,7 @@ void RgbPacketFormatter::previousMode() {
   command(RGB_MODE_DOWN, 0);
 }
 
-BulbId RgbPacketFormatter::parsePacket(const uint8_t* packet, JsonObject& result) {
+BulbId RgbPacketFormatter::parsePacket(const uint8_t* packet, JsonObject result) {
   uint8_t command = packet[RGB_COMMAND_INDEX] & 0x7F;
 
   BulbId bulbId(

+ 1 - 1
lib/MiLight/RgbPacketFormatter.h

@@ -39,7 +39,7 @@ public:
   virtual void modeSpeedUp();
   virtual void nextMode();
   virtual void previousMode();
-  virtual BulbId parsePacket(const uint8_t* packet, JsonObject& result);
+  virtual BulbId parsePacket(const uint8_t* packet, JsonObject result);
 
   virtual void initializePacket(uint8_t* packet);
 };

+ 1 - 1
lib/MiLight/RgbwPacketFormatter.cpp

@@ -100,7 +100,7 @@ void RgbwPacketFormatter::enableNightMode() {
   command(button | 0x10, 0);
 }
 
-BulbId RgbwPacketFormatter::parsePacket(const uint8_t* packet, JsonObject& result) {
+BulbId RgbwPacketFormatter::parsePacket(const uint8_t* packet, JsonObject result) {
   uint8_t command = packet[RGBW_COMMAND_INDEX] & 0x7F;
 
   BulbId bulbId(

+ 1 - 1
lib/MiLight/RgbwPacketFormatter.h

@@ -70,7 +70,7 @@ public:
   virtual void previousMode();
   virtual void updateMode(uint8_t mode);
   virtual void enableNightMode();
-  virtual BulbId parsePacket(const uint8_t* packet, JsonObject& result);
+  virtual BulbId parsePacket(const uint8_t* packet, JsonObject result);
 
   virtual void initializePacket(uint8_t* packet);
 

+ 18 - 21
lib/MiLightState/GroupState.cpp

@@ -133,7 +133,7 @@ GroupState::GroupState(const GroupState& other)
   scratchpad.rawData = other.scratchpad.rawData;
 }
 
-GroupState::GroupState(const GroupState* previousState, const JsonObject& jsonState)
+GroupState::GroupState(const GroupState* previousState, JsonObject jsonState)
   : previousState(previousState)
 {
   initFields();
@@ -735,12 +735,12 @@ void GroupState::patch(const GroupState& other) {
 
   Returns true if the packet changes affects a state change
 */
-bool GroupState::patch(const JsonObject& state) {
+bool GroupState::patch(JsonObject state) {
   bool changes = false;
 
 #ifdef STATE_DEBUG
   Serial.print(F("Patching existing state with: "));
-  state.printTo(Serial);
+  serializeJson(state, Serial);
   Serial.println();
 #endif
 
@@ -753,7 +753,7 @@ bool GroupState::patch(const JsonObject& state) {
   // changes to devices we know are off.
 
   if (isOn() && state.containsKey("brightness")) {
-    bool stateChange = setBrightness(Units::rescale(state.get<uint8_t>("brightness"), 100, 255));
+    bool stateChange = setBrightness(Units::rescale(state["brightness"].as<uint8_t>(), 100, 255));
     changes |= stateChange;
   }
   if (isOn() && state.containsKey("hue")) {
@@ -802,7 +802,7 @@ bool GroupState::patch(const JsonObject& state) {
   return changes;
 }
 
-void GroupState::applyColor(ArduinoJson::JsonObject& state) const {
+void GroupState::applyColor(JsonObject state) const {
   uint8_t rgb[3];
   RGBConverter converter;
   converter.hsvToRgb(
@@ -815,14 +815,14 @@ void GroupState::applyColor(ArduinoJson::JsonObject& state) const {
   applyColor(state, rgb[0], rgb[1], rgb[2]);
 }
 
-void GroupState::applyColor(ArduinoJson::JsonObject& state, uint8_t r, uint8_t g, uint8_t b) const {
-  JsonObject& color = state.createNestedObject("color");
+void GroupState::applyColor(JsonObject state, uint8_t r, uint8_t g, uint8_t b) const {
+  JsonObject color = state.createNestedObject("color");
   color["r"] = r;
   color["g"] = g;
   color["b"] = b;
 }
 
-void GroupState::applyOhColor(ArduinoJson::JsonObject& state) const {
+void GroupState::applyOhColor(JsonObject state) const {
   uint8_t rgb[3];
   RGBConverter converter;
   converter.hsvToRgb(
@@ -838,7 +838,7 @@ void GroupState::applyOhColor(ArduinoJson::JsonObject& state) const {
 }
 
 // gather partial state for a single field; see GroupState::applyState to gather many fields
-void GroupState::applyField(JsonObject& partialState, const BulbId& bulbId, GroupStateField field) const {
+void GroupState::applyField(JsonObject partialState, const BulbId& bulbId, GroupStateField field) const {
   if (isSetField(field)) {
     switch (field) {
       case GroupStateField::STATE:
@@ -946,10 +946,11 @@ void GroupState::applyField(JsonObject& partialState, const BulbId& bulbId, Grou
 void GroupState::debugState(char const *debugMessage) const {
 #ifdef STATE_DEBUG
   // using static to keep large buffers off the call stack
-  static StaticJsonBuffer<500> jsonBuffer;
+  StaticJsonDocument<500> jsonDoc;
+  JsonObject jsonState = jsonDoc.to<JsonObject>();
 
   // define fields to show (if count changes, make sure to update count to applyState below)
-  GroupStateField fields[] {
+  std::vector<GroupStateField> fields({
       GroupStateField::LEVEL,
       GroupStateField::BULB_MODE,
       GroupStateField::COLOR_TEMP,
@@ -959,20 +960,16 @@ void GroupState::debugState(char const *debugMessage) const {
       GroupStateField::MODE,
       GroupStateField::SATURATION,
       GroupStateField::STATE
-  };
-
-  // since our buffer is reused, make sure to clear it every time
-  jsonBuffer.clear();
-  JsonObject& jsonState = jsonBuffer.createObject();
+  });
 
   // Fake id
   BulbId id;
 
   // use applyState to build JSON of all fields (from above)
-  applyState(jsonState, id, fields, size(fields));
+  applyState(jsonState, id, fields);
   // convert to string and print
   Serial.printf("%s: ", debugMessage);
-  jsonState.printTo(Serial);
+  serializeJson(jsonState, Serial);
   Serial.println("");
   Serial.printf("Raw data: %08X %08X\n", state.rawData[0], state.rawData[1]);
 #endif
@@ -980,8 +977,8 @@ void GroupState::debugState(char const *debugMessage) const {
 
 // build up a partial state representation based on the specified GrouipStateField array.  Used
 // to gather a subset of states (configurable in the UI) for sending to MQTT and web responses.
-void GroupState::applyState(JsonObject& partialState, const BulbId& bulbId, GroupStateField* fields, size_t numFields) const {
-  for (size_t i = 0; i < numFields; i++) {
-    applyField(partialState, bulbId, fields[i]);
+void GroupState::applyState(JsonObject partialState, const BulbId& bulbId, std::vector<GroupStateField>& fields) const {
+  for (std::vector<GroupStateField>::const_iterator itr = fields.begin(); itr != fields.end(); ++itr) {
+    applyField(partialState, bulbId, *itr);
   }
 }

+ 7 - 7
lib/MiLightState/GroupState.h

@@ -44,7 +44,7 @@ public:
 
   // Convenience constructor that patches transient state from a previous GroupState,
   // and defaults with JSON state
-  GroupState(const GroupState* previousState, const JsonObject& jsonState);
+  GroupState(const GroupState* previousState, JsonObject jsonState);
 
   void initFields();
 
@@ -124,14 +124,14 @@ public:
 
   // Patches this state with the fields defined in the JSON state.  Returns
   // true if there were any changes.
-  bool patch(const JsonObject& state);
+  bool patch(JsonObject state);
 
   // It's a little weird to need to pass in a BulbId here.  The purpose is to
   // support fields like DEVICE_ID, which aren't otherweise available to the
   // state in this class.  The alternative is to have every GroupState object
   // keep a reference to its BulbId, which feels too heavy-weight.
-  void applyField(JsonObject& state, const BulbId& bulbId, GroupStateField field) const;
-  void applyState(JsonObject& state, const BulbId& bulbId, GroupStateField* fields, size_t numFields) const;
+  void applyField(JsonObject state, const BulbId& bulbId, GroupStateField field) const;
+  void applyState(JsonObject state, const BulbId& bulbId, std::vector<GroupStateField>& fields) const;
 
   // Attempt to keep track of increment commands in such a way that we can
   // know what state it's in.  When we get an increment command (like "increase
@@ -210,10 +210,10 @@ private:
   // it here.
   const GroupState* previousState;
 
-  void applyColor(JsonObject& state, uint8_t r, uint8_t g, uint8_t b) const;
-  void applyColor(JsonObject& state) const;
+  void applyColor(JsonObject state, uint8_t r, uint8_t g, uint8_t b) const;
+  void applyColor(JsonObject state) const;
   // Apply OpenHAB-style color, e.g., {"color":"0,0,0"}
-  void applyOhColor(JsonObject& state) const;
+  void applyOhColor(JsonObject state) const;
 };
 
 extern const BulbId DEFAULT_BULB_ID;

+ 14 - 14
lib/Radio/MiLightRadioFactory.cpp

@@ -1,18 +1,18 @@
 #include <MiLightRadioFactory.h>
 
-MiLightRadioFactory* MiLightRadioFactory::fromSettings(const Settings& settings) {
+std::shared_ptr<MiLightRadioFactory> MiLightRadioFactory::fromSettings(const Settings& settings) {
   switch (settings.radioInterfaceType) {
     case nRF24:
-      return new NRF24Factory(
-        settings.csnPin, 
-        settings.cePin, 
-        settings.rf24PowerLevel, 
+      return std::make_shared<NRF24Factory>(
+        settings.csnPin,
+        settings.cePin,
+        settings.rf24PowerLevel,
         settings.rf24Channels,
         settings.rf24ListenChannel
       );
 
     case LT8900:
-      return new LT8900Factory(settings.csnPin, settings.resetPin, settings.cePin);
+      return std::make_shared<LT8900Factory>(settings.csnPin, settings.resetPin, settings.cePin);
 
     default:
       return NULL;
@@ -20,21 +20,21 @@ MiLightRadioFactory* MiLightRadioFactory::fromSettings(const Settings& settings)
 }
 
 NRF24Factory::NRF24Factory(
-  uint8_t csnPin, 
-  uint8_t cePin, 
-  RF24PowerLevel rF24PowerLevel, 
+  uint8_t csnPin,
+  uint8_t cePin,
+  RF24PowerLevel rF24PowerLevel,
   const std::vector<RF24Channel>& channels,
   RF24Channel listenChannel
 )
 : rf24(RF24(cePin, csnPin)),
   channels(channels),
   listenChannel(listenChannel)
-{ 
+{
   rf24.setPALevel(RF24PowerLevelHelpers::rf24ValueFromValue(rF24PowerLevel));
 }
 
-MiLightRadio* NRF24Factory::create(const MiLightRadioConfig &config) {
-  return new NRF24MiLightRadio(rf24, config, channels, listenChannel);
+std::shared_ptr<MiLightRadio> NRF24Factory::create(const MiLightRadioConfig &config) {
+  return std::make_shared<NRF24MiLightRadio>(rf24, config, channels, listenChannel);
 }
 
 LT8900Factory::LT8900Factory(uint8_t csPin, uint8_t resetPin, uint8_t pktFlag)
@@ -43,6 +43,6 @@ LT8900Factory::LT8900Factory(uint8_t csPin, uint8_t resetPin, uint8_t pktFlag)
     _pktFlag(pktFlag)
 { }
 
-MiLightRadio* LT8900Factory::create(const MiLightRadioConfig& config) {
-  return new LT8900MiLightRadio(_csPin, _resetPin, _pktFlag, config);
+std::shared_ptr<MiLightRadio> LT8900Factory::create(const MiLightRadioConfig& config) {
+  return std::make_shared<LT8900MiLightRadio>(_csPin, _resetPin, _pktFlag, config);
 }

+ 5 - 4
lib/Radio/MiLightRadioFactory.h

@@ -8,6 +8,7 @@
 #include <RF24Channel.h>
 #include <Settings.h>
 #include <vector>
+#include <memory>
 
 #ifndef _MILIGHT_RADIO_FACTORY_H
 #define _MILIGHT_RADIO_FACTORY_H
@@ -16,9 +17,9 @@ class MiLightRadioFactory {
 public:
 
   virtual ~MiLightRadioFactory() { };
-  virtual MiLightRadio* create(const MiLightRadioConfig& config) = 0;
+  virtual std::shared_ptr<MiLightRadio> create(const MiLightRadioConfig& config) = 0;
 
-  static MiLightRadioFactory* fromSettings(const Settings& settings);
+  static std::shared_ptr<MiLightRadioFactory> fromSettings(const Settings& settings);
 
 };
 
@@ -33,7 +34,7 @@ public:
     RF24Channel listenChannel
   );
 
-  virtual MiLightRadio* create(const MiLightRadioConfig& config);
+  virtual std::shared_ptr<MiLightRadio> create(const MiLightRadioConfig& config);
 
 protected:
 
@@ -48,7 +49,7 @@ public:
 
   LT8900Factory(uint8_t csPin, uint8_t resetPin, uint8_t pktFlag);
 
-  virtual MiLightRadio* create(const MiLightRadioConfig& config);
+  virtual std::shared_ptr<MiLightRadio> create(const MiLightRadioConfig& config);
 
 protected:
 

+ 4 - 5
lib/Settings/AboutHelper.cpp

@@ -4,18 +4,17 @@
 #include <ESP8266WiFi.h>
 
 String AboutHelper::generateAboutString(bool abbreviated) {
-  DynamicJsonBuffer buffer;
-  JsonObject& response = buffer.createObject();
+  DynamicJsonDocument buffer(1024);
 
-  generateAboutObject(response, abbreviated);
+  generateAboutObject(buffer, abbreviated);
 
   String body;
-  response.printTo(body);
+  serializeJson(buffer, body);
 
   return body;
 }
 
-void AboutHelper::generateAboutObject(JsonObject& obj, bool abbreviated) {
+void AboutHelper::generateAboutObject(JsonDocument& obj, bool abbreviated) {
   obj["firmware"] = QUOTE(FIRMWARE_NAME);
   obj["version"] = QUOTE(MILIGHT_HUB_VERSION);
   obj["ip_address"] = WiFi.localIP().toString();

+ 1 - 1
lib/Settings/AboutHelper.h

@@ -7,7 +7,7 @@
 class AboutHelper {
 public:
   static String generateAboutString(bool abbreviated = false);
-  static void generateAboutObject(JsonObject& obj, bool abbreviated = false);
+  static void generateAboutObject(JsonDocument& obj, bool abbreviated = false);
 };
 
 #endif

+ 126 - 151
lib/Settings/Settings.cpp

@@ -7,10 +7,24 @@
 
 #define PORT_POSITION(s) ( s.indexOf(':') )
 
-bool Settings::hasAuthSettings() {
+GatewayConfig::GatewayConfig(uint16_t deviceId, uint16_t port, uint8_t protocolVersion)
+  : deviceId(deviceId)
+  , port(port)
+  , protocolVersion(protocolVersion)
+{ }
+
+bool Settings::isAuthenticationEnabled() const {
   return adminUsername.length() > 0 && adminPassword.length() > 0;
 }
 
+const String& Settings::getUsername() const {
+  return adminUsername;
+}
+
+const String& Settings::getPassword() const {
+  return adminPassword;
+}
+
 bool Settings::isAutoRestartEnabled() {
   return _autoRestartPeriod > 0;
 }
@@ -23,141 +37,110 @@ size_t Settings::getAutoRestartPeriod() {
   return std::max(_autoRestartPeriod, static_cast<size_t>(MINIMUM_RESTART_PERIOD));
 }
 
-void Settings::deserialize(Settings& settings, String json) {
-  DynamicJsonBuffer jsonBuffer;
-  JsonObject& parsedSettings = jsonBuffer.parseObject(json);
-  settings.patch(parsedSettings);
-}
-
-void Settings::updateDeviceIds(JsonArray& arr) {
-  if (arr.success()) {
-    if (this->deviceIds) {
-      delete this->deviceIds;
-    }
+void Settings::updateDeviceIds(JsonArray arr) {
+  this->deviceIds.clear();
 
-    this->deviceIds = new uint16_t[arr.size()];
-    this->numDeviceIds = arr.size();
-    arr.copyTo(this->deviceIds, arr.size());
+  for (size_t i = 0; i < arr.size(); ++i) {
+    this->deviceIds.push_back(arr[i]);
   }
 }
 
-void Settings::updateGatewayConfigs(JsonArray& arr) {
-  if (arr.success()) {
-    if (this->gatewayConfigs) {
-      delete[] this->gatewayConfigs;
-    }
-
-    this->gatewayConfigs = new GatewayConfig*[arr.size()];
-    this->numGatewayConfigs = arr.size();
+void Settings::updateGatewayConfigs(JsonArray arr) {
+  gatewayConfigs.clear();
 
-    for (size_t i = 0; i < arr.size(); i++) {
-      JsonArray& params = arr[i];
+  for (size_t i = 0; i < arr.size(); i++) {
+    JsonArray params = arr[i];
 
-      if (params.success() && params.size() == 3) {
-        this->gatewayConfigs[i] = new GatewayConfig(parseInt<uint16_t>(params[0]), params[1], params[2]);
-      } else {
-        Serial.print(F("Settings - skipped parsing gateway ports settings for element #"));
-        Serial.println(i);
-      }
+    if (params.size() == 3) {
+      std::shared_ptr<GatewayConfig> ptr = std::make_shared<GatewayConfig>(parseInt<uint16_t>(params[0]), params[1], params[2]);
+      gatewayConfigs.push_back(std::move(ptr));
+    } else {
+      Serial.print(F("Settings - skipped parsing gateway ports settings for element #"));
+      Serial.println(i);
     }
   }
 }
 
-void Settings::updateGroupStateFields(JsonArray &arr) {
-  if (arr.success()) {
-    if (this->groupStateFields) {
-      delete this->groupStateFields;
-    }
-
-    this->groupStateFields = new GroupStateField[arr.size()];
-    this->numGroupStateFields = arr.size();
-
-    for (size_t i = 0; i < arr.size(); i++) {
-      String name = arr[i];
-      name.toLowerCase();
-
-      this->groupStateFields[i] = GroupStateFieldHelpers::getFieldByName(name.c_str());
-    }
+void Settings::patch(JsonObject parsedSettings) {
+  if (parsedSettings.isNull()) {
+    Serial.println(F("Skipping patching loaded settings.  Parsed settings was null."));
+    return;
   }
-}
 
-void Settings::patch(JsonObject& parsedSettings) {
-  if (parsedSettings.success()) {
-    this->setIfPresent<String>(parsedSettings, "admin_username", adminUsername);
-    this->setIfPresent(parsedSettings, "admin_password", adminPassword);
-    this->setIfPresent(parsedSettings, "ce_pin", cePin);
-    this->setIfPresent(parsedSettings, "csn_pin", csnPin);
-    this->setIfPresent(parsedSettings, "reset_pin", resetPin);
-    this->setIfPresent(parsedSettings, "led_pin", ledPin);
-    this->setIfPresent(parsedSettings, "packet_repeats", packetRepeats);
-    this->setIfPresent(parsedSettings, "http_repeat_factor", httpRepeatFactor);
-    this->setIfPresent(parsedSettings, "auto_restart_period", _autoRestartPeriod);
-    this->setIfPresent(parsedSettings, "mqtt_server", _mqttServer);
-    this->setIfPresent(parsedSettings, "mqtt_username", mqttUsername);
-    this->setIfPresent(parsedSettings, "mqtt_password", mqttPassword);
-    this->setIfPresent(parsedSettings, "mqtt_topic_pattern", mqttTopicPattern);
-    this->setIfPresent(parsedSettings, "mqtt_update_topic_pattern", mqttUpdateTopicPattern);
-    this->setIfPresent(parsedSettings, "mqtt_state_topic_pattern", mqttStateTopicPattern);
-    this->setIfPresent(parsedSettings, "mqtt_client_status_topic", mqttClientStatusTopic);
-    this->setIfPresent(parsedSettings, "discovery_port", discoveryPort);
-    this->setIfPresent(parsedSettings, "listen_repeats", listenRepeats);
-    this->setIfPresent(parsedSettings, "state_flush_interval", stateFlushInterval);
-    this->setIfPresent(parsedSettings, "mqtt_state_rate_limit", mqttStateRateLimit);
-    this->setIfPresent(parsedSettings, "packet_repeat_throttle_threshold", packetRepeatThrottleThreshold);
-    this->setIfPresent(parsedSettings, "packet_repeat_throttle_sensitivity", packetRepeatThrottleSensitivity);
-    this->setIfPresent(parsedSettings, "packet_repeat_minimum", packetRepeatMinimum);
-    this->setIfPresent(parsedSettings, "enable_automatic_mode_switching", enableAutomaticModeSwitching);
-    this->setIfPresent(parsedSettings, "led_mode_packet_count", ledModePacketCount);
-    this->setIfPresent(parsedSettings, "hostname", hostname);
-    this->setIfPresent(parsedSettings, "wifi_static_ip", wifiStaticIP);
-    this->setIfPresent(parsedSettings, "wifi_static_ip_gateway", wifiStaticIPGateway);
-    this->setIfPresent(parsedSettings, "wifi_static_ip_netmask", wifiStaticIPNetmask);
-
-    if (parsedSettings.containsKey("rf24_channels")) {
-      JsonArray& arr = parsedSettings["rf24_channels"];
-      rf24Channels = JsonHelpers::jsonArrToVector<RF24Channel>(arr, RF24ChannelHelpers::valueFromName);
-    }
+  this->setIfPresent(parsedSettings, "admin_username", adminUsername);
+  this->setIfPresent(parsedSettings, "admin_password", adminPassword);
+  this->setIfPresent(parsedSettings, "ce_pin", cePin);
+  this->setIfPresent(parsedSettings, "csn_pin", csnPin);
+  this->setIfPresent(parsedSettings, "reset_pin", resetPin);
+  this->setIfPresent(parsedSettings, "led_pin", ledPin);
+  this->setIfPresent(parsedSettings, "packet_repeats", packetRepeats);
+  this->setIfPresent(parsedSettings, "http_repeat_factor", httpRepeatFactor);
+  this->setIfPresent(parsedSettings, "auto_restart_period", _autoRestartPeriod);
+  this->setIfPresent(parsedSettings, "mqtt_server", _mqttServer);
+  this->setIfPresent(parsedSettings, "mqtt_username", mqttUsername);
+  this->setIfPresent(parsedSettings, "mqtt_password", mqttPassword);
+  this->setIfPresent(parsedSettings, "mqtt_topic_pattern", mqttTopicPattern);
+  this->setIfPresent(parsedSettings, "mqtt_update_topic_pattern", mqttUpdateTopicPattern);
+  this->setIfPresent(parsedSettings, "mqtt_state_topic_pattern", mqttStateTopicPattern);
+  this->setIfPresent(parsedSettings, "mqtt_client_status_topic", mqttClientStatusTopic);
+  this->setIfPresent(parsedSettings, "discovery_port", discoveryPort);
+  this->setIfPresent(parsedSettings, "listen_repeats", listenRepeats);
+  this->setIfPresent(parsedSettings, "state_flush_interval", stateFlushInterval);
+  this->setIfPresent(parsedSettings, "mqtt_state_rate_limit", mqttStateRateLimit);
+  this->setIfPresent(parsedSettings, "packet_repeat_throttle_threshold", packetRepeatThrottleThreshold);
+  this->setIfPresent(parsedSettings, "packet_repeat_throttle_sensitivity", packetRepeatThrottleSensitivity);
+  this->setIfPresent(parsedSettings, "packet_repeat_minimum", packetRepeatMinimum);
+  this->setIfPresent(parsedSettings, "enable_automatic_mode_switching", enableAutomaticModeSwitching);
+  this->setIfPresent(parsedSettings, "led_mode_packet_count", ledModePacketCount);
+  this->setIfPresent(parsedSettings, "hostname", hostname);
+  this->setIfPresent(parsedSettings, "wifi_static_ip", wifiStaticIP);
+  this->setIfPresent(parsedSettings, "wifi_static_ip_gateway", wifiStaticIPGateway);
+  this->setIfPresent(parsedSettings, "wifi_static_ip_netmask", wifiStaticIPNetmask);
+
+  if (parsedSettings.containsKey("rf24_channels")) {
+    JsonArray arr = parsedSettings["rf24_channels"];
+    rf24Channels = JsonHelpers::jsonArrToVector<RF24Channel, String>(arr, RF24ChannelHelpers::valueFromName);
+  }
 
-    if (parsedSettings.containsKey("rf24_listen_channel")) {
-      this->rf24ListenChannel = RF24ChannelHelpers::valueFromName(parsedSettings["rf24_listen_channel"]);
-    }
+  if (parsedSettings.containsKey("rf24_listen_channel")) {
+    this->rf24ListenChannel = RF24ChannelHelpers::valueFromName(parsedSettings["rf24_listen_channel"]);
+  }
 
-    if (parsedSettings.containsKey("rf24_power_level")) {
-      this->rf24PowerLevel = RF24PowerLevelHelpers::valueFromName(parsedSettings["rf24_power_level"]);
-    }
+  if (parsedSettings.containsKey("rf24_power_level")) {
+    this->rf24PowerLevel = RF24PowerLevelHelpers::valueFromName(parsedSettings["rf24_power_level"]);
+  }
 
-    if (parsedSettings.containsKey("led_mode_wifi_config")) {
-      this->ledModeWifiConfig = LEDStatus::stringToLEDMode(parsedSettings["led_mode_wifi_config"]);
-    }
+  if (parsedSettings.containsKey("led_mode_wifi_config")) {
+    this->ledModeWifiConfig = LEDStatus::stringToLEDMode(parsedSettings["led_mode_wifi_config"]);
+  }
 
-    if (parsedSettings.containsKey("led_mode_wifi_failed")) {
-      this->ledModeWifiFailed = LEDStatus::stringToLEDMode(parsedSettings["led_mode_wifi_failed"]);
-    }
+  if (parsedSettings.containsKey("led_mode_wifi_failed")) {
+    this->ledModeWifiFailed = LEDStatus::stringToLEDMode(parsedSettings["led_mode_wifi_failed"]);
+  }
 
-    if (parsedSettings.containsKey("led_mode_operating")) {
-      this->ledModeOperating = LEDStatus::stringToLEDMode(parsedSettings["led_mode_operating"]);
-    }
+  if (parsedSettings.containsKey("led_mode_operating")) {
+    this->ledModeOperating = LEDStatus::stringToLEDMode(parsedSettings["led_mode_operating"]);
+  }
 
-    if (parsedSettings.containsKey("led_mode_packet")) {
-      this->ledModePacket = LEDStatus::stringToLEDMode(parsedSettings["led_mode_packet"]);
-    }
+  if (parsedSettings.containsKey("led_mode_packet")) {
+    this->ledModePacket = LEDStatus::stringToLEDMode(parsedSettings["led_mode_packet"]);
+  }
 
-    if (parsedSettings.containsKey("radio_interface_type")) {
-      this->radioInterfaceType = Settings::typeFromString(parsedSettings["radio_interface_type"]);
-    }
+  if (parsedSettings.containsKey("radio_interface_type")) {
+    this->radioInterfaceType = Settings::typeFromString(parsedSettings["radio_interface_type"]);
+  }
 
-    if (parsedSettings.containsKey("device_ids")) {
-      JsonArray& arr = parsedSettings["device_ids"];
-      updateDeviceIds(arr);
-    }
-    if (parsedSettings.containsKey("gateway_configs")) {
-      JsonArray& arr = parsedSettings["gateway_configs"];
-      updateGatewayConfigs(arr);
-    }
-    if (parsedSettings.containsKey("group_state_fields")) {
-      JsonArray& arr = parsedSettings["group_state_fields"];
-      updateGroupStateFields(arr);
-    }
+  if (parsedSettings.containsKey("device_ids")) {
+    JsonArray arr = parsedSettings["device_ids"];
+    updateDeviceIds(arr);
+  }
+  if (parsedSettings.containsKey("gateway_configs")) {
+    JsonArray arr = parsedSettings["gateway_configs"];
+    updateGatewayConfigs(arr);
+  }
+  if (parsedSettings.containsKey("group_state_fields")) {
+    JsonArray arr = parsedSettings["group_state_fields"];
+    groupStateFields = JsonHelpers::jsonArrToVector<GroupStateField, const char*>(arr, GroupStateFieldHelpers::getFieldByName);
   }
 }
 
@@ -167,10 +150,18 @@ void Settings::load(Settings& settings) {
     settings = Settings();
 
     File f = SPIFFS.open(SETTINGS_FILE, "r");
-    String settingsContents = f.readStringUntil(SETTINGS_TERMINATOR);
+
+    DynamicJsonDocument json(MILIGHT_HUB_SETTINGS_BUFFER_SIZE);
+    auto error = deserializeJson(json, f);
     f.close();
 
-    deserialize(settings, settingsContents);
+    if (! error) {
+      JsonObject parsedSettings = json.as<JsonObject>();
+      settings.patch(parsedSettings);
+    } else {
+      Serial.print(F("Error parsing saved settings file: "));
+      Serial.println(error.c_str());
+    }
   } else {
     settings.save();
   }
@@ -194,9 +185,8 @@ void Settings::save() {
   }
 }
 
-void Settings::serialize(Stream& stream, const bool prettyPrint) {
-  DynamicJsonBuffer jsonBuffer;
-  JsonObject& root = jsonBuffer.createObject();
+void Settings::serialize(Print& stream, const bool prettyPrint) {
+  DynamicJsonDocument root(MILIGHT_HUB_SETTINGS_BUFFER_SIZE);
 
   root["admin_username"] = this->adminUsername;
   root["admin_password"] = this->adminPassword;
@@ -235,42 +225,27 @@ void Settings::serialize(Stream& stream, const bool prettyPrint) {
   root["wifi_static_ip_gateway"] = this->wifiStaticIPGateway;
   root["wifi_static_ip_netmask"] = this->wifiStaticIPNetmask;
 
-  JsonArray& channelArr = jsonBuffer.createArray();
-  JsonHelpers::vectorToJsonArr<RF24Channel>(channelArr, rf24Channels, RF24ChannelHelpers::nameFromValue);
-  root["rf24_channels"] = channelArr;
+  JsonArray channelArr = root.createNestedArray("rf24_channels");
+  JsonHelpers::vectorToJsonArr<RF24Channel, String>(channelArr, rf24Channels, RF24ChannelHelpers::nameFromValue);
 
-  if (this->deviceIds) {
-    JsonArray& arr = jsonBuffer.createArray();
-    arr.copyFrom(this->deviceIds, this->numDeviceIds);
-    root["device_ids"] = arr;
-  }
+  JsonArray deviceIdsArr = root.createNestedArray("device_ids");
+  JsonHelpers::copyFrom<uint16_t>(deviceIdsArr, this->deviceIds);
 
-  if (this->gatewayConfigs) {
-    JsonArray& arr = jsonBuffer.createArray();
-    for (size_t i = 0; i < this->numGatewayConfigs; i++) {
-      JsonArray& elmt = jsonBuffer.createArray();
-      elmt.add(this->gatewayConfigs[i]->deviceId);
-      elmt.add(this->gatewayConfigs[i]->port);
-      elmt.add(this->gatewayConfigs[i]->protocolVersion);
-      arr.add(elmt);
-    }
-
-    root["gateway_configs"] = arr;
+  JsonArray gatewayConfigsArr = root.createNestedArray("gateway_configs");
+  for (size_t i = 0; i < this->gatewayConfigs.size(); i++) {
+    JsonArray elmt = gatewayConfigsArr.createNestedArray();
+    elmt.add(this->gatewayConfigs[i]->deviceId);
+    elmt.add(this->gatewayConfigs[i]->port);
+    elmt.add(this->gatewayConfigs[i]->protocolVersion);
   }
 
-  if (this->groupStateFields) {
-    JsonArray& arr = jsonBuffer.createArray();
-    for (size_t i = 0; i < this->numGroupStateFields; i++) {
-      arr.add(GroupStateFieldHelpers::getFieldName(this->groupStateFields[i]));
-    }
-
-    root["group_state_fields"] = arr;
-  }
+  JsonArray groupStateFieldArr = root.createNestedArray("group_state_fields");
+  JsonHelpers::vectorToJsonArr<GroupStateField, const char*>(groupStateFieldArr, groupStateFields, GroupStateFieldHelpers::getFieldName);
 
   if (prettyPrint) {
-    root.prettyPrintTo(stream);
+    serializeJsonPretty(root, stream);
   } else {
-    root.printTo(stream);
+    serializeJson(root, stream);
   }
 }
 
@@ -311,4 +286,4 @@ String Settings::typeToString(RadioInterfaceType type) {
     default:
       return "nRF24";
   }
-}
+}

+ 24 - 41
lib/Settings/Settings.h

@@ -6,11 +6,17 @@
 #include <RF24Channel.h>
 #include <Size.h>
 #include <LEDStatus.h>
+#include <AuthProviders.h>
 #include <vector>
+#include <memory>
 
 #ifndef _SETTINGS_H_INCLUDED
 #define _SETTINGS_H_INCLUDED
 
+#ifndef MILIGHT_HUB_SETTINGS_BUFFER_SIZE
+#define MILIGHT_HUB_SETTINGS_BUFFER_SIZE 4096
+#endif
+
 #define XQUOTE(x) #x
 #define QUOTE(x) XQUOTE(x)
 
@@ -61,13 +67,8 @@ static const GroupStateField DEFAULT_GROUP_STATE_FIELDS[] = {
   GroupStateField::BULB_MODE
 };
 
-class GatewayConfig {
-public:
-  GatewayConfig(uint16_t deviceId, uint16_t port, uint8_t protocolVersion)
-    : deviceId(deviceId),
-      port(port),
-      protocolVersion(protocolVersion)
-    { }
+struct GatewayConfig {
+  GatewayConfig(uint16_t deviceId, uint16_t port, uint8_t protocolVersion);
 
   const uint16_t deviceId;
   const uint16_t port;
@@ -85,10 +86,6 @@ public:
     resetPin(0),
     ledPin(-2),
     radioInterfaceType(nRF24),
-    deviceIds(NULL),
-    gatewayConfigs(NULL),
-    numDeviceIds(0),
-    numGatewayConfigs(0),
     packetRepeats(50),
     httpRepeatFactor(1),
     listenRepeats(3),
@@ -98,8 +95,6 @@ public:
     packetRepeatThrottleThreshold(200),
     packetRepeatThrottleSensitivity(0),
     packetRepeatMinimum(3),
-    groupStateFields(NULL),
-    numGroupStateFields(0),
     enableAutomaticModeSwitching(false),
     ledModeWifiConfig(LEDStatus::LEDMode::FastToggle),
     ledModeWifiFailed(LEDStatus::LEDMode::On),
@@ -111,25 +106,17 @@ public:
     rf24Channels(RF24ChannelHelpers::allValues()),
     rf24ListenChannel(RF24Channel::RF24_LOW),
     _autoRestartPeriod(0)
-  {
-    if (groupStateFields == NULL) {
-      numGroupStateFields = size(DEFAULT_GROUP_STATE_FIELDS);
-      groupStateFields = new GroupStateField[numGroupStateFields];
-      memcpy(groupStateFields, DEFAULT_GROUP_STATE_FIELDS, numGroupStateFields * sizeof(GroupStateField));
-    }
-  }
+  { }
 
-  ~Settings() {
-    if (deviceIds) {
-      delete deviceIds;
-    }
-  }
+  ~Settings() { }
+
+  bool isAuthenticationEnabled() const;
+  const String& getUsername() const;
+  const String& getPassword() const;
 
-  bool hasAuthSettings();
   bool isAutoRestartEnabled();
   size_t getAutoRestartPeriod();
 
-  static void deserialize(Settings& settings, String json);
   static void load(Settings& settings);
 
   static RadioInterfaceType typeFromString(const String& s);
@@ -138,11 +125,10 @@ public:
 
   void save();
   String toJson(const bool prettyPrint = true);
-  void serialize(Stream& stream, const bool prettyPrint = false);
-  void updateDeviceIds(JsonArray& arr);
-  void updateGatewayConfigs(JsonArray& arr);
-  void updateGroupStateFields(JsonArray& arr);
-  void patch(JsonObject& obj);
+  void serialize(Print& stream, const bool prettyPrint = false);
+  void updateDeviceIds(JsonArray arr);
+  void updateGatewayConfigs(JsonArray arr);
+  void patch(JsonObject obj);
   String mqttServer();
   uint16_t mqttPort();
 
@@ -153,10 +139,6 @@ public:
   uint8_t resetPin;
   int8_t ledPin;
   RadioInterfaceType radioInterfaceType;
-  uint16_t *deviceIds;
-  GatewayConfig **gatewayConfigs;
-  size_t numDeviceIds;
-  size_t numGatewayConfigs;
   size_t packetRepeats;
   size_t httpRepeatFactor;
   uint8_t listenRepeats;
@@ -173,8 +155,6 @@ public:
   size_t packetRepeatThrottleThreshold;
   size_t packetRepeatThrottleSensitivity;
   size_t packetRepeatMinimum;
-  GroupStateField *groupStateFields;
-  size_t numGroupStateFields;
   bool enableAutomaticModeSwitching;
   LEDStatus::LEDMode ledModeWifiConfig;
   LEDStatus::LEDMode ledModeWifiFailed;
@@ -183,20 +163,23 @@ public:
   size_t ledModePacketCount;
   String hostname;
   RF24PowerLevel rf24PowerLevel;
+  std::vector<uint16_t> deviceIds;
   std::vector<RF24Channel> rf24Channels;
+  std::vector<GroupStateField> groupStateFields;
+  std::vector<std::shared_ptr<GatewayConfig>> gatewayConfigs;
   RF24Channel rf24ListenChannel;
   String wifiStaticIP;
   String wifiStaticIPNetmask;
   String wifiStaticIPGateway;
 
-
 protected:
   size_t _autoRestartPeriod;
 
   template <typename T>
-  void setIfPresent(JsonObject& obj, const char* key, T& var) {
+  void setIfPresent(JsonObject obj, const char* key, T& var) {
     if (obj.containsKey(key)) {
-      var = obj.get<T>(key);
+      JsonVariant val = obj[key];
+      var = val.as<T>();
     }
   }
 };

+ 6 - 6
lib/Udp/MiLightDiscoveryServer.cpp

@@ -49,15 +49,15 @@ void MiLightDiscoveryServer::handleClient() {
 
 void MiLightDiscoveryServer::handleDiscovery(uint8_t version) {
 #ifdef MILIGHT_UDP_DEBUG
-  printf_P(PSTR("Handling discovery for version: %u, %d configs to consider\n"), version, settings.numGatewayConfigs);
+  printf_P(PSTR("Handling discovery for version: %u, %d configs to consider\n"), version, settings.gatewayConfigs.size());
 #endif
 
   char buffer[40];
 
-  for (size_t i = 0; i < settings.numGatewayConfigs; i++) {
-    GatewayConfig* config = settings.gatewayConfigs[i];
+  for (size_t i = 0; i < settings.gatewayConfigs.size(); i++) {
+    const GatewayConfig& config = *settings.gatewayConfigs[i];
 
-    if (config->protocolVersion != version) {
+    if (config.protocolVersion != version) {
       continue;
     }
 
@@ -67,10 +67,10 @@ void MiLightDiscoveryServer::handleDiscovery(uint8_t version) {
       buffer,
       PSTR("%d.%d.%d.%d,00000000%02X%02X"),
       addr[0], addr[1], addr[2], addr[3],
-      (config->deviceId >> 8), (config->deviceId & 0xFF)
+      (config.deviceId >> 8), (config.deviceId & 0xFF)
     );
 
-    if (config->protocolVersion == 5) {
+    if (config.protocolVersion == 5) {
       sendResponse(buffer);
     } else {
       sprintf_P(ptr, PSTR(",HF-LPB100"));

+ 3 - 3
lib/Udp/MiLightUdpServer.cpp

@@ -40,11 +40,11 @@ void MiLightUdpServer::handleClient() {
   }
 }
 
-MiLightUdpServer* MiLightUdpServer::fromVersion(uint8_t version, MiLightClient*& client, uint16_t port, uint16_t deviceId) {
+std::shared_ptr<MiLightUdpServer> MiLightUdpServer::fromVersion(uint8_t version, MiLightClient*& client, uint16_t port, uint16_t deviceId) {
   if (version == 0 || version == 5) {
-    return new V5MiLightUdpServer(client, port, deviceId);
+    return std::make_shared<V5MiLightUdpServer>(client, port, deviceId);
   } else if (version == 6) {
-    return new V6MiLightUdpServer(client, port, deviceId);
+    return std::make_shared<V6MiLightUdpServer>(client, port, deviceId);
   }
 
   return NULL;

+ 3 - 1
lib/Udp/MiLightUdpServer.h

@@ -2,6 +2,8 @@
 #include <MiLightClient.h>
 #include <WiFiUdp.h>
 
+#include <memory>
+
 // This protocol is documented here:
 // http://www.limitlessled.com/dev/
 
@@ -22,7 +24,7 @@ public:
   void begin();
   void handleClient();
 
-  static MiLightUdpServer* fromVersion(uint8_t version, MiLightClient*&, uint16_t port, uint16_t deviceId);
+  static std::shared_ptr<MiLightUdpServer> fromVersion(uint8_t version, MiLightClient*&, uint16_t port, uint16_t deviceId);
 
 protected:
   WiFiUDP socket;

+ 150 - 164
lib/WebServer/MiLightHttpServer.cpp

@@ -9,33 +9,60 @@
 #include <AboutHelper.h>
 #include <index.html.gz.h>
 
-void MiLightHttpServer::begin() {
-  applySettings(settings);
+using namespace std::placeholders;
 
+void MiLightHttpServer::begin() {
   // set up HTTP end points to serve
 
-  _handleRootPage = handleServe_P(index_html_gz, index_html_gz_len);
-  server.onAuthenticated("/", HTTP_GET, [this]() { _handleRootPage(); });
-  server.onAuthenticated("/settings", HTTP_GET, [this]() { serveSettings(); });
-  server.onAuthenticated("/settings", HTTP_PUT, [this]() { handleUpdateSettings(); });
-  server.onAuthenticated("/settings", HTTP_POST, [this]() { handleUpdateSettingsPost(); }, handleUpdateFile(SETTINGS_FILE));
-  server.onAuthenticated("/remote_configs", HTTP_GET, [this]() { handleGetRadioConfigs(); });
+  server
+    .buildHandler("/")
+    .onSimple(HTTP_GET, std::bind(&MiLightHttpServer::handleServe_P, this, index_html_gz, index_html_gz_len));
+
+  server
+    .buildHandler("/settings")
+    .on(HTTP_GET, std::bind(&MiLightHttpServer::serveSettings, this))
+    .on(HTTP_PUT, std::bind(&MiLightHttpServer::handleUpdateSettings, this, _1))
+    .on(
+      HTTP_POST,
+      std::bind(&MiLightHttpServer::handleUpdateSettingsPost, this, _1),
+      std::bind(&MiLightHttpServer::handleUpdateFile, this, SETTINGS_FILE)
+    );
+
+  server
+    .buildHandler("/remote_configs")
+    .on(HTTP_GET, std::bind(&MiLightHttpServer::handleGetRadioConfigs, this, _1));
+
+  server
+    .buildHandler("/gateway_traffic")
+    .on(HTTP_GET, std::bind(&MiLightHttpServer::handleListenGateway, this, _1));
+  server
+    .buildHandler("/gateway_traffic/:type")
+    .on(HTTP_GET, std::bind(&MiLightHttpServer::handleListenGateway, this, _1));
+
+  server
+    .buildHandler("/gateways/:device_id/:type/:group_id")
+    .on(HTTP_PUT, std::bind(&MiLightHttpServer::handleUpdateGroup, this, _1))
+    .on(HTTP_POST, std::bind(&MiLightHttpServer::handleUpdateGroup, this, _1))
+    .on(HTTP_DELETE, std::bind(&MiLightHttpServer::handleDeleteGroup, this, _1))
+    .on(HTTP_GET, std::bind(&MiLightHttpServer::handleGetGroup, this, _1));
 
-  server.onAuthenticated("/gateway_traffic", HTTP_GET, [this]() { handleListenGateway(NULL); });
-  server.onPatternAuthenticated("/gateway_traffic/:type", HTTP_GET, [this](const UrlTokenBindings* b) { handleListenGateway(b); });
+  server
+    .buildHandler("/raw_commands/:type")
+    .on(HTTP_ANY, std::bind(&MiLightHttpServer::handleSendRaw, this, _1));
 
-  const char groupPattern[] = "/gateways/:device_id/:type/:group_id";
-  server.onPatternAuthenticated(groupPattern, HTTP_PUT, [this](const UrlTokenBindings* b) { handleUpdateGroup(b); });
-  server.onPatternAuthenticated(groupPattern, HTTP_POST, [this](const UrlTokenBindings* b) { handleUpdateGroup(b); });
-  server.onPatternAuthenticated(groupPattern, HTTP_DELETE, [this](const UrlTokenBindings* b) { handleDeleteGroup(b); });
-  server.onPatternAuthenticated(groupPattern, HTTP_GET, [this](const UrlTokenBindings* b) { handleGetGroup(b); });
+  server
+    .buildHandler("/about")
+    .on(HTTP_GET, std::bind(&MiLightHttpServer::handleAbout, this, _1));
 
-  server.onPatternAuthenticated("/raw_commands/:type", HTTP_ANY, [this](const UrlTokenBindings* b) { handleSendRaw(b); });
-  server.onAuthenticated("/web", HTTP_POST, [this]() { server.send_P(200, TEXT_PLAIN, PSTR("success")); }, handleUpdateFile(WEB_INDEX_FILENAME));
-  server.onAuthenticated("/about", HTTP_GET, [this]() { handleAbout(); });
-  server.onAuthenticated("/system", HTTP_POST, [this]() { handleSystemPost(); });
-  server.onAuthenticated("/firmware", HTTP_POST, [this]() { handleFirmwarePost(); }, [this]() { handleFirmwareUpload(); });
+  server
+    .buildHandler("/system")
+    .on(HTTP_POST, std::bind(&MiLightHttpServer::handleSystemPost, this, _1));
 
+  server
+    .buildHandler("/firmware")
+    .handleOTA();
+
+  server.clearBuilders();
 
   // set up web socket server
   wsServer.onEvent(
@@ -53,22 +80,21 @@ void MiLightHttpServer::handleClient() {
   wsServer.loop();
 }
 
-void MiLightHttpServer::on(const char* path, HTTPMethod method, ESP8266WebServer::THandlerFunction handler) {
-  server.on(path, method, handler);
-}
-
 WiFiClient MiLightHttpServer::client() {
   return server.client();
 }
 
-void MiLightHttpServer::handleSystemPost() {
-  DynamicJsonBuffer buffer;
-  JsonObject& request = buffer.parse(server.arg("plain"));
+void MiLightHttpServer::on(const char* path, HTTPMethod method, ESP8266WebServer::THandlerFunction handler) {
+  server.on(path, method, handler);
+}
+
+void MiLightHttpServer::handleSystemPost(RequestContext& request) {
+  JsonObject requestBody = request.getJsonBody().as<JsonObject>();
 
   bool handled = false;
 
-  if (request.containsKey("command")) {
-    if (request["command"] == "restart") {
+  if (requestBody.containsKey("command")) {
+    if (requestBody["command"] == "restart") {
       Serial.println(F("Restarting..."));
       server.send_P(200, TEXT_PLAIN, PSTR("true"));
 
@@ -77,7 +103,7 @@ void MiLightHttpServer::handleSystemPost() {
       ESP.restart();
 
       handled = true;
-    } else if (request["command"] == "clear_wifi_config") {
+    } else if (requestBody["command"] == "clear_wifi_config") {
         Serial.println(F("Resetting Wifi and then Restarting..."));
         server.send_P(200, TEXT_PLAIN, PSTR("true"));
 
@@ -91,9 +117,11 @@ void MiLightHttpServer::handleSystemPost() {
   }
 
   if (handled) {
-    server.send_P(200, TEXT_PLAIN, PSTR("true"));
+    request.response.json["success"] = true;
   } else {
-    server.send_P(400, TEXT_PLAIN, PSTR("{\"error\":\"Unhandled command\"}"));
+    request.response.json["success"] = false;
+    request.response.json["error"] = "Unhandled command";
+    request.response.setCode(400);
   }
 }
 
@@ -103,14 +131,6 @@ void MiLightHttpServer::serveSettings() {
   serveFile(SETTINGS_FILE, APPLICATION_JSON);
 }
 
-void MiLightHttpServer::applySettings(Settings& settings) {
-  if (settings.hasAuthSettings()) {
-    server.requireAuthentication(settings.adminUsername, settings.adminPassword);
-  } else {
-    server.disableAuthentication();
-  }
-}
-
 void MiLightHttpServer::onSettingsSaved(SettingsSavedHandler handler) {
   this->settingsSavedHandler = handler;
 }
@@ -119,39 +139,17 @@ void MiLightHttpServer::onGroupDeleted(GroupDeletedHandler handler) {
   this->groupDeletedHandler = handler;
 }
 
-void MiLightHttpServer::handleAbout() {
-  server.send(200, APPLICATION_JSON, AboutHelper::generateAboutString());
+void MiLightHttpServer::handleAbout(RequestContext& request) {
+  AboutHelper::generateAboutObject(request.response.json);
 }
 
-void MiLightHttpServer::handleGetRadioConfigs() {
-  DynamicJsonBuffer buffer;
-  JsonArray& arr = buffer.createArray();
+void MiLightHttpServer::handleGetRadioConfigs(RequestContext& request) {
+  JsonArray arr = request.response.json.to<JsonArray>();
 
   for (size_t i = 0; i < MiLightRemoteConfig::NUM_REMOTES; i++) {
     const MiLightRemoteConfig* config = MiLightRemoteConfig::ALL_REMOTES[i];
     arr.add(config->name);
   }
-
-  String body;
-  arr.printTo(body);
-
-  server.send(200, APPLICATION_JSON, body);
-}
-
-ESP8266WebServer::THandlerFunction MiLightHttpServer::handleServeFile(
-  const char* filename,
-  const char* contentType,
-  const char* defaultText) {
-
-  return [this, filename, contentType, defaultText]() {
-    if (!serveFile(filename)) {
-      if (defaultText) {
-        server.send(200, contentType, defaultText);
-      } else {
-        server.send(404);
-      }
-    }
-  };
 }
 
 bool MiLightHttpServer::serveFile(const char* file, const char* contentType) {
@@ -165,54 +163,44 @@ bool MiLightHttpServer::serveFile(const char* file, const char* contentType) {
   return false;
 }
 
-ESP8266WebServer::THandlerFunction MiLightHttpServer::handleUpdateFile(const char* filename) {
-  return [this, filename]() {
-    HTTPUpload& upload = server.upload();
+void MiLightHttpServer::handleUpdateFile(const char* filename) {
+  HTTPUpload& upload = server.upload();
 
-    if (upload.status == UPLOAD_FILE_START) {
-      updateFile = SPIFFS.open(filename, "w");
-    } else if(upload.status == UPLOAD_FILE_WRITE){
-      if (updateFile.write(upload.buf, upload.currentSize) != upload.currentSize) {
-        Serial.println(F("Error updating web file"));
-      }
-    } else if (upload.status == UPLOAD_FILE_END) {
-      updateFile.close();
+  if (upload.status == UPLOAD_FILE_START) {
+    updateFile = SPIFFS.open(filename, "w");
+  } else if(upload.status == UPLOAD_FILE_WRITE){
+    if (updateFile.write(upload.buf, upload.currentSize) != upload.currentSize) {
+      Serial.println(F("Error updating web file"));
     }
-  };
+  } else if (upload.status == UPLOAD_FILE_END) {
+    updateFile.close();
+  }
 }
 
-void MiLightHttpServer::handleUpdateSettings() {
-  DynamicJsonBuffer buffer;
-  const String& rawSettings = server.arg("plain");
-  JsonObject& parsedSettings = buffer.parse(rawSettings);
+void MiLightHttpServer::handleUpdateSettings(RequestContext& request) {
+  JsonObject parsedSettings = request.getJsonBody().as<JsonObject>();
 
-  if (parsedSettings.success()) {
+  if (! parsedSettings.isNull()) {
     settings.patch(parsedSettings);
     settings.save();
 
-    this->applySettings(settings);
-
     if (this->settingsSavedHandler) {
       this->settingsSavedHandler();
     }
 
-    server.send(200, APPLICATION_JSON, "true");
+    request.response.json["success"] = true;
     Serial.println(F("Settings successfully updated"));
-  } else {
-    server.send(400, APPLICATION_JSON, "\"Invalid JSON\"");
-    Serial.println(F("Settings failed to update; invalid JSON"));
   }
 }
 
-void MiLightHttpServer::handleUpdateSettingsPost() {
+void MiLightHttpServer::handleUpdateSettingsPost(RequestContext& request) {
   Settings::load(settings);
 
-  this->applySettings(settings);
   if (this->settingsSavedHandler) {
     this->settingsSavedHandler();
   }
 
-  server.send_P(200, TEXT_PLAIN, PSTR("success."));
+  request.response.json["success"] = true;
 }
 
 void MiLightHttpServer::handleFirmwarePost() {
@@ -260,38 +248,46 @@ void MiLightHttpServer::handleFirmwareUpload() {
 }
 
 
-void MiLightHttpServer::handleListenGateway(const UrlTokenBindings* bindings) {
-  bool listenAll = bindings == NULL;
+void MiLightHttpServer::handleListenGateway(RequestContext& request) {
+  bool listenAll = !request.pathVariables.hasBinding("type");
   size_t configIx = 0;
-  const MiLightRadioConfig* radioConfig = NULL;
+  std::shared_ptr<MiLightRadio> radio = NULL;
   const MiLightRemoteConfig* remoteConfig = NULL;
+  const MiLightRemoteConfig* tmpRemoteConfig = NULL;
+
   uint8_t packet[MILIGHT_MAX_PACKET_LENGTH];
 
-  if (bindings != NULL) {
-    String strType(bindings->get("type"));
-    const MiLightRemoteConfig* remoteConfig = MiLightRemoteConfig::fromType(strType);
-    milightClient->prepare(remoteConfig, 0, 0);
-    radioConfig = &remoteConfig->radioConfig;
+  if (!listenAll) {
+    String strType(request.pathVariables.get("type"));
+    tmpRemoteConfig = MiLightRemoteConfig::fromType(strType);
+    milightClient->prepare(tmpRemoteConfig, 0, 0);
   }
 
-  if (radioConfig == NULL && !listenAll) {
-    server.send_P(400, TEXT_PLAIN, PSTR("Unknown device type supplied."));
+  if (tmpRemoteConfig == NULL && !listenAll) {
+    request.response.setCode(400);
+    request.response.json["error"] = "Unknown device type supplied";
     return;
   }
 
+  if (tmpRemoteConfig != NULL) {
+    radio = milightClient->switchRadio(tmpRemoteConfig);
+  }
+
   while (remoteConfig == NULL) {
-    if (!server.clientConnected()) {
+    if (!server.client().connected()) {
       return;
     }
 
     if (listenAll) {
-      radioConfig = &milightClient->switchRadio(configIx++ % milightClient->getNumRadios())->config();
+      radio = milightClient->switchRadio(configIx++ % milightClient->getNumRadios());
+    } else {
+      radio->configure();
     }
 
     if (milightClient->available()) {
       size_t packetLen = milightClient->read(packet);
       remoteConfig = MiLightRemoteConfig::fromReceivedPacket(
-        *radioConfig,
+        radio->config(),
         packet,
         packetLen
       );
@@ -300,8 +296,8 @@ void MiLightHttpServer::handleListenGateway(const UrlTokenBindings* bindings) {
     yield();
   }
 
-  char response[200];
-  char* responseBuffer = response;
+  char responseBody[200];
+  char* responseBuffer = responseBody;
 
   responseBuffer += sprintf_P(
     responseBuffer,
@@ -311,77 +307,68 @@ void MiLightHttpServer::handleListenGateway(const UrlTokenBindings* bindings) {
   );
   remoteConfig->packetFormatter->format(packet, responseBuffer);
 
-  server.send(200, "text/plain", response);
+  request.response.json["packet_info"] = responseBody;
 }
 
-void MiLightHttpServer::sendGroupState(BulbId& bulbId, GroupState* state) {
-  String body;
-  StaticJsonBuffer<200> jsonBuffer;
-  JsonObject& obj = jsonBuffer.createObject();
+void MiLightHttpServer::sendGroupState(BulbId& bulbId, GroupState* state, RichHttp::Response& response) {
+  JsonObject obj = response.json.to<JsonObject>();
 
   if (state != NULL) {
-    state->applyState(obj, bulbId, settings.groupStateFields, settings.numGroupStateFields);
+    state->applyState(obj, bulbId, settings.groupStateFields);
+    state->debugState("test");
   }
-
-  obj.printTo(body);
-
-  server.send(200, APPLICATION_JSON, body);
 }
 
-void MiLightHttpServer::handleGetGroup(const UrlTokenBindings* urlBindings) {
-  const String _deviceId = urlBindings->get("device_id");
-  uint8_t _groupId = atoi(urlBindings->get("group_id"));
-  const MiLightRemoteConfig* _remoteType = MiLightRemoteConfig::fromType(urlBindings->get("type"));
+void MiLightHttpServer::handleGetGroup(RequestContext& request) {
+  const String _deviceId = request.pathVariables.get("device_id");
+  uint8_t _groupId = atoi(request.pathVariables.get("group_id"));
+  const MiLightRemoteConfig* _remoteType = MiLightRemoteConfig::fromType(request.pathVariables.get("type"));
 
   if (_remoteType == NULL) {
     char buffer[40];
     sprintf_P(buffer, PSTR("Unknown device type\n"));
-    server.send(400, TEXT_PLAIN, buffer);
+    request.response.setCode(400);
+    request.response.json["error"] = buffer;
     return;
   }
 
   BulbId bulbId(parseInt<uint16_t>(_deviceId), _groupId, _remoteType->type);
-  sendGroupState(bulbId, stateStore->get(bulbId));
+  sendGroupState(bulbId, stateStore->get(bulbId), request.response);
 }
 
-void MiLightHttpServer::handleDeleteGroup(const UrlTokenBindings* urlBindings) {
-  const String _deviceId = urlBindings->get("device_id");
-  uint8_t _groupId = atoi(urlBindings->get("group_id"));
-  const MiLightRemoteConfig* _remoteType = MiLightRemoteConfig::fromType(urlBindings->get("type"));
+void MiLightHttpServer::handleDeleteGroup(RequestContext& request) {
+  const String _deviceId = request.pathVariables.get("device_id");
+  uint8_t _groupId = atoi(request.pathVariables.get("group_id"));
+  const MiLightRemoteConfig* _remoteType = MiLightRemoteConfig::fromType(request.pathVariables.get("type"));
 
   if (_remoteType == NULL) {
     char buffer[40];
     sprintf_P(buffer, PSTR("Unknown device type\n"));
-    server.send(400, TEXT_PLAIN, buffer);
+    request.response.setCode(400);
+    request.response.json["error"] = buffer;
     return;
   }
 
   BulbId bulbId(parseInt<uint16_t>(_deviceId), _groupId, _remoteType->type);
   stateStore->clear(bulbId);
 
-  server.send_P(200, APPLICATION_JSON, PSTR("true"));
-
   if (groupDeletedHandler != NULL) {
     this->groupDeletedHandler(bulbId);
   }
-}
 
-void MiLightHttpServer::handleUpdateGroup(const UrlTokenBindings* urlBindings) {
-  DynamicJsonBuffer buffer;
-  JsonObject& request = buffer.parse(server.arg("plain"));
+  request.response.json["success"] = true;
+}
 
-  if (!request.success()) {
-    server.send_P(400, TEXT_PLAIN, PSTR("Invalid JSON"));
-    return;
-  }
+void MiLightHttpServer::handleUpdateGroup(RequestContext& request) {
+  JsonObject reqObj = request.getJsonBody().as<JsonObject>();
 
   milightClient->setResendCount(
     settings.httpRepeatFactor * settings.packetRepeats
   );
 
-  String _deviceIds = urlBindings->get("device_id");
-  String _groupIds = urlBindings->get("group_id");
-  String _remoteTypes = urlBindings->get("type");
+  String _deviceIds = request.pathVariables.get("device_id");
+  String _groupIds = request.pathVariables.get("group_id");
+  String _remoteTypes = request.pathVariables.get("type");
   char deviceIds[_deviceIds.length()];
   char groupIds[_groupIds.length()];
   char remoteTypes[_remoteTypes.length()];
@@ -403,7 +390,8 @@ void MiLightHttpServer::handleUpdateGroup(const UrlTokenBindings* urlBindings) {
     if (config == NULL) {
       char buffer[40];
       sprintf_P(buffer, PSTR("Unknown device type: %s"), _remoteType);
-      server.send(400, "text/plain", buffer);
+      request.response.setCode(400);
+      request.response.json["error"] = buffer;
       return;
     }
 
@@ -416,7 +404,7 @@ void MiLightHttpServer::handleUpdateGroup(const UrlTokenBindings* urlBindings) {
         const uint8_t groupId = atoi(groupIdItr.nextToken());
 
         milightClient->prepare(config, deviceId, groupId);
-        handleRequest(request);
+        handleRequest(reqObj);
         foundBulbId = BulbId(deviceId, groupId, config->type);
         groupCount++;
       }
@@ -424,9 +412,9 @@ void MiLightHttpServer::handleUpdateGroup(const UrlTokenBindings* urlBindings) {
   }
 
   if (groupCount == 1) {
-    sendGroupState(foundBulbId, stateStore->get(foundBulbId));
+    sendGroupState(foundBulbId, stateStore->get(foundBulbId), request.response);
   } else {
-    server.send(200, APPLICATION_JSON, "true");
+    request.response.json["success"] = true;
   }
 }
 
@@ -434,25 +422,25 @@ void MiLightHttpServer::handleRequest(const JsonObject& request) {
   milightClient->update(request);
 }
 
-void MiLightHttpServer::handleSendRaw(const UrlTokenBindings* bindings) {
-  DynamicJsonBuffer buffer;
-  JsonObject& request = buffer.parse(server.arg("plain"));
-  const MiLightRemoteConfig* config = MiLightRemoteConfig::fromType(bindings->get("type"));
+void MiLightHttpServer::handleSendRaw(RequestContext& request) {
+  JsonObject requestBody = request.getJsonBody().as<JsonObject>();
+  const MiLightRemoteConfig* config = MiLightRemoteConfig::fromType(request.pathVariables.get("type"));
 
   if (config == NULL) {
     char buffer[50];
-    sprintf_P(buffer, PSTR("Unknown device type: %s"), bindings->get("type"));
-    server.send(400, "text/plain", buffer);
+    sprintf_P(buffer, PSTR("Unknown device type: %s"), request.pathVariables.get("type"));
+    request.response.setCode(400);
+    request.response.json["error"] = buffer;
     return;
   }
 
   uint8_t packet[MILIGHT_MAX_PACKET_LENGTH];
-  const String& hexPacket = request["packet"];
+  const String& hexPacket = requestBody["packet"];
   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"];
+  if (requestBody.containsKey("num_repeats")) {
+    numRepeats = requestBody["num_repeats"];
   }
 
   milightClient->prepare(config, 0, 0);
@@ -461,7 +449,7 @@ void MiLightHttpServer::handleSendRaw(const UrlTokenBindings* bindings) {
     milightClient->write(packet);
   }
 
-  server.send_P(200, TEXT_PLAIN, PSTR("true"));
+  request.response.json["success"] = true;
 }
 
 void MiLightHttpServer::handleWsEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) {
@@ -503,14 +491,12 @@ void MiLightHttpServer::handlePacketSent(uint8_t *packet, const MiLightRemoteCon
   }
 }
 
-ESP8266WebServer::THandlerFunction MiLightHttpServer::handleServe_P(const char* data, size_t length) {
-  return [this, data, length]() {
-    server.sendHeader("Content-Encoding", "gzip");
-    server.setContentLength(CONTENT_LENGTH_UNKNOWN);
-    server.send(200, "text/html", "");
-    server.sendContent_P(data, length);
-    server.sendContent("");
-    server.client().stop();
-  };
+void MiLightHttpServer::handleServe_P(const char* data, size_t length) {
+  server.sendHeader("Content-Encoding", "gzip");
+  server.setContentLength(CONTENT_LENGTH_UNKNOWN);
+  server.send(200, "text/html", "");
+  server.sendContent_P(data, length);
+  server.sendContent("");
+  server.client().stop();
 }
 

+ 31 - 31
lib/WebServer/MiLightHttpServer.h

@@ -1,4 +1,4 @@
-#include <WebServer.h>
+#include <RichHttpServer.h>
 #include <MiLightClient.h>
 #include <Settings.h>
 #include <WebSocketsServer.h>
@@ -12,21 +12,23 @@
 typedef std::function<void(void)> SettingsSavedHandler;
 typedef std::function<void(const BulbId& id)> GroupDeletedHandler;
 
+using RichHttpConfig = RichHttp::Generics::Configs::EspressifBuiltin;
+using RequestContext = RichHttpConfig::RequestContextType;
+
 const char TEXT_PLAIN[] PROGMEM = "text/plain";
 const char APPLICATION_JSON[] = "application/json";
 
 class MiLightHttpServer {
 public:
   MiLightHttpServer(Settings& settings, MiLightClient*& milightClient, GroupStateStore*& stateStore)
-    : server(80),
-      wsServer(WebSocketsServer(81)),
-      numWsClients(0),
-      milightClient(milightClient),
-      settings(settings),
-      stateStore(stateStore)
-  {
-    this->applySettings(settings);
-  }
+    : authProvider(settings)
+    , server(80, authProvider)
+    , wsServer(WebSocketsServer(81))
+    , numWsClients(0)
+    , milightClient(milightClient)
+    , settings(settings)
+    , stateStore(stateStore)
+  { }
 
   void begin();
   void handleClient();
@@ -37,37 +39,35 @@ public:
   WiFiClient client();
 
 protected:
-  ESP8266WebServer::THandlerFunction handleServeFile(
-    const char* filename,
-    const char* contentType,
-    const char* defaultText = NULL);
 
-  void serveSettings();
   bool serveFile(const char* file, const char* contentType = "text/html");
-  ESP8266WebServer::THandlerFunction handleUpdateFile(const char* filename);
-  ESP8266WebServer::THandlerFunction handleServe_P(const char* data, size_t length);
-  void applySettings(Settings& settings);
-  void sendGroupState(BulbId& bulbId, GroupState* state);
-
-  void handleUpdateSettings();
-  void handleUpdateSettingsPost();
-  void handleGetRadioConfigs();
-  void handleAbout();
-  void handleSystemPost();
+  void handleServe_P(const char* data, size_t length);
+  void sendGroupState(BulbId& bulbId, GroupState* state, RichHttp::Response& response);
+
+  void serveSettings();
+  void handleUpdateSettings(RequestContext& request);
+  void handleUpdateSettingsPost(RequestContext& request);
+  void handleUpdateFile(const char* filename);
+
+  void handleGetRadioConfigs(RequestContext& request);
+
+  void handleAbout(RequestContext& request);
+  void handleSystemPost(RequestContext& request);
   void handleFirmwareUpload();
   void handleFirmwarePost();
-  void handleListenGateway(const UrlTokenBindings* urlBindings);
-  void handleSendRaw(const UrlTokenBindings* urlBindings);
-  void handleUpdateGroup(const UrlTokenBindings* urlBindings);
-  void handleDeleteGroup(const UrlTokenBindings* urlBindings);
-  void handleGetGroup(const UrlTokenBindings* urlBindings);
+  void handleListenGateway(RequestContext& request);
+  void handleSendRaw(RequestContext& request);
+  void handleUpdateGroup(RequestContext& request);
+  void handleDeleteGroup(RequestContext& request);
+  void handleGetGroup(RequestContext& request);
 
   void handleRequest(const JsonObject& request);
   void handleWsEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length);
 
   File updateFile;
 
-  WebServer server;
+  PassthroughAuthProvider<Settings> authProvider;
+  RichHttpServer<RichHttp::Generics::Configs::EspressifBuiltin> server;
   WebSocketsServer wsServer;
   size_t numWsClients;
   MiLightClient*& milightClient;

+ 0 - 68
lib/WebServer/WebServer.cpp

@@ -1,68 +0,0 @@
-#include <WebServer.h>
-#include <PathVariableHandler.h>
-
-void WebServer::onAuthenticated(const String &uri, THandlerFunction handler) {
-  THandlerFunction authHandler = [this, handler]() {
-    if (this->validateAuthentiation()) {
-      handler();
-    }
-  };
-
-  ESP8266WebServer::on(uri, authHandler);
-}
-
-void WebServer::onAuthenticated(const String &uri, HTTPMethod method, THandlerFunction handler) {
-  THandlerFunction authHandler = [this, handler]() {
-    if (this->validateAuthentiation()) {
-      handler();
-    }
-  };
-
-  ESP8266WebServer::on(uri, method, authHandler);
-}
-
-void WebServer::onAuthenticated(const String &uri, HTTPMethod method, THandlerFunction handler, THandlerFunction ufn) {
-  THandlerFunction authHandler = [this, handler]() {
-    if (this->validateAuthentiation()) {
-      handler();
-    }
-  };
-
-  ESP8266WebServer::on(uri, method, authHandler, ufn);
-}
-
-void WebServer::onPattern(const String& pattern, const HTTPMethod method, PathVariableHandler::TPathVariableHandlerFn handler) {
-  addHandler(new PathVariableHandler(pattern.c_str(), method, handler));
-}
-
-void WebServer::onPatternAuthenticated(const String& pattern, const HTTPMethod method, PathVariableHandler::TPathVariableHandlerFn fn) {
-  PathVariableHandler::TPathVariableHandlerFn authHandler = [this, fn](UrlTokenBindings* bindings) {
-    if (this->validateAuthentiation()) {
-      fn(bindings);
-    }
-  };
-
-  addHandler(new PathVariableHandler(pattern.c_str(), method, authHandler));
-}
-
-
-
-void WebServer::requireAuthentication(const String& username, const String& password) {
-  this->username = String(username);
-  this->password = String(password);
-  this->authEnabled = true;
-}
-
-void WebServer::disableAuthentication() {
-  this->authEnabled = false;
-}
-
-bool WebServer::validateAuthentiation() {
-  if (this->authEnabled &&
-    !authenticate(this->username.c_str(), this->password.c_str())) {
-      requestAuthentication();
-      return false;
-    }
-    return true;
-}
-

+ 0 - 42
lib/WebServer/WebServer.h

@@ -1,42 +0,0 @@
-#ifndef _WEBSERVER_H
-#define _WEBSERVER_H
-
-#include <Arduino.h>
-#include <ArduinoJson.h>
-#include <ESP8266WebServer.h>
-#include <PathVariableHandler.h>
-
-#define HTTP_DOWNLOAD_UNIT_SIZE 1460
-#define HTTP_MAX_SEND_WAIT 5000 //ms to wait for data chunk to be ACKed
-#define HTTP_MAX_CLOSE_WAIT 2000 //ms to wait for the client to close the connection
-
-class WebServer : public ESP8266WebServer {
-public:
-  WebServer(int port) : ESP8266WebServer(port) { }
-
-  void onAuthenticated(const String &uri, THandlerFunction handler);
-  void onAuthenticated(const String &uri, HTTPMethod method, THandlerFunction fn);
-  void onAuthenticated(const String &uri, HTTPMethod method, THandlerFunction fn, THandlerFunction ufn);
-  void onPattern(const String& pattern, const HTTPMethod method, PathVariableHandler::TPathVariableHandlerFn fn);
-  void onPatternAuthenticated(const String& pattern, const HTTPMethod method, PathVariableHandler::TPathVariableHandlerFn handler);
-  void requireAuthentication(const String& username, const String& password);
-  void disableAuthentication();
-  bool validateAuthentiation();
-
-  inline bool clientConnected() {
-    return _currentClient && _currentClient.connected();
-  }
-
-  bool authenticationRequired() {
-    return authEnabled;
-  }
-
-protected:
-
-  bool authEnabled;
-  String username;
-  String password;
-
-};
-
-#endif

+ 13 - 5
platformio.ini

@@ -10,24 +10,32 @@
 
 [common]
 framework = arduino
-platform = espressif8266@~2.0
+platform = espressif8266@~1.8
 board_f_cpu = 160000000L
 lib_deps_builtin =
   SPI
 lib_deps_external =
   sidoh/WiFiManager#cmidgley
   RF24@~1.3.2
-  ArduinoJson@~5.13.2
+  ArduinoJson@~6.10.1
   PubSubClient@~2.7
   ratkins/RGBConverter@07010f2
   WebSockets@~2.1.2
   CircularBuffer@~1.2.0
-  PathVariableHandlers@~1.0.0
+  PathVariableHandlers@~2.0.0
+  RichHttpServer@~2.0.2
 extra_scripts =
   pre:.build_web.py
 test_ignore = remote
-upload_speed = 115200
-build_flags = !python .get_version.py -DMQTT_MAX_PACKET_SIZE=250 -DHTTP_UPLOAD_BUFLEN=128 -D FIRMWARE_NAME=milight-hub -Idist -Ilib/DataStructures
+upload_speed = 460800
+build_flags =
+  !python .get_version.py
+  -D MQTT_MAX_PACKET_SIZE=250
+  -D HTTP_UPLOAD_BUFLEN=128
+  -D FIRMWARE_NAME=milight-hub
+  -D RICH_HTTP_REQUEST_BUFFER_SIZE=2048
+  -D RICH_HTTP_RESPONSE_BUFFER_SIZE=2048
+  -Idist -Ilib/DataStructures
 # -D STATE_DEBUG
 # -D DEBUG_PRINTF
 # -D MQTT_DEBUG

+ 20 - 32
src/main.cpp

@@ -23,6 +23,9 @@
 #include <BulbStateUpdater.h>
 #include <LEDStatus.h>
 
+#include <vector>
+#include <memory>
+
 WiFiManager wifiManager;
 // because of callbacks, these need to be in the higher scope :(
 WiFiManagerParameter* wifiStaticIP = NULL;
@@ -34,7 +37,7 @@ static LEDStatus *ledStatus;
 Settings settings;
 
 MiLightClient* milightClient = NULL;
-MiLightRadioFactory* radioFactory = NULL;
+std::shared_ptr<MiLightRadioFactory> radioFactory;
 MiLightHttpServer *httpServer = NULL;
 MqttClient* mqttClient = NULL;
 MiLightDiscoveryServer* discoveryServer = NULL;
@@ -45,40 +48,30 @@ GroupStateStore* stateStore = NULL;
 BulbStateUpdater* bulbStateUpdater = NULL;
 
 int numUdpServers = 0;
-MiLightUdpServer** udpServers = NULL;
+std::vector<std::shared_ptr<MiLightUdpServer>> udpServers;
 WiFiUDP udpSeder;
 
 /**
  * Set up UDP servers (both v5 and v6).  Clean up old ones if necessary.
  */
 void initMilightUdpServers() {
-  if (udpServers) {
-    for (int i = 0; i < numUdpServers; i++) {
-      if (udpServers[i]) {
-        delete udpServers[i];
-      }
-    }
+  udpServers.clear();
 
-    delete udpServers;
-  }
+  for (size_t i = 0; i < settings.gatewayConfigs.size(); ++i) {
+    const GatewayConfig& config = *settings.gatewayConfigs[i];
 
-  udpServers = new MiLightUdpServer*[settings.numGatewayConfigs];
-  numUdpServers = settings.numGatewayConfigs;
-
-  for (size_t i = 0; i < settings.numGatewayConfigs; i++) {
-    GatewayConfig* config = settings.gatewayConfigs[i];
-    MiLightUdpServer* server = MiLightUdpServer::fromVersion(
-      config->protocolVersion,
+    std::shared_ptr<MiLightUdpServer> server = MiLightUdpServer::fromVersion(
+      config.protocolVersion,
       milightClient,
-      config->port,
-      config->deviceId
+      config.port,
+      config.deviceId
     );
 
     if (server == NULL) {
       Serial.print(F("Error creating UDP server with protocol version: "));
-      Serial.println(config->protocolVersion);
+      Serial.println(config.protocolVersion);
     } else {
-      udpServers[i] = server;
+      udpServers.push_back(std::move(server));
       udpServers[i]->begin();
     }
   }
@@ -91,8 +84,8 @@ void initMilightUdpServers() {
  * is read.
  */
 void onPacketSentHandler(uint8_t* packet, const MiLightRemoteConfig& config) {
-  StaticJsonBuffer<200> buffer;
-  JsonObject& result = buffer.createObject();
+  StaticJsonDocument<200> buffer;
+  JsonObject result = buffer.to<JsonObject>();
 
   BulbId bulbId = config.packetFormatter->parsePacket(packet, result);
 
@@ -123,7 +116,7 @@ void onPacketSentHandler(uint8_t* packet, const MiLightRemoteConfig& config) {
   if (mqttClient) {
     // Sends the state delta derived from the raw packet
     char output[200];
-    result.printTo(output);
+    serializeJson(result, output);
     mqttClient->sendUpdate(remoteConfig, bulbId.deviceId, bulbId.groupId, output);
 
     // Sends the entire state
@@ -144,7 +137,7 @@ void handleListen() {
     return;
   }
 
-  MiLightRadio* radio = milightClient->switchRadio(currentRadioType++ % milightClient->getNumRadios());
+  std::shared_ptr<MiLightRadio> radio = milightClient->switchRadio(currentRadioType++ % milightClient->getNumRadios());
 
   for (size_t i = 0; i < settings.listenRepeats; i++) {
     if (milightClient->available()) {
@@ -198,9 +191,6 @@ void applySettings() {
   if (milightClient) {
     delete milightClient;
   }
-  if (radioFactory) {
-    delete radioFactory;
-  }
   if (mqttClient) {
     delete mqttClient;
     delete bulbStateUpdater;
@@ -402,10 +392,8 @@ void loop() {
     bulbStateUpdater->loop();
   }
 
-  if (udpServers) {
-    for (size_t i = 0; i < settings.numGatewayConfigs; i++) {
-      udpServers[i]->handleClient();
-    }
+  for (size_t i = 0; i < udpServers.size(); i++) {
+    udpServers[i]->handleClient();
   }
 
   if (discoveryServer) {

+ 19 - 0
test/remote/spec/rest_spec.rb

@@ -66,4 +66,23 @@ RSpec.describe 'REST Server' do
       expect { @client.get('/settings') }.not_to raise_error
     end
   end
+
+  context 'misc routes' do
+    it 'should respond to /about' do
+      result = @client.get('/about')
+
+      expect(result['firmware']).to eq('milight-hub')
+    end
+
+    it 'should respond to /system' do
+      expect { @client.post('/system', {}) }.to raise_error('400 "Bad Request"')
+    end
+
+    it 'should respond to /remote_configs' do
+      result = @client.get('/remote_configs')
+
+      expect(result).to be_a(Array)
+      expect(result).to include('rgb_cct')
+    end
+  end
 end

+ 23 - 3
test/remote/spec/settings_spec.rb

@@ -37,7 +37,7 @@ RSpec.describe 'Settings' do
       expect(settings['mqtt_server']).to eq('test123')
 
       @client.put('/settings', {mqtt_server: 'abc123', mqtt_username: 'foo'})
-      
+
       settings = @client.get('/settings')
       expect(settings['mqtt_server']).to eq('abc123')
       expect(settings['mqtt_username']).to eq('foo')
@@ -65,6 +65,26 @@ RSpec.describe 'Settings' do
     end
   end
 
+  context 'PUT settings file' do
+    it 'should accept a fairly large request body' do
+      contents = (1..25).reduce({}) { |a, x| a[x] = "test#{x}"*10; a }
+
+      expect { @client.put('/settings', contents) }.to_not raise_error
+    end
+
+    it 'should not cause excessive memory leaks' do
+      start_mem = @client.get('/about')['free_heap']
+
+      20.times do
+        @client.put('/settings', mqtt_username: 'a')
+      end
+
+      end_mem = @client.get('/about')['free_heap']
+
+      expect(end_mem - start_mem).to_not be < -200
+    end
+  end
+
   context 'radio' do
     it 'should store a set of channels' do
       val = %w(HIGH LOW)
@@ -111,7 +131,7 @@ RSpec.describe 'Settings' do
       # Wait for it to come back up
       ping_test = Net::Ping::External.new(static_ip)
 
-      10.times do 
+      10.times do
         break if ping_test.ping?
         sleep 1
       end
@@ -124,7 +144,7 @@ RSpec.describe 'Settings' do
 
       ping_test = Net::Ping::External.new(ENV.fetch('ESPMH_HOSTNAME'))
 
-      10.times do 
+      10.times do
         break if ping_test.ping?
         sleep 1
       end

+ 4 - 25
web/src/index.html

@@ -114,27 +114,6 @@
 
       <div class="col-sm-4">
         <label for="mode">Remote Type</label>
-
-        <!-- <div class="btn-group" id="mode" data-toggle="buttons">
-          <label class="btn btn-secondary active">
-            <input type="radio" name="mode" autocomplete="off" data-value="rgbw" checked> RGBW
-          </label>
-          <label class="btn btn-secondary">
-            <input type="radio" name="mode" autocomplete="off" data-value="cct"> CCT
-          </label>
-          <label class="btn btn-secondary">
-            <input type="radio" name="mode" autocomplete="off" data-value="rgb_cct"> RGB+CCT
-          </label>
-          <label class="btn btn-secondary">
-            <input type="radio" name="mode" autocomplete="off" data-value="rgb"> RGB
-          </label>
-          <label class="btn btn-secondary">
-            <input type="radio" name="mode" autocomplete="off" data-value="fut089"> FUT089
-          </label>
-          <label class="btn btn-secondary">
-            <input type="radio" name="mode" autocomplete="off" data-value="fut091"> FUT091
-          </label>
-        </div> -->
         <div class="dropdown">
           <button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">
             <label>Mode</label>
@@ -426,9 +405,9 @@
                   <input type="file" name="file" style="display: none;">
                 </span>
               </label>
-              
+
               <input type="text" class="form-control" readonly="">
-              
+
               <label class="input-group-btn">
                 <span class="btn btn-success">
                   Upload Firmware
@@ -517,9 +496,9 @@
                     <input type="file" name="file" style="display: none;">
                   </span>
                 </label>
-                
+
                 <input type="text" class="form-control" readonly="">
-                
+
                 <label class="input-group-btn">
                   <span class="btn btn-success">
                     Upload

+ 1 - 1
web/src/js/script.js

@@ -839,7 +839,7 @@ $(function() {
 
   $('.raw-update').change(function() {
     var data = {}
-      , val = $(this).attr('type') == 'checkbox' ? $(this).is(':checked') : $(this).val()
+      , val = $(this).attr('type') == 'checkbox' ? ($(this).is(':checked') ? 'on' : 'off') : $(this).val()
       ;
 
     data[$(this).attr('name')] = val;