From 8d02ecc19cb766a041f2676999c33b0513739305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xose=20P=C3=A9rez?= Date: Sun, 17 Jul 2016 15:33:35 +0200 Subject: [PATCH] OTA auto update server and other small changes --- README.md | 12 +- code/lib/DebounceEvent/DebounceEvent.cpp | 2 - code/platformio.ini | 6 +- code/src/{code.ino => main.cpp} | 628 ++++++++++++++--------- server/.gitignore | 4 + server/README.md | 8 + server/composer.json | 21 + server/composer.lock | 532 +++++++++++++++++++ server/data/versions.json | 14 + server/public/.htaccess | 10 + server/public/index.php | 30 ++ server/src/dependencies.php | 36 ++ server/src/middleware.php | 4 + server/src/routes.php | 36 ++ server/src/settings.php | 26 + server/templates/index.phtml | 57 ++ 16 files changed, 1189 insertions(+), 237 deletions(-) rename code/src/{code.ino => main.cpp} (82%) create mode 100644 server/.gitignore create mode 100644 server/README.md create mode 100644 server/composer.json create mode 100644 server/composer.lock create mode 100644 server/data/versions.json create mode 100644 server/public/.htaccess create mode 100644 server/public/index.php create mode 100644 server/src/dependencies.php create mode 100644 server/src/middleware.php create mode 100644 server/src/routes.php create mode 100644 server/src/settings.php create mode 100644 server/templates/index.phtml diff --git a/README.md b/README.md index c14f30c2..c1a1139b 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,8 @@ Once you have flashed it you can flash it again over-the-air using the ```ota``` When using OTA environment it defaults to the IP address of the device in SoftAP mode. If you want to flash it when connected to your home network best way is to supply the IP of the device: ```bash -> platformio run --target upload -e ota --upload_port 192.168.1.151 -> platformio run --target uploadfs -e ota --upload_port 192.168.1.151 +> platformio run --target upload -e ota --upload-port 192.168.1.151 +> platformio run --target uploadfs -e ota --upload-port 192.168.1.151 ``` @@ -84,7 +84,15 @@ you will be able to switch it on/off sending "1"/"0" to "/home/living/switch/set You can also use "{identifier}" as place holder in the topic. It will be translated to your device ID (same as the soft AP network it creates). +## Troubleshooting + +After flashing the firmware via serial do a hard reset of the device (unplug & plug). There is an issue with the ESP.reset() method. Check [https://github.com/esp8266/Arduino/issues/1017][4] for more info. + +Current version of ESP8266httpUpdate restarts the modules after SPIFFS update, thus preventing the firmware to be updated too. There is a recent commit fixing that which is not yet pushed to PLatformIO. Check [Fix example for ESP8266httpUpdate][5] for more info. + [1]: https://www.itead.cc/sonoff-wifi-wireless-switch.html [2]: http://tinkerman.cat/adding-rf-to-a-non-rf-itead-sonoff [3]: http://www.platformio.org +[4]: https://github.com/esp8266/Arduino/issues/1017 +[5]: https://github.com/esp8266/Arduino/pull/2251 diff --git a/code/lib/DebounceEvent/DebounceEvent.cpp b/code/lib/DebounceEvent/DebounceEvent.cpp index cc458f8a..842d4716 100644 --- a/code/lib/DebounceEvent/DebounceEvent.cpp +++ b/code/lib/DebounceEvent/DebounceEvent.cpp @@ -65,7 +65,6 @@ bool DebounceEvent::loop() { } else if (millis() - _last_start < DOUBLE_CLICK_DELAY ) { _event = EVENT_DOUBLE_CLICK; } else { - Serial.println("deferring"); changed = false; pending = true; //_event = EVENT_SINGLE_CLICK; @@ -84,7 +83,6 @@ bool DebounceEvent::loop() { } if (pending && (millis() - _this_start > DOUBLE_CLICK_DELAY) && (!changed) && (_status == _defaultStatus)) { - Serial.println("catched"); pending = false; changed = true; _event = EVENT_SINGLE_CLICK; diff --git a/code/platformio.ini b/code/platformio.ini index 1ea0f7de..8cfd091a 100644 --- a/code/platformio.ini +++ b/code/platformio.ini @@ -24,19 +24,19 @@ platform = espressif framework = arduino board = esp01_1m -lib_install = 89 +lib_install = 89,64 [env:node] platform = espressif framework = arduino board = nodemcuv2 -lib_install = 89 +lib_install = 89,64 [env:ota] platform = espressif framework = arduino board = esp01_1m -lib_install = 89 +lib_install = 89,64 upload_speed = 115200 upload_port = "192.168.4.1" upload_flags = --auth=fibonacci --port 8266 diff --git a/code/src/code.ino b/code/src/main.cpp similarity index 82% rename from code/src/code.ino rename to code/src/main.cpp index c09163fc..169c9ef2 100644 --- a/code/src/code.ino +++ b/code/src/main.cpp @@ -21,6 +21,7 @@ along with this program. If not, see . #include #include #include +#include #include #include #include @@ -30,6 +31,8 @@ along with this program. If not, see . #include "FS.h" #include #include +#include +#include // ----------------------------------------------------------------------------- // ConfiguraciĆ³ @@ -39,14 +42,19 @@ along with this program. If not, see . #define ENABLE_RF 1 #define ENABLE_OTA 1 +#define ENABLE_OTA_AUTO 0 #define ENABLE_MQTT 1 #define ENABLE_WEBSERVER 1 -#define ENABLE_ENERGYMONITOR 1 +#define ENABLE_ENERGYMONITOR 0 -#define APP_NAME "Espurna 0.9.1" +#define APP_NAME "Espurna" +#define APP_VERSION "0.9.2" #define APP_AUTHOR "xose.perez@gmail.com" #define APP_WEBSITE "http://tinkerman.cat" +#define OTA_SERVER "http://192.168.1.100" +#define OTA_CHECK_INTERVAL 30000 + #define MODEL "SONOFF" #define BUTTON_PIN 0 #define RELAY_PIN 12 @@ -165,6 +173,31 @@ char * getCompileTime(char * buffer) { } +char * getIdentifier() { + if (identifier[0] == 0) { + sprintf(identifier, "%s_%06X", MODEL, ESP.getChipId()); + } + return identifier; +} + +void blink(unsigned long delayOff, unsigned long delayOn) { + static unsigned long next = millis(); + static bool status = HIGH; + if (next < millis()) { + status = !status; + digitalWrite(LED_PIN, status); + next += ((status) ? delayOff : delayOn); + } +} + +void showStatus() { + if (WiFi.status() == WL_CONNECTED) { + blink(5000, 500); + } else { + blink(500, 500); + } +} + // ----------------------------------------------------------------------------- // Relay // ----------------------------------------------------------------------------- @@ -214,16 +247,298 @@ void toggleRelay() { } // ----------------------------------------------------------------------------- -// Wifi +// Configuration // ----------------------------------------------------------------------------- -char * getIdentifier() { - if (identifier[0] == 0) { - sprintf(identifier, "%s_%06X", MODEL, ESP.getChipId()); +bool saveConfig() { + File file = SPIFFS.open(CONFIG_PATH, "w"); + if (file) { + file.println("ssid0=" + configSSID[0]); + file.println("pass0=" + configPASS[0]); + file.println("ssid1=" + configSSID[1]); + file.println("pass1=" + configPASS[1]); + file.println("ssid2=" + configSSID[2]); + file.println("pass2=" + configPASS[2]); + #if ENABLE_MQTT + file.println("mqttServer=" + mqttServer); + file.println("mqttPort=" + mqttPort); + file.println("mqttTopic=" + mqttTopic); + #endif + #if ENABLE_RF + file.println("rfChannel=" + rfChannel); + file.println("rfDevice=" + rfDevice); + #endif + file.close(); + return true; } - return identifier; + return false; } +bool loadConfig() { + + if (SPIFFS.exists(CONFIG_PATH)) { + + #ifdef DEBUG + Serial.println("Reading config file"); + #endif + + // Read contents + File file = SPIFFS.open(CONFIG_PATH, "r"); + String content = file.readString(); + file.close(); + + // Parse contents + content.replace("\r\n", "\n"); + content.replace("\r", "\n"); + + int start = 0; + int end = content.indexOf("\n", start); + while (end > 0) { + String line = content.substring(start, end); + #ifdef DEBUG + Serial.println(line); + #endif + if (line.startsWith("ssid0=")) configSSID[0] = line.substring(6); + else if (line.startsWith("pass0=")) configPASS[0] = line.substring(6); + else if (line.startsWith("ssid1=")) configSSID[1] = line.substring(6); + else if (line.startsWith("pass1=")) configPASS[1] = line.substring(6); + else if (line.startsWith("ssid2=")) configSSID[2] = line.substring(6); + else if (line.startsWith("pass2=")) configPASS[2] = line.substring(6); + #if ENABLE_MQTT + else if (line.startsWith("mqttServer=")) mqttServer = line.substring(11); + else if (line.startsWith("mqttPort=")) mqttPort = line.substring(9); + else if (line.startsWith("mqttTopic=")) mqttTopic = line.substring(10); + #endif + #if ENABLE_RF + else if (line.startsWith("rfChannel=")) rfChannel = line.substring(10); + else if (line.startsWith("rfDevice=")) rfDevice = line.substring(9); + #endif + if (end < 0) break; + start = end + 1; + end = content.indexOf("\n", start); + } + + return true; + } + return false; + +} + +// ----------------------------------------------------------------------------- +// OTA +// ----------------------------------------------------------------------------- + +#if ENABLE_OTA + + void OTASetup() { + + // Port defaults to 8266 + ArduinoOTA.setPort(8266); + + // Hostname defaults to esp8266-[ChipID] + ArduinoOTA.setHostname(getIdentifier()); + + // No authentication by default + ArduinoOTA.setPassword((const char *) ADMIN_PASS); + + ArduinoOTA.onStart([]() { + #if ENABLE_RF + RemoteReceiver::disable(); + #endif + #ifdef DEBUG + Serial.println("OTA - Start"); + #endif + }); + + ArduinoOTA.onEnd([]() { + #ifdef DEBUG + Serial.println("OTA - End"); + #endif + #if ENABLE_RF + RemoteReceiver::enable(); + #endif + }); + + ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { + #ifdef DEBUG + Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + #endif + }); + + ArduinoOTA.onError([](ota_error_t error) { + #ifdef DEBUG + Serial.printf("Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); + else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); + else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); + else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); + else if (error == OTA_END_ERROR) Serial.println("End Failed"); + #endif + }); + + ArduinoOTA.begin(); + + } + + #if ENABLE_OTA_AUTO + void OTAUpdate() { + + static unsigned long last_check = 0; + + if (WiFi.status() != WL_CONNECTED) return; + if ((last_check > 0) && (millis() - last_check < OTA_CHECK_INTERVAL)) return; + last_check = millis(); + + HTTPClient http; + char url[100]; + sprintf(url, "%s/%s/%s", OTA_SERVER, MODEL, APP_VERSION); + http.begin(url); + int httpCode = http.GET(); + + #ifdef DEBUG + Serial.print("AUTO OTA UPDATE - GET "); + Serial.print(url); + Serial.print(" ["); + Serial.print(httpCode); + Serial.println("]"); + #endif + + if (httpCode > 0) { + + String payload = http.getString(); + StaticJsonBuffer<500> jsonBuffer; + JsonObject& root = jsonBuffer.parseObject(payload); + + if (root.success()) { + + const char* action = root["action"]; + if (strcmp("update", action) == 0) { + + bool error = false; + uint8_t updates = 0; + + #ifdef DEBUG + const char* version = root["target"]["version"]; + Serial.print("Updating with version: "); + Serial.println(version); + #endif + + const char* spiffs = root["target"]["spiffs"]; + if (spiffs[0] != 0) { + + // Update SPIFFS + sprintf(url, "%s/%s", OTA_SERVER, spiffs); + #ifdef DEBUG + Serial.print("Updating file system from "); + Serial.println(url); + #endif + + t_httpUpdate_return ret = ESPhttpUpdate.updateSpiffs(url); + if (ret == HTTP_UPDATE_FAILED) { + error = true; + #ifdef DEBUG + Serial.printf("HTTP_UPDATE_FAILD Error (%d): %s", ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str()); + #endif + } else if (ret == HTTP_UPDATE_OK) { + updates++; + #ifdef DEBUG + Serial.println("HTTP_UPDATE_OK"); + #endif + } else { + #ifdef DEBUG + Serial.printf("HTTP_UPDATE_NO_UPDATES"); + #endif + } + + // Restore config + saveConfig(); + + } else { + + #ifdef DEBUG + Serial.println("No file system binary available"); + #endif + + } + + if (!error) { + + const char* firmware = root["target"]["firmware"]; + if (firmware[0] != 0) { + + // Update binary + sprintf(url, "%s%s", OTA_SERVER, firmware); + #ifdef DEBUG + Serial.print("Updating firmware from "); + Serial.println(url); + #endif + + t_httpUpdate_return ret = ESPhttpUpdate.update(url); + if (ret == HTTP_UPDATE_FAILED) { + #ifdef DEBUG + Serial.printf("HTTP_UPDATE_FAILD Error (%d): %s", ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str()); + #endif + } else if (ret == HTTP_UPDATE_OK) { + updates++; + #ifdef DEBUG + Serial.println("HTTP_UPDATE_OK"); + #endif + } else { + #ifdef DEBUG + Serial.printf("HTTP_UPDATE_NO_UPDATES"); + #endif + } + + if (updates > 0) { + ESP.restart(); + } + + } else { + + #ifdef DEBUG + Serial.println("No firmware binary available"); + #endif + + } + + } + + } else { + + #ifdef DEBUG + Serial.println("Already in the latest version"); + #endif + + } + + } else { + + #ifdef DEBUG + Serial.println("Error parsing JSON"); + #endif + + } + + } + + http.end(); + + } + #endif + + void OTALoop() { + ArduinoOTA.handle(); + #if ENABLE_OTA_AUTO + OTAUpdate(); + #endif + } + +#endif + +// ----------------------------------------------------------------------------- +// Wifi +// ----------------------------------------------------------------------------- + void wifiSetupAP() { // Set WIFI module AP mode @@ -235,6 +550,7 @@ void wifiSetupAP() { // SoftAP mode WiFi.softAP(getIdentifier(), ADMIN_PASS); status = WIFI_STATUS_AP; + delay(100); #ifdef DEBUG Serial.print("[AP Mode] SSID: "); Serial.print(getIdentifier()); @@ -310,6 +626,9 @@ void wifiSetupSTA(bool force) { Serial.print(", IP address: "); Serial.println(WiFi.localIP()); #endif + #if ENABLE_OTA_AUTO + OTAUpdate(); + #endif } else { #ifdef DEBUG Serial.println("[STA Mode] NOT CONNECTED"); @@ -329,6 +648,68 @@ void wifiLoop() { } +// ----------------------------------------------------------------------------- +// RF +// ----------------------------------------------------------------------------- + +#if ENABLE_RF + + void rfLoop() { + if (rfCode == 0) return; + #ifdef DEBUG + Serial.print("RF code: "); + Serial.println(rfCode); + #endif + if (rfCode == rfCodeON) switchRelayOn(); + if (rfCode == rfCodeOFF) switchRelayOff(); + rfCode = 0; + } + + void rfBuildCodes() { + + unsigned long code = 0; + + // channel + unsigned int channel = rfChannel.toInt(); + for (byte i = 0; i < 5; i++) { + code *= 3; + if (channel & 1) code += 1; + channel >>= 1; + } + + // device + unsigned int device = rfDevice.toInt(); + for (byte i = 0; i < 5; i++) { + code *= 3; + if (device != i) code += 2; + } + + // status + code *= 9; + rfCodeOFF = code + 2; + rfCodeON = code + 6; + + #ifdef DEBUG + Serial.print("RF code ON: "); + Serial.println(rfCodeON); + Serial.print("RF code OFF: "); + Serial.println(rfCodeOFF); + #endif + + } + + void rfCallback(unsigned long code, unsigned int period) { + rfCode = code; + } + + void rfSetup() { + rfBuildCodes(); + RemoteReceiver::init(RF_PIN, 3, rfCallback); + RemoteReceiver::enable(); + } + +#endif + // ----------------------------------------------------------------------------- // WebServer // ----------------------------------------------------------------------------- @@ -416,7 +797,7 @@ void wifiLoop() { // Replace placeholders getCompileTime(buffer); - content.replace("{appname}", String(APP_NAME) + "." + String(buffer)); + content.replace("{appname}", String(APP_NAME) + " " + String(APP_VERSION) + " built " + String(buffer)); content.replace("{status}", digitalRead(RELAY_PIN) ? "1" : "0"); content.replace("{updateInterval}", String(STATUS_UPDATE_INTERVAL)); content.replace("{ssid0}", configSSID[0]); @@ -697,208 +1078,6 @@ void wifiLoop() { #endif -// ----------------------------------------------------------------------------- -// RF -// ----------------------------------------------------------------------------- - -#if ENABLE_RF - - void rfLoop() { - if (rfCode == 0) return; - #ifdef DEBUG - Serial.print("RF code: "); - Serial.println(rfCode); - #endif - if (rfCode == rfCodeON) switchRelayOn(); - if (rfCode == rfCodeOFF) switchRelayOff(); - rfCode = 0; - } - - void rfBuildCodes() { - - unsigned long code = 0; - - // channel - unsigned int channel = rfChannel.toInt(); - for (byte i = 0; i < 5; i++) { - code *= 3; - if (channel & 1) code += 1; - channel >>= 1; - } - - // device - unsigned int device = rfDevice.toInt(); - for (byte i = 0; i < 5; i++) { - code *= 3; - if (device != i) code += 2; - } - - // status - code *= 9; - rfCodeOFF = code + 2; - rfCodeON = code + 6; - - #ifdef DEBUG - Serial.print("RF code ON: "); - Serial.println(rfCodeON); - Serial.print("RF code OFF: "); - Serial.println(rfCodeOFF); - #endif - - } - - void rfCallback(unsigned long code, unsigned int period) { - rfCode = code; - } - - void rfSetup() { - rfBuildCodes(); - RemoteReceiver::init(RF_PIN, 3, rfCallback); - RemoteReceiver::enable(); - } - -#endif - -// ----------------------------------------------------------------------------- -// Configuration -// ----------------------------------------------------------------------------- - -bool saveConfig() { - File file = SPIFFS.open(CONFIG_PATH, "w"); - if (file) { - file.println("ssid0=" + configSSID[0]); - file.println("pass0=" + configPASS[0]); - file.println("ssid1=" + configSSID[1]); - file.println("pass1=" + configPASS[1]); - file.println("ssid2=" + configSSID[2]); - file.println("pass2=" + configPASS[2]); - #if ENABLE_MQTT - file.println("mqttServer=" + mqttServer); - file.println("mqttPort=" + mqttPort); - file.println("mqttTopic=" + mqttTopic); - #endif - #if ENABLE_RF - file.println("rfChannel=" + rfChannel); - file.println("rfDevice=" + rfDevice); - #endif - file.close(); - return true; - } - return false; -} - -bool loadConfig() { - - if (SPIFFS.exists(CONFIG_PATH)) { - - #ifdef DEBUG - Serial.println("Reading config file"); - #endif - - // Read contents - File file = SPIFFS.open(CONFIG_PATH, "r"); - String content = file.readString(); - file.close(); - - // Parse contents - content.replace("\r\n", "\n"); - content.replace("\r", "\n"); - - int start = 0; - int end = content.indexOf("\n", start); - while (end > 0) { - String line = content.substring(start, end); - #ifdef DEBUG - Serial.println(line); - #endif - if (line.startsWith("ssid0=")) configSSID[0] = line.substring(6); - else if (line.startsWith("pass0=")) configPASS[0] = line.substring(6); - else if (line.startsWith("ssid1=")) configSSID[1] = line.substring(6); - else if (line.startsWith("pass1=")) configPASS[1] = line.substring(6); - else if (line.startsWith("ssid2=")) configSSID[2] = line.substring(6); - else if (line.startsWith("pass2=")) configPASS[2] = line.substring(6); - #if ENABLE_MQTT - else if (line.startsWith("mqttServer=")) mqttServer = line.substring(11); - else if (line.startsWith("mqttPort=")) mqttPort = line.substring(9); - else if (line.startsWith("mqttTopic=")) mqttTopic = line.substring(10); - #endif - #if ENABLE_RF - else if (line.startsWith("rfChannel=")) rfChannel = line.substring(10); - else if (line.startsWith("rfDevice=")) rfDevice = line.substring(9); - #endif - if (end < 0) break; - start = end + 1; - end = content.indexOf("\n", start); - } - - return true; - } - return false; - -} - -// ----------------------------------------------------------------------------- -// OTA -// ----------------------------------------------------------------------------- - -#if ENABLE_OTA - - void OTASetup() { - - // Port defaults to 8266 - ArduinoOTA.setPort(8266); - - // Hostname defaults to esp8266-[ChipID] - ArduinoOTA.setHostname(getIdentifier()); - - // No authentication by default - ArduinoOTA.setPassword((const char *) ADMIN_PASS); - - ArduinoOTA.onStart([]() { - #if ENABLE_RF - RemoteReceiver::disable(); - #endif - #ifdef DEBUG - Serial.println("OTA - Start"); - #endif - }); - - ArduinoOTA.onEnd([]() { - #ifdef DEBUG - Serial.println("OTA - End"); - #endif - #if ENABLE_RF - RemoteReceiver::enable(); - #endif - }); - - ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { - #ifdef DEBUG - Serial.printf("Progress: %u%%\r", (progress / (total / 100))); - #endif - }); - - ArduinoOTA.onError([](ota_error_t error) { - #ifdef DEBUG - Serial.printf("Error[%u]: ", error); - if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); - else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); - else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); - else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); - else if (error == OTA_END_ERROR) Serial.println("End Failed"); - #endif - }); - - ArduinoOTA.begin(); - - } - - void OTALoop() { - ArduinoOTA.handle(); - } - -#endif - // ----------------------------------------------------------------------------- // Energy Monitor // ----------------------------------------------------------------------------- @@ -964,24 +1143,6 @@ void hardwareSetup() { EEPROM.read(0) == 1 ? switchRelayOn() : switchRelayOff(); } -void blink(unsigned long delayOff, unsigned long delayOn) { - static unsigned long next = millis(); - static bool status = HIGH; - if (next < millis()) { - status = !status; - digitalWrite(LED_PIN, status); - next += ((status) ? delayOff : delayOn); - } -} - -void showStatus() { - if (WiFi.status() == WL_CONNECTED) { - blink(5000, 500); - } else { - blink(500, 500); - } -} - void hardwareLoop() { if (button1.loop()) { if (button1.getEvent() == EVENT_SINGLE_CLICK) toggleRelay(); @@ -996,10 +1157,16 @@ void hardwareLoop() { // ----------------------------------------------------------------------------- void welcome() { + char buffer[BUFFER_SIZE]; + getCompileTime(buffer); Serial.println(); - Serial.println(APP_NAME); - Serial.println(APP_WEBSITE); + Serial.print(APP_NAME); + Serial.print(" "); + Serial.print(APP_VERSION); + Serial.print(" built "); + Serial.println(buffer); Serial.println(APP_AUTHOR); + Serial.println(APP_WEBSITE); Serial.println(); Serial.print("Device: "); Serial.println(getIdentifier()); @@ -1011,6 +1178,7 @@ void welcome() { void setup() { hardwareSetup(); + delay(1000); welcome(); #if ENABLE_OTA diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 00000000..d66a419a --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,4 @@ +/cache/ +/vendor/ +/logs/* +!/logs/README.md diff --git a/server/README.md b/server/README.md new file mode 100644 index 00000000..438f9acc --- /dev/null +++ b/server/README.md @@ -0,0 +1,8 @@ +# ESPurna Update Server + +First version of an ESPurna update server, an API that responfs to ESPurna devices with information about the last available firmware. + +## Use + +1. Modify ```data/versions.js``` with info about available firmware versions depending on current model (device type) and firmware version. +1. Perform GET queries against http:////, for instance: ```http://192.168.1.105/espurna/0.9.1``` diff --git a/server/composer.json b/server/composer.json new file mode 100644 index 00000000..29310bdd --- /dev/null +++ b/server/composer.json @@ -0,0 +1,21 @@ +{ + "name": "tinkerman/espurna-update-server", + "description": "Update server that listen to ESPurna devices queries and returns last available firmware", + "keywords": ["esp8266", "ota", "espurna", "firmware"], + "homepage": "http://tinkerman.cat", + "license": "GPLv3", + "authors": [ + { + "name": "Xose PĆ©rez", + "email": "xose.perez@gmail.com", + "homepage": "http://tinkerman.cat/" + } + ], + "require": { + "php": ">=5.5.0", + "slim/slim": "^3.1", + "slim/php-view": "^2.0", + "monolog/monolog": "^1.17", + "slim/twig-view": "^2.1" + } +} diff --git a/server/composer.lock b/server/composer.lock new file mode 100644 index 00000000..4e2704de --- /dev/null +++ b/server/composer.lock @@ -0,0 +1,532 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "7cd735ef5dd6b16144dfd2a940224676", + "content-hash": "df0b70ace71b7e873d1119b4788805db", + "packages": [ + { + "name": "container-interop/container-interop", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/container-interop/container-interop.git", + "reference": "fc08354828f8fd3245f77a66b9e23a6bca48297e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/container-interop/container-interop/zipball/fc08354828f8fd3245f77a66b9e23a6bca48297e", + "reference": "fc08354828f8fd3245f77a66b9e23a6bca48297e", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Interop\\Container\\": "src/Interop/Container/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", + "time": "2014-12-30 15:22:37" + }, + { + "name": "monolog/monolog", + "version": "1.20.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "55841909e2bcde01b5318c35f2b74f8ecc86e037" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/55841909e2bcde01b5318c35f2b74f8ecc86e037", + "reference": "55841909e2bcde01b5318c35f2b74f8ecc86e037", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/log": "~1.0" + }, + "provide": { + "psr/log-implementation": "1.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9", + "doctrine/couchdb": "~1.0@dev", + "graylog2/gelf-php": "~1.0", + "jakub-onderka/php-parallel-lint": "0.9", + "php-amqplib/php-amqplib": "~2.4", + "php-console/php-console": "^3.1.3", + "phpunit/phpunit": "~4.5", + "phpunit/phpunit-mock-objects": "2.3.0", + "ruflin/elastica": ">=0.90 <3.0", + "sentry/sentry": "^0.13", + "swiftmailer/swiftmailer": "~5.3" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-mongo": "Allow sending log messages to a MongoDB server", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "php-console/php-console": "Allow sending log messages to Google Chrome", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server", + "sentry/sentry": "Allow sending log messages to a Sentry server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "http://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "time": "2016-07-02 14:02:10" + }, + { + "name": "nikic/fast-route", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/nikic/FastRoute.git", + "reference": "8ea928195fa9b907f0d6e48312d323c1a13cc2af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/FastRoute/zipball/8ea928195fa9b907f0d6e48312d323c1a13cc2af", + "reference": "8ea928195fa9b907f0d6e48312d323c1a13cc2af", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "FastRoute\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "description": "Fast request router for PHP", + "keywords": [ + "router", + "routing" + ], + "time": "2016-06-12 19:08:51" + }, + { + "name": "pimple/pimple", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/silexphp/Pimple.git", + "reference": "a30f7d6e57565a2e1a316e1baf2a483f788b258a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/silexphp/Pimple/zipball/a30f7d6e57565a2e1a316e1baf2a483f788b258a", + "reference": "a30f7d6e57565a2e1a316e1baf2a483f788b258a", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Pimple": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Pimple, a simple Dependency Injection Container", + "homepage": "http://pimple.sensiolabs.org", + "keywords": [ + "container", + "dependency injection" + ], + "time": "2015-09-11 15:10:35" + }, + { + "name": "psr/http-message", + "version": "1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/85d63699f0dbedb190bbd4b0d2b9dc707ea4c298", + "reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2015-05-04 20:22:00" + }, + { + "name": "psr/log", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe0936ee26643249e916849d48e3a51d5f5e278b", + "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-0": { + "Psr\\Log\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2012-12-21 11:40:51" + }, + { + "name": "slim/php-view", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/slimphp/PHP-View.git", + "reference": "8bae5b10d10c51596ef8d8113b3b63678718adcb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/PHP-View/zipball/8bae5b10d10c51596ef8d8113b3b63678718adcb", + "reference": "8bae5b10d10c51596ef8d8113b3b63678718adcb", + "shasum": "" + }, + "require": { + "psr/http-message": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.0", + "slim/slim": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\Views\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Glenn Eggleton", + "email": "geggleto@gmail.com" + } + ], + "description": "Render PHP view scripts into a PSR-7 Response object.", + "keywords": [ + "framework", + "php", + "phtml", + "renderer", + "slim", + "template", + "view" + ], + "time": "2016-03-04 09:48:50" + }, + { + "name": "slim/slim", + "version": "3.4.2", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Slim.git", + "reference": "a132385f736063d00632b52b3f8a389fe66fe4fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Slim/zipball/a132385f736063d00632b52b3f8a389fe66fe4fa", + "reference": "a132385f736063d00632b52b3f8a389fe66fe4fa", + "shasum": "" + }, + "require": { + "container-interop/container-interop": "^1.1", + "nikic/fast-route": "^1.0", + "php": ">=5.5.0", + "pimple/pimple": "^3.0", + "psr/http-message": "^1.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0", + "squizlabs/php_codesniffer": "^2.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\": "Slim" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rob Allen", + "email": "rob@akrabat.com", + "homepage": "http://akrabat.com" + }, + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "https://joshlockhart.com" + }, + { + "name": "Gabriel Manricks", + "email": "gmanricks@me.com", + "homepage": "http://gabrielmanricks.com" + }, + { + "name": "Andrew Smith", + "email": "a.smith@silentworks.co.uk", + "homepage": "http://silentworks.co.uk" + } + ], + "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs", + "homepage": "http://slimframework.com", + "keywords": [ + "api", + "framework", + "micro", + "router" + ], + "time": "2016-05-25 11:23:38" + }, + { + "name": "slim/twig-view", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Twig-View.git", + "reference": "16fded26a44b8e8e0e041f1cff32afa21daeb284" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Twig-View/zipball/16fded26a44b8e8e0e041f1cff32afa21daeb284", + "reference": "16fded26a44b8e8e0e041f1cff32afa21daeb284", + "shasum": "" + }, + "require": { + "php": ">=5.5.0", + "psr/http-message": "^1.0", + "twig/twig": "^1.18" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\Views\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "http://joshlockhart.com" + } + ], + "description": "Slim Framework 3 view helper built on top of the Twig templating component", + "homepage": "http://slimframework.com", + "keywords": [ + "framework", + "slim", + "template", + "twig", + "view" + ], + "time": "2016-03-13 20:58:41" + }, + { + "name": "twig/twig", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "3566d311a92aae4deec6e48682dc5a4528c4a512" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/3566d311a92aae4deec6e48682dc5a4528c4a512", + "reference": "3566d311a92aae4deec6e48682dc5a4528c4a512", + "shasum": "" + }, + "require": { + "php": ">=5.2.7" + }, + "require-dev": { + "symfony/debug": "~2.7", + "symfony/phpunit-bridge": "~2.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.24-dev" + } + }, + "autoload": { + "psr-0": { + "Twig_": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + }, + { + "name": "Twig Team", + "homepage": "http://twig.sensiolabs.org/contributors", + "role": "Contributors" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "http://twig.sensiolabs.org", + "keywords": [ + "templating" + ], + "time": "2016-05-30 09:11:59" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.5.0" + }, + "platform-dev": [] +} diff --git a/server/data/versions.json b/server/data/versions.json new file mode 100644 index 00000000..3806122c --- /dev/null +++ b/server/data/versions.json @@ -0,0 +1,14 @@ +[ + { + "model": "SONOFF", + "firmware": { + "min": "*", + "max": "0.9.2" + }, + "target": { + "version": "0.9.2", + "firmware": "/firmware/espurna-0.9.2.bin", + "spiffs": "/firmware/espurna-0.9.1-spiffs.bin" + } + } +] diff --git a/server/public/.htaccess b/server/public/.htaccess new file mode 100644 index 00000000..c506092e --- /dev/null +++ b/server/public/.htaccess @@ -0,0 +1,10 @@ +RewriteEngine On + +# Some hosts may require you to use the `RewriteBase` directive. +# If you need to use the `RewriteBase` directive, it should be the +# absolute physical path to the directory that contains this htaccess file. +# +# RewriteBase / + +RewriteCond %{REQUEST_FILENAME} !-f +RewriteRule ^ index.php [QSA,L] diff --git a/server/public/index.php b/server/public/index.php new file mode 100644 index 00000000..2784ee00 --- /dev/null +++ b/server/public/index.php @@ -0,0 +1,30 @@ +run(); diff --git a/server/src/dependencies.php b/server/src/dependencies.php new file mode 100644 index 00000000..f188a741 --- /dev/null +++ b/server/src/dependencies.php @@ -0,0 +1,36 @@ +getContainer(); + +// view renderer +// Register component on container +$container['view'] = function ($container) { + $settings = $container->get('settings')['renderer']; + $view = new \Slim\Views\Twig($settings['template_path'], [ + 'cache' => $settings['cache_path'] + ]); + $view->addExtension(new \Slim\Views\TwigExtension( + $container['router'], + $container['request']->getUri() + )); + + return $view; +}; + +// monolog +$container['logger'] = function ($container) { + $settings = $container->get('settings')['logger']; + $logger = new Monolog\Logger($settings['name']); + $logger->pushProcessor(new Monolog\Processor\UidProcessor()); + $logger->pushHandler(new Monolog\Handler\StreamHandler($settings['path'], Monolog\Logger::DEBUG)); + return $logger; +}; + +// version database +$container['data'] = function($container) { + $settings = $container->get('settings')['database']; + $json_data = file_get_contents($settings['path']); + $data = json_decode($json_data, true); + return $data; +}; diff --git a/server/src/middleware.php b/server/src/middleware.php new file mode 100644 index 00000000..116a0ffe --- /dev/null +++ b/server/src/middleware.php @@ -0,0 +1,4 @@ +add(new \Slim\Csrf\Guard); diff --git a/server/src/routes.php b/server/src/routes.php new file mode 100644 index 00000000..40368edc --- /dev/null +++ b/server/src/routes.php @@ -0,0 +1,36 @@ +get('/{model}/{current}', function($request, $response, $args) { + + $found = false; + $model = $request->getAttribute('model'); + $current = $request->getAttribute('current'); + + foreach ($this->get('data') as $version) { + + if (($model == $version['model']) + && (($version['firmware']['min'] == "*" || version_compare($version['firmware']['min'], $current, "<="))) + && (($version['firmware']['max'] == "*" || version_compare($version['firmware']['max'], $current, ">")))) { + + $response->getBody()->write(stripslashes(json_encode(array( + 'action' => 'update', + 'target' => $version["target"] + )))); + + $found = true; + break; + + } + }; + + if (!$found) { + $response->getBody()->write(stripslashes(json_encode(array( + 'action' => 'none', + )))); + } + + return $response; + +}); diff --git a/server/src/settings.php b/server/src/settings.php new file mode 100644 index 00000000..4751d57f --- /dev/null +++ b/server/src/settings.php @@ -0,0 +1,26 @@ + [ + 'displayErrorDetails' => true, // set to false in production + 'addContentLengthHeader' => false, // Allow the web server to send the content-length header + + // Renderer settings + 'renderer' => [ + 'template_path' => __DIR__ . '/../templates/', + 'cache_path' => __DIR__ . '/../cache/', + ], + + // Monolog settings + 'logger' => [ + 'name' => 'espurna-update-server', + 'path' => __DIR__ . '/../logs/app.log', + ], + + // Database + 'database' => [ + 'type' => 'json', + 'path' => __DIR__ . '/../data/versions.json', + ], + + ], +]; diff --git a/server/templates/index.phtml b/server/templates/index.phtml new file mode 100644 index 00000000..59e4dfe9 --- /dev/null +++ b/server/templates/index.phtml @@ -0,0 +1,57 @@ + + + + + ESPurna Update Server + + + + +

ESPurna Update Server

+ + + + + + + + + + + + + + {% for version in versions %} + + + + + + + + + {% endfor %} + +
DeviceMinimum Hardware VersionMaximum Hardware VersionMinimum Firmware VersionMaximum Firmware VersionLatest Firmware Version
{{ version.model }}{{ version.hardware_min }}{{ version.hardware_max }}{{ version.firmware_min }}{{ version.firmware_max }}{{ version.firmware_version }}
+ + +