Ver código fonte

Merge pull request #8 from sidoh/http_refactor

Pull HTTP server handling out of main
Chris Mullins 8 anos atrás
pai
commit
7ba4e7e7c6

+ 28 - 22
lib/MiLight/MiLightClient.cpp

@@ -2,21 +2,32 @@
 #include <MiLightRadioConfig.h>
 
 MiLightRadio* MiLightClient::getRadio(const MiLightRadioType type) {
-  MiLightRadio* radio = NULL;
+  MiLightRadioStack* stack = NULL;
   
   if (type == RGBW) {
-    return rgbwRadio->getRadio();
+    stack = rgbwRadio;
   } else if (type == CCT) {
-    return cctRadio->getRadio();
+    stack = cctRadio;
   } else if (type == RGBW_CCT) {
-    return rgbwCctRadio->getRadio();
+    stack = rgbwCctRadio;
   }
   
-  if (radio != NULL) {
-    radio->configure();
+  if (stack != NULL) {
+    MiLightRadio *radio = stack->getRadio();
+    
+    if (currentRadio != stack->type) {
+      radio->configure();
+    }
+    
+    currentRadio = stack->type;
+    return radio;
   }
   
-  return radio;
+  return NULL;
+}
+
+void MiLightClient::setResendCount(const unsigned int resendCount) {
+  this->resendCount = resendCount;
 }
 
 uint8_t MiLightClient::nextSequenceNum() {
@@ -25,7 +36,6 @@ uint8_t MiLightClient::nextSequenceNum() {
 
 bool MiLightClient::available(const MiLightRadioType radioType) {
   MiLightRadio* radio = getRadio(radioType);
-  radio->begin();
   
   if (radio == NULL) {
     return false;
@@ -46,8 +56,7 @@ void MiLightClient::read(const MiLightRadioType radioType, uint8_t packet[]) {
 }
 
 void MiLightClient::write(const MiLightRadioType radioType, 
-  uint8_t packet[],
-  const unsigned int resendCount) {
+  uint8_t packet[]) {
     
   MiLightRadio* radio = getRadio(radioType);
   
@@ -55,9 +64,8 @@ void MiLightClient::write(const MiLightRadioType radioType,
     return;
   }
   
-  for (int i = 0; i < resendCount; i++) {
+  for (int i = 0; i < this->resendCount; i++) {
     radio->write(packet, MILIGHT_PACKET_LENGTH);
-    yield();
   }
 }
 
@@ -66,8 +74,7 @@ void MiLightClient::writeRgbw(
   const uint8_t color,
   const uint8_t brightness,
   const uint8_t groupId,
-  const uint8_t button,
-  const unsigned int resendCount) {
+  const uint8_t button) {
   
   uint8_t packet[MilightRgbwConfig.packetLength];
   size_t packetPtr = 0;
@@ -80,13 +87,12 @@ void MiLightClient::writeRgbw(
   packet[packetPtr++] = button;
   packet[packetPtr++] = nextSequenceNum();
   
-  write(RGBW, packet, resendCount);
+  write(RGBW, packet);
 }
 
 void MiLightClient::writeCct(const uint16_t deviceId,
   const uint8_t groupId,
-  const uint8_t button,
-  const unsigned int resendCount) {
+  const uint8_t button) {
     
   uint8_t packet[MilightRgbwConfig.packetLength];
   uint8_t sequenceNum = nextSequenceNum();
@@ -100,7 +106,7 @@ void MiLightClient::writeCct(const uint16_t deviceId,
   packet[packetPtr++] = sequenceNum;
   packet[packetPtr++] = sequenceNum;
   
-  write(CCT, packet, resendCount);
+  write(CCT, packet);
 }
     
 void MiLightClient::updateColorRaw(const uint16_t deviceId, const uint8_t groupId, const uint16_t color) {
@@ -185,11 +191,11 @@ void MiLightClient::allOff(const MiLightRadioType type, const uint16_t deviceId)
 }
 
 void MiLightClient::increaseCctBrightness(const uint16_t deviceId, const uint8_t groupId) {
-  writeCct(deviceId, groupId, CCT_BRIGHTNESS_UP, 10);
+  writeCct(deviceId, groupId, CCT_BRIGHTNESS_UP);
 }
 
 void MiLightClient::decreaseCctBrightness(const uint16_t deviceId, const uint8_t groupId) {
-  writeCct(deviceId, groupId, CCT_BRIGHTNESS_DOWN, 10);
+  writeCct(deviceId, groupId, CCT_BRIGHTNESS_DOWN);
 }
 
 void MiLightClient::updateCctBrightness(const uint16_t deviceId, const uint8_t groupId, const uint8_t brightness) {
@@ -202,11 +208,11 @@ void MiLightClient::updateCctBrightness(const uint16_t deviceId, const uint8_t g
 }
 
 void MiLightClient::increaseTemperature(const uint16_t deviceId, const uint8_t groupId) {
-  writeCct(deviceId, groupId, CCT_TEMPERATURE_UP, 10);
+  writeCct(deviceId, groupId, CCT_TEMPERATURE_UP);
 }
 
 void MiLightClient::decreaseTemperature(const uint16_t deviceId, const uint8_t groupId) {
-  writeCct(deviceId, groupId, CCT_TEMPERATURE_DOWN, 10);
+  writeCct(deviceId, groupId, CCT_TEMPERATURE_DOWN);
 }
 
 void MiLightClient::updateTemperature(const uint16_t deviceId, const uint8_t groupId, const uint8_t temperature) {

+ 15 - 15
lib/MiLight/MiLightClient.h

@@ -9,20 +9,15 @@
 
 #define MILIGHT_PACKET_LENGTH 7
 #define MILIGHT_CCT_INTERVALS 10
-#define MILIGHT_DEFAULT_RESEND_COUNT 50
-
-enum MiLightRadioType {
-  UNKNOWN = 0,
-  RGBW  = 0xB8,
-  CCT   = 0x5A,
-  RGBW_CCT = 0x99
-};
+#define MILIGHT_DEFAULT_RESEND_COUNT 10
 
 enum MiLightStatus { ON = 0, OFF = 1 };
 
 class MiLightRadioStack {
 public:
-  MiLightRadioStack(RF24& rf, const MiLightRadioConfig& config) {
+  MiLightRadioStack(RF24& rf, const MiLightRadioConfig& config) 
+    : type(config.type)
+  {
     nrf = new PL1167_nRF24(rf);
     radio = new MiLightRadio(*nrf, config);
   }
@@ -36,6 +31,8 @@ public:
     return this->radio;
   }
   
+  const MiLightRadioType& type;
+  
 private:
   PL1167_nRF24 *nrf;
   MiLightRadio *radio;
@@ -45,7 +42,8 @@ class MiLightClient {
   public:
     MiLightClient(uint8_t cePin, uint8_t csnPin)
     : sequenceNum(0),
-      rf(RF24(cePin, csnPin))
+      rf(RF24(cePin, csnPin)),
+      resendCount(MILIGHT_DEFAULT_RESEND_COUNT)
     {
       rgbwRadio = new MiLightRadioStack(rf, MilightRgbwConfig);
       cctRadio = new MiLightRadioStack(rf, MilightCctConfig);
@@ -64,24 +62,24 @@ class MiLightClient {
       rgbwCctRadio->getRadio()->begin();
     }
     
+    void setResendCount(const unsigned int resendCount);
+    
     bool available(const MiLightRadioType radioType);
     void read(const MiLightRadioType radioType, uint8_t packet[]);
-    void write(const MiLightRadioType radioType, uint8_t packet[], const unsigned int resendCount = MILIGHT_DEFAULT_RESEND_COUNT);
+    void write(const MiLightRadioType radioType, uint8_t packet[]);
     
     void writeRgbw(
       const uint16_t deviceId,
       const uint8_t color,
       const uint8_t brightness,
       const uint8_t groupId,
-      const uint8_t button,
-      const unsigned int resendCount = MILIGHT_DEFAULT_RESEND_COUNT
+      const uint8_t button
     );
     
     void writeCct(
       const uint16_t deviceId,
       const uint8_t groupId,
-      const uint8_t button,
-      const unsigned int resendCount = MILIGHT_DEFAULT_RESEND_COUNT
+      const uint8_t button
     );
     
     // Common methods
@@ -116,9 +114,11 @@ class MiLightClient {
     MiLightRadioStack* rgbwRadio;
     MiLightRadioStack* cctRadio;
     MiLightRadioStack* rgbwCctRadio;
+    MiLightRadioType currentRadio;
     
     uint8_t sequenceNum;
     uint8_t nextSequenceNum();
+    unsigned int resendCount;
 };
 
 #endif

+ 0 - 4
lib/MiLight/MiLightRadio.cpp

@@ -62,8 +62,6 @@ int MiLightRadio::configure() {
 
 bool MiLightRadio::available()
 {
-  configure();
-  
   if (_waiting) {
 #ifdef DEBUG_PRINTF
   printf("_waiting\n");
@@ -123,8 +121,6 @@ int MiLightRadio::write(uint8_t frame[], size_t frame_length)
   if (frame_length > sizeof(_out_packet) - 1) {
     return -1;
   }
-  
-  configure();
 
   memcpy(_out_packet + 1, frame, frame_length);
   _out_packet[0] = frame_length;

+ 15 - 5
lib/MiLight/MiLightRadioConfig.h

@@ -3,18 +3,27 @@
 #ifndef _MILIGHT_RADIO_CONFIG
 #define _MILIGHT_RADIO_CONFIG 
 
+enum MiLightRadioType {
+  UNKNOWN = 0,
+  RGBW  = 0xB8,
+  CCT   = 0x5A,
+  RGBW_CCT = 0x99
+};
+
 class MiLightRadioConfig {
 public:
   MiLightRadioConfig(const uint16_t syncword0,
   const uint16_t syncword3,
   const size_t packetLength,
   const uint8_t* channels,
-  const size_t numChannels) 
+  const size_t numChannels,
+  const MiLightRadioType type) 
     : syncword0(syncword0),
       syncword3(syncword3),
       packetLength(packetLength),
       channels(channels),
-      numChannels(numChannels)
+      numChannels(numChannels),
+      type(type)
   {}
     
   const uint16_t syncword0;
@@ -22,21 +31,22 @@ public:
   const size_t packetLength;
   const uint8_t* channels;
   const size_t numChannels;
+  const MiLightRadioType type;
 };
 
 const uint8_t RGBW_CHANNELS[] = {9, 40, 71};
 static MiLightRadioConfig MilightRgbwConfig(
-  0x147A, 0x258B, 7, RGBW_CHANNELS, 3
+  0x147A, 0x258B, 7, RGBW_CHANNELS, 3, RGBW
 );
 
 const uint8_t CCT_CHANNELS[] = {4, 39, 74};
 static MiLightRadioConfig MilightCctConfig(
-  0x050A, 0x55AA, 7, CCT_CHANNELS, 3
+  0x050A, 0x55AA, 7, CCT_CHANNELS, 3, CCT
 );
 
 const uint8_t RGBWCCT_CHANNELS[] = {70, 39, 8};
 static MiLightRadioConfig MilightRgbwCctConfig(
-  0x7236, 0x1809, 8, RGBWCCT_CHANNELS, 3
+  0x7236, 0x1809, 8, RGBWCCT_CHANNELS, 3, RGBW_CCT
 );
 
 #endif

+ 8 - 0
lib/MiLight/MiLightUdpServer.cpp

@@ -25,6 +25,14 @@ void MiLightUdpServer::handleClient() {
   if (packetSize) {
     if (packetSize >= 2 && packetSize <= 3) {
       socket.read(packetBuffer, packetSize);
+      
+#ifdef MILIGHT_UDP_DEBUG
+      Serial.print("Handling command: ");
+      Serial.print(String(packetBuffer[0], HEX));
+      Serial.print(" ");
+      Serial.println(String(packetBuffer[1], HEX));
+#endif
+      
       handleCommand(packetBuffer[0], packetBuffer[1]);
     } else {
       Serial.print("Error, unexpected packet length (should always be 2-3, was: ");

+ 3 - 0
lib/MiLight/MiLightUdpServer.h

@@ -7,6 +7,9 @@
 
 #define MILIGHT_PACKET_BUFFER_SIZE 10
 
+// Uncomment to enable Serial printing of packets
+//#define MILIGHT_UDP_DEBUG
+
 #ifndef _MILIGHT_UDP_SERVER
 #define _MILIGHT_UDP_SERVER 
 

+ 3 - 0
lib/MiLight/PL1167_nRF24.cpp

@@ -191,6 +191,7 @@ int PL1167_nRF24::transmit(uint8_t channel)
     if (retval < 0) {
       return retval;
     }
+    yield();
   }
 
   _radio.stopListening();
@@ -260,6 +261,8 @@ int PL1167_nRF24::transmit(uint8_t channel)
       buffer_fill -= 8;
     }
   }
+  
+  yield();
 
   _radio.write(tmp, outp);
   return 0;

+ 279 - 0
lib/WebServer/MiLightHttpServer.cpp

@@ -0,0 +1,279 @@
+#include <fs.h>
+#include <WiFiUdp.h>
+#include <IntParsing.h>
+#include <Settings.h>
+#include <MiLightHttpServer.h>
+#include <MiLightRadioConfig.h>
+
+void MiLightHttpServer::begin() {
+  server.on("/", HTTP_GET, handleServeFile(WEB_INDEX_FILENAME, "text/html"));
+  server.on("/settings", HTTP_GET, handleServeFile(SETTINGS_FILE, "application/json"));
+  server.on("/settings", HTTP_PUT, [this]() { handleUpdateSettings(); });
+  server.on("/gateway_traffic", HTTP_GET, [this]() { handleListenGateway(); });
+  server.onPattern("/gateways/:device_id/:type/:group_id", HTTP_PUT, [this](const UrlTokenBindings* b) { handleUpdateGroup(b); });
+  server.onPattern("/gateways/:device_id/:type", HTTP_PUT, [this](const UrlTokenBindings* b) { handleUpdateGateway(b); });
+  server.on("/web", HTTP_POST, [this]() { server.send(200, "text/plain", "success"); }, handleUpdateFile(WEB_INDEX_FILENAME));
+  server.on("/firmware", HTTP_POST, 
+    [this](){
+      server.sendHeader("Connection", "close");
+      server.sendHeader("Access-Control-Allow-Origin", "*");
+      server.send(200, "text/plain", (Update.hasError())?"FAIL":"OK");
+      ESP.restart();
+    },
+    [this](){
+      HTTPUpload& upload = server.upload();
+      if(upload.status == UPLOAD_FILE_START){
+        WiFiUDP::stopAll();
+        uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
+        if(!Update.begin(maxSketchSpace)){//start with max available size
+          Update.printError(Serial);
+        }
+      } else if(upload.status == UPLOAD_FILE_WRITE){
+        if(Update.write(upload.buf, upload.currentSize) != upload.currentSize){
+          Update.printError(Serial);
+        }
+      } else if(upload.status == UPLOAD_FILE_END){
+        if(Update.end(true)){ //true to set the size to the current progress
+        } else {
+          Update.printError(Serial);
+        }
+      }
+      yield();
+    }
+  );
+  
+  server.begin();
+}
+
+void MiLightHttpServer::handleClient() {
+  server.handleClient();
+}
+
+void MiLightHttpServer::applySettings(Settings& settings) {
+  if (server.authenticationRequired() && !settings.hasAuthSettings()) {
+    server.disableAuthentication();
+  } else {
+    server.requireAuthentication(settings.adminUsername, settings.adminPassword);
+  }
+}
+
+void MiLightHttpServer::onSettingsSaved(SettingsSavedHandler handler) {
+  this->settingsSavedHandler = handler;
+}
+  
+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) {
+  if (SPIFFS.exists(file)) {
+    File f = SPIFFS.open(file, "r");
+    server.send(200, contentType, f.readString());
+    f.close();
+    return true;
+  }
+  
+  return false;
+}
+
+ESP8266WebServer::THandlerFunction MiLightHttpServer::handleUpdateFile(const char* filename) {
+  return [this, 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("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);
+  
+  if (parsedSettings.success()) {
+    settings.patch(parsedSettings);
+    settings.save();
+    
+    this->applySettings(settings);
+    this->settingsSavedHandler();
+    
+    server.send(200, "application/json", "true");
+  } else {
+    server.send(400, "application/json", "\"Invalid JSON\"");
+  }
+}
+
+void MiLightHttpServer::handleListenGateway() {
+  uint8_t readType = 0;
+  MiLightRadioConfig *config;
+  
+  while (readType == 0) {
+    if (!server.clientConnected()) {
+      return;
+    }
+    
+    if (milightClient->available(RGBW)) {
+      readType = RGBW;
+      config = &MilightRgbwConfig;
+    } else if (milightClient->available(CCT)) {
+      readType = CCT;
+      config = &MilightCctConfig;
+    } else if (milightClient->available(RGBW_CCT)) {
+      readType = RGBW_CCT;
+      config = &MilightRgbwCctConfig;
+    }
+    
+    yield();
+  }
+  
+  uint8_t packet[config->packetLength];
+  milightClient->read(static_cast<MiLightRadioType>(readType), packet);
+  
+  String response = "Packet received (";
+  response += String(sizeof(packet)) + " bytes)";
+  response += ":\n";
+  
+  for (int i = 0; i < sizeof(packet); i++) {
+    response += String(packet[i], HEX) + " ";
+  }
+  
+  response += "\n\n";
+  
+  server.send(200, "text/plain", response);
+}
+
+void MiLightHttpServer::handleUpdateGroup(const UrlTokenBindings* urlBindings) {
+  DynamicJsonBuffer buffer;
+  JsonObject& request = buffer.parse(server.arg("plain"));
+  
+  if (!request.success()) {
+    server.send(400, "text/plain", "Invalid JSON");
+    return;
+  }
+  
+  const uint16_t deviceId = parseInt<uint16_t>(urlBindings->get("device_id"));
+  const uint8_t groupId = urlBindings->get("group_id").toInt();
+  const MiLightRadioType type = MiLightClient::getRadioType(urlBindings->get("type"));
+  
+  if (type == UNKNOWN) {
+    String body = "Unknown device type: ";
+    body += urlBindings->get("type");
+    
+    server.send(400, "text/plain", body);
+    return;
+  }
+  
+  milightClient->setResendCount(MILIGHT_RESEND_COUNT_FOR_HTTP);
+    
+  if (request.containsKey("status")) {
+    const String& statusStr = request.get<String>("status");
+    MiLightStatus status = (statusStr == "on" || statusStr == "true") ? ON : OFF;
+    milightClient->updateStatus(type, deviceId, groupId, status);
+  }
+      
+  if (request.containsKey("command")) {
+    if (request["command"] == "unpair") {
+      milightClient->unpair(type, deviceId, groupId);
+    }
+    
+    if (request["command"] == "pair") {
+      milightClient->pair(type, deviceId, groupId);
+    }
+  }
+  
+  if (type == RGBW) {
+    if (request.containsKey("hue")) {
+      milightClient->updateHue(deviceId, groupId, request["hue"]);
+    }
+    
+    if (request.containsKey("level")) {
+      milightClient->updateBrightness(deviceId, groupId, request["level"]);
+    }
+    
+    if (request.containsKey("command")) {
+      if (request["command"] == "set_white") {
+        milightClient->updateColorWhite(deviceId, groupId);
+      }
+    }
+  } else if (type == CCT) {
+    if (request.containsKey("temperature")) {
+      milightClient->updateTemperature(deviceId, groupId, request["temperature"]);
+    }
+    
+    if (request.containsKey("level")) {
+      milightClient->updateCctBrightness(deviceId, groupId, request["level"]);
+    }
+    
+    if (request.containsKey("command")) {
+      // CCT command work more effectively with a lower number of repeats it seems.
+      milightClient->setResendCount(MILIGHT_DEFAULT_RESEND_COUNT);
+      
+      if (request["command"] == "level_up") {
+        milightClient->increaseCctBrightness(deviceId, groupId);
+      }
+      
+      if (request["command"] == "level_down") {
+        milightClient->decreaseCctBrightness(deviceId, groupId);
+      }
+      
+      if (request["command"] == "temperature_up") {
+        milightClient->increaseTemperature(deviceId, groupId);
+      }
+      
+      if (request["command"] == "temperature_down") {
+        milightClient->decreaseTemperature(deviceId, groupId);
+      }
+    }
+  } 
+  
+  milightClient->setResendCount(MILIGHT_DEFAULT_RESEND_COUNT);
+  
+  server.send(200, "application/json", "true");
+}
+
+void MiLightHttpServer::handleUpdateGateway(const UrlTokenBindings* urlBindings) {
+  DynamicJsonBuffer buffer;
+  JsonObject& request = buffer.parse(server.arg("plain"));
+  
+  const uint16_t deviceId = parseInt<uint16_t>(urlBindings->get("device_id"));
+  const MiLightRadioType type = MiLightClient::getRadioType(urlBindings->get("type"));
+  
+  if (type == UNKNOWN) {
+    String body = "Unknown device type: ";
+    body += urlBindings->get("type");
+    
+    server.send(400, "text/plain", body);
+    return;
+  }
+  
+  milightClient->setResendCount(MILIGHT_DEFAULT_RESEND_COUNT);
+  
+  if (request.containsKey("status")) {
+    if (request["status"] == "on") {
+      milightClient->allOn(type, deviceId);
+    } else if (request["status"] == "off") {
+      milightClient->allOff(type, deviceId);
+    }
+  }
+  
+  server.send(200, "application/json", "true");
+}

+ 50 - 0
lib/WebServer/MiLightHttpServer.h

@@ -0,0 +1,50 @@
+#include <WebServer.h>
+#include <MiLightClient.h>
+#include <Settings.h>
+
+#ifndef _MILIGHT_HTTP_SERVER
+#define _MILIGHT_HTTP_SERVER 
+
+#define MILIGHT_RESEND_COUNT_FOR_HTTP 50
+
+typedef std::function<void(void)> SettingsSavedHandler;
+
+class MiLightHttpServer {
+public:
+  MiLightHttpServer(Settings& settings, MiLightClient*& milightClient)
+    : server(WebServer(80)),
+      milightClient(milightClient),
+      settings(settings)
+  { 
+    this->applySettings(settings);
+  }
+  
+  void begin();
+  void handleClient();
+  void onSettingsSaved(SettingsSavedHandler handler);
+  
+protected:
+  ESP8266WebServer::THandlerFunction handleServeFile(
+    const char* filename, 
+    const char* contentType, 
+    const char* defaultText = NULL); 
+    
+  bool serveFile(const char* file, const char* contentType = "text/html");
+  ESP8266WebServer::THandlerFunction handleUpdateFile(const char* filename);
+  void applySettings(Settings& settings);
+  
+  void handleUpdateSettings();
+  void handleListenGateway();
+  void handleUpdateGroup(const UrlTokenBindings* urlBindings);
+  void handleUpdateGateway(const UrlTokenBindings* urlBindings);
+  
+  File updateFile;
+  
+  WebServer server;
+  Settings& settings;
+  MiLightClient*& milightClient;
+  SettingsSavedHandler settingsSavedHandler;
+  
+};
+
+#endif

+ 1 - 1
platformio.ini

@@ -9,13 +9,13 @@
 ; http://docs.platformio.org/page/projectconf.html
 
 [common]
+board_f_cpu = 160000000L
 lib_deps_builtin = 
   SPI
 lib_deps_external = 
   RF24
   WiFiManager
   ArduinoJson
-  Vector
 
 [env:nodemcuv2]
 platform = espressif8266

+ 15 - 265
src/main.cpp

@@ -3,214 +3,22 @@
 #include <ArduinoJson.h>
 #include <stdlib.h>
 #include <fs.h>
+#include <IntParsing.h>
 #include <MiLightClient.h>
 #include <MiLightRadioConfig.h>
-#include <WebServer.h>
-#include <IntParsing.h>
+#include <MiLightHttpServer.h>
 #include <Settings.h>
 #include <MiLightUdpServer.h>
 
-MiLightClient* milightClient;
-MiLightUdpServer** udpServers;
-int numUdpServers = 0;
 WiFiManager wifiManager;
-WebServer *server = new WebServer(80);
-File updateFile;
-Settings settings;
-
-void handleUpdateGateway(const UrlTokenBindings* urlBindings) {
-  DynamicJsonBuffer buffer;
-  JsonObject& request = buffer.parse(server->arg("plain"));
-  
-  const uint16_t deviceId = parseInt<uint16_t>(urlBindings->get("device_id"));
-  const MiLightRadioType type = MiLightClient::getRadioType(urlBindings->get("type"));
-  
-  if (type == UNKNOWN) {
-    String body = "Unknown device type: ";
-    body += urlBindings->get("type");
-    
-    server->send(400, "text/plain", body);
-    return;
-  }
-  
-  if (request.containsKey("status")) {
-    if (request["status"] == "on") {
-      milightClient->allOn(type, deviceId);
-    } else if (request["status"] == "off") {
-      milightClient->allOff(type, deviceId);
-    }
-  }
-  
-  server->send(200, "application/json", "true");
-}
-
-void handleUpdateGroup(const UrlTokenBindings* urlBindings) {
-  DynamicJsonBuffer buffer;
-  JsonObject& request = buffer.parse(server->arg("plain"));
-  
-  if (!request.success()) {
-    server->send(400, "text/plain", "Invalid JSON");
-    return;
-  }
-  
-  const uint16_t deviceId = parseInt<uint16_t>(urlBindings->get("device_id"));
-  const uint8_t groupId = urlBindings->get("group_id").toInt();
-  const MiLightRadioType type = MiLightClient::getRadioType(urlBindings->get("type"));
-  
-  if (type == UNKNOWN) {
-    String body = "Unknown device type: ";
-    body += urlBindings->get("type");
-    
-    server->send(400, "text/plain", body);
-    return;
-  }
-    
-  if (request.containsKey("status")) {
-    const String& statusStr = request.get<String>("status");
-    MiLightStatus status = (statusStr == "on" || statusStr == "true") ? ON : OFF;
-    milightClient->updateStatus(type, deviceId, groupId, status);
-  }
-      
-  if (request.containsKey("command")) {
-    if (request["command"] == "unpair") {
-      milightClient->unpair(type, deviceId, groupId);
-    }
-    
-    if (request["command"] == "pair") {
-      milightClient->pair(type, deviceId, groupId);
-    }
-  }
-  
-  if (type == RGBW) {
-    if (request.containsKey("hue")) {
-      milightClient->updateHue(deviceId, groupId, request["hue"]);
-    }
-    
-    if (request.containsKey("level")) {
-      milightClient->updateBrightness(deviceId, groupId, request["level"]);
-    }
-    
-    if (request.containsKey("command")) {
-      if (request["command"] == "set_white") {
-        milightClient->updateColorWhite(deviceId, groupId);
-      }
-    }
-  } else if (type == CCT) {
-    if (request.containsKey("temperature")) {
-      milightClient->updateTemperature(deviceId, groupId, request["temperature"]);
-    }
-    
-    if (request.containsKey("level")) {
-      milightClient->updateCctBrightness(deviceId, groupId, request["level"]);
-    }
-    
-    if (request.containsKey("command")) {
-      if (request["command"] == "level_up") {
-        milightClient->increaseCctBrightness(deviceId, groupId);
-      }
-      
-      if (request["command"] == "level_down") {
-        milightClient->decreaseCctBrightness(deviceId, groupId);
-      }
-      
-      if (request["command"] == "temperature_up") {
-        milightClient->increaseTemperature(deviceId, groupId);
-      }
-      
-      if (request["command"] == "temperature_down") {
-        milightClient->decreaseTemperature(deviceId, groupId);
-      }
-    }
-  } 
-  
-  server->send(200, "application/json", "true");
-}
 
-void handleListenGateway() {
-  uint8_t readType = 0;
-  MiLightRadioConfig *config;
-  
-  while (readType == 0) {
-    if (!server->clientConnected()) {
-      return;
-    }
-    
-    if (milightClient->available(RGBW)) {
-      readType = RGBW;
-      config = &MilightRgbwConfig;
-    } else if (milightClient->available(CCT)) {
-      readType = CCT;
-      config = &MilightCctConfig;
-    } else if (milightClient->available(RGBW_CCT)) {
-      readType = RGBW_CCT;
-      config = &MilightRgbwCctConfig;
-    }
-    
-    yield();
-  }
-  
-  uint8_t packet[config->packetLength];
-  milightClient->read(static_cast<MiLightRadioType>(readType), packet);
-  
-  String response = "Packet received (";
-  response += String(sizeof(packet)) + " bytes)";
-  response += ":\n";
-  
-  for (int i = 0; i < sizeof(packet); i++) {
-    response += String(packet[i], HEX) + " ";
-  }
-  
-  response += "\n\n";
-  
-  server->send(200, "text/plain", response);
-}
-
-bool serveFile(const char* file, const char* contentType = "text/html") {
-  if (SPIFFS.exists(file)) {
-    File f = SPIFFS.open(file, "r");
-    server->send(200, contentType, f.readString());
-    f.close();
-    return true;
-  }
-  
-  return false;
-}
-
-ESP8266WebServer::THandlerFunction handleServeFile(const char* filename, 
-  const char* contentType, 
-  const char* defaultText = NULL) {
-    
-  return [filename, contentType, defaultText]() {
-    if (!serveFile(filename)) {
-      if (defaultText) {
-        server->send(200, contentType, defaultText);
-      } else {
-        server->send(404);
-      }
-    }
-  };
-}
+Settings settings;
 
-ESP8266WebServer::THandlerFunction handleUpdateFile(const char* filename) {
-  return [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("Error updating web file");
-      }
-    } else if (upload.status == UPLOAD_FILE_END) {
-      updateFile.close();
-    }
-  };
-  yield();
-}
+MiLightClient* milightClient;
+MiLightHttpServer *httpServer;
 
-void onWebUpdated() {
-  server->send(200, "text/plain", "success");
-}
+int numUdpServers = 0;
+MiLightUdpServer** udpServers;
 
 void initMilightUdpServers() {
   if (udpServers) {
@@ -248,27 +56,9 @@ void initMilightClient() {
   milightClient->begin();
 }
 
-void handleUpdateSettings() {
-  DynamicJsonBuffer buffer;
-  const String& rawSettings = server->arg("plain");
-  JsonObject& parsedSettings = buffer.parse(rawSettings);
-  
-  if (parsedSettings.success()) {
-    settings.patch(parsedSettings);
-    settings.save();
-    initMilightClient();
-    initMilightUdpServers();
-    
-    if (server->authenticationRequired() && !settings.hasAuthSettings()) {
-      server->disableAuthentication();
-    } else {
-      server->requireAuthentication(settings.adminUsername, settings.adminPassword);
-    }
-    
-    server->send(200, "application/json", "true");
-  } else {
-    server->send(400, "application/json", "\"Invalid JSON\"");
-  }
+void applySettings() {
+  initMilightClient();
+  initMilightUdpServers();
 }
 
 void setup() {
@@ -276,58 +66,18 @@ void setup() {
   wifiManager.autoConnect();
   SPIFFS.begin();
   Settings::load(settings);
-  initMilightClient();
-  initMilightUdpServers();
-  
-  server->on("/", HTTP_GET, handleServeFile(WEB_INDEX_FILENAME, "text/html"));
-  server->on("/settings", HTTP_GET, handleServeFile(SETTINGS_FILE, "application/json"));
-  server->on("/settings", HTTP_PUT, handleUpdateSettings);
-  server->on("/gateway_traffic", HTTP_GET, handleListenGateway);
-  server->onPattern("/gateways/:device_id/:type/:group_id", HTTP_PUT, handleUpdateGroup);
-  server->onPattern("/gateways/:device_id/:type", HTTP_PUT, handleUpdateGateway);
-  server->on("/web", HTTP_POST, onWebUpdated, handleUpdateFile(WEB_INDEX_FILENAME));
-  server->on("/firmware", HTTP_POST, 
-    [](){
-      server->sendHeader("Connection", "close");
-      server->sendHeader("Access-Control-Allow-Origin", "*");
-      server->send(200, "text/plain", (Update.hasError())?"FAIL":"OK");
-      ESP.restart();
-    },
-    [](){
-      HTTPUpload& upload = server->upload();
-      if(upload.status == UPLOAD_FILE_START){
-        WiFiUDP::stopAll();
-        uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
-        if(!Update.begin(maxSketchSpace)){//start with max available size
-          Update.printError(Serial);
-        }
-      } else if(upload.status == UPLOAD_FILE_WRITE){
-        if(Update.write(upload.buf, upload.currentSize) != upload.currentSize){
-          Update.printError(Serial);
-        }
-      } else if(upload.status == UPLOAD_FILE_END){
-        if(Update.end(true)){ //true to set the size to the current progress
-        } else {
-          Update.printError(Serial);
-        }
-      }
-      yield();
-    }
-  );
-  
-  if (settings.adminUsername.length() > 0 && settings.adminPassword.length() > 0) {
-    server->requireAuthentication(settings.adminUsername, settings.adminPassword);
-  }
+  applySettings();
   
-  server->begin();
+  httpServer = new MiLightHttpServer(settings, milightClient);
+  httpServer->onSettingsSaved(applySettings);
+  httpServer->begin();
 }
 
 void loop() {
-  server->handleClient();
+  httpServer->handleClient();
   
   if (udpServers) {
     for (size_t i = 0; i < settings.numGatewayConfigs; i++) {
-      yield();
       udpServers[i]->handleClient();
     }
   }