From 507248a4ca87a67397870cb909ebcf1aa3d62620 Mon Sep 17 00:00:00 2001 From: Hieromon Ikasamo Date: Sun, 5 May 2019 09:21:13 +0900 Subject: [PATCH] Supports AutoConnectUpdate --- examples/Update/Update.ino | 49 ++++ library.json | 2 +- library.properties | 2 +- src/AutoConnect.cpp | 33 ++- src/AutoConnect.h | 11 +- src/AutoConnectDefs.h | 35 ++- src/AutoConnectElementJson.h | 46 +--- src/AutoConnectJsonDefs.h | 59 +++++ src/AutoConnectLabels.h | 5 + src/AutoConnectUpdate.cpp | 403 +++++++++++++++++++++++++++++++ src/AutoConnectUpdate.h | 139 +++++++++++ src/AutoConnectUpdatePage.h | 46 ++++ src/updateserver/updateserver.py | 168 +++++++++++++ 13 files changed, 941 insertions(+), 57 deletions(-) create mode 100644 examples/Update/Update.ino create mode 100644 src/AutoConnectJsonDefs.h create mode 100644 src/AutoConnectUpdate.cpp create mode 100644 src/AutoConnectUpdate.h create mode 100644 src/AutoConnectUpdatePage.h create mode 100644 src/updateserver/updateserver.py diff --git a/examples/Update/Update.ino b/examples/Update/Update.ino new file mode 100644 index 0000000..bf579a8 --- /dev/null +++ b/examples/Update/Update.ino @@ -0,0 +1,49 @@ +/* + Update.ino, Example for the AutoConnect library. + Copyright (c) 2019, Hieromon Ikasamo + https://github.com/Hieromon/AutoConnect + This software is released under the MIT License. + https://opensource.org/licenses/MIT + + This example presents the simplest OTA Updates scheme. +*/ + +#if defined(ARDUINO_ARCH_ESP8266) +#include +#include +using WebServerClass = ESP8266WebServer; +#elif defined(ARDUINO_ARCH_ESP32) +#include +#include +using WebServerClass = WebServer; +#endif +#include + +#define UPDATESERVER_URL "192.168.1.9" +#define UPDATESERVER_PORT 8000 +#define UPDATESERVER_PATH "bin" + +WebServerClass server; +AutoConnect portal(server); +AutoConnectUpdate update(UPDATESERVER_URL, UPDATESERVER_PORT, UPDATESERVER_PATH); +bool atFirst; + +void setup() { + delay(1000); + Serial.begin(115200); + Serial.println(); + + // Responder of root page handled directly from WebServer class. + server.on("/", []() { + String content = "Place the root page with the sketch application. "; + content += AUTOCONNECT_LINK(COG_24); + server.send(200, "text/html", content); + }); + + portal.begin(); + update.attach(portal); +} + +void loop() { + portal.handleClient(); +} diff --git a/library.json b/library.json index a40071f..640fa15 100644 --- a/library.json +++ b/library.json @@ -25,6 +25,6 @@ "espressif8266", "espressif32" ], - "version": "0.9.8", + "version": "0.9.9", "license": "MIT" } diff --git a/library.properties b/library.properties index 168d52a..ce6e312 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=AutoConnect -version=0.9.8 +version=0.9.9 author=Hieromon Ikasamo maintainer=Hieromon Ikasamo sentence=ESP8266/ESP32 WLAN configuration at runtime with web interface. diff --git a/src/AutoConnect.cpp b/src/AutoConnect.cpp index 5cafa1e..0e4c11a 100644 --- a/src/AutoConnect.cpp +++ b/src/AutoConnect.cpp @@ -2,8 +2,8 @@ * AutoConnect class implementation. * @file AutoConnect.cpp * @author hieromon@gmail.com - * @version 0.9.7 - * @date 2019-01-21 + * @version 0.9.9 + * @date 2019-05-04 * @copyright MIT license. */ @@ -59,6 +59,11 @@ void AutoConnect::_initialize(void) { #endif _aux.release(); _auxUri = String(""); + + // Prepare to attach the updater. + // If an updater is attached, handleClient includes an update process + // in the handleClient behavior. + _update.reset(nullptr); } /** @@ -154,9 +159,14 @@ bool AutoConnect::begin(const char* ssid, const char* passphrase, unsigned long } _currentHostIP = WiFi.localIP(); - // Rushing into the portal. - if (!cs) { - + // End first begin process, the captive portal specific process starts here. + if (cs) { + // Activate AutoConnectUpdate if it is attached and incorporate it into the AutoConnect menu. + if (_update) + _update->enable(); + } + // Rushing into the portal. + else { // The captive portal is effective at the autoRise is valid only. if (_apConfig.autoRise) { @@ -324,6 +334,9 @@ void AutoConnect::end(void) { break; } } + + // Release the updater + _update.reset(nullptr); } /** @@ -463,6 +476,11 @@ void AutoConnect::handleRequest(void) { } else AC_DBG("%s has no BSSID, saving is unavailable\n", reinterpret_cast(_credential.ssid)); + + // Activate AutoConnectUpdate if it is attached and incorporate + // it into the AutoConnect menu. + if (_update) + _update->enable(); } else { _currentHostIP = WiFi.softAPIP(); @@ -488,7 +506,6 @@ void AutoConnect::handleRequest(void) { if (_rfDisconnect) { // Disconnect from the current AP. -// _waitForEndTransmission(); _stopPortal(); _disconnectWiFi(false); while (WiFi.status() == WL_CONNECTED) { @@ -505,6 +522,10 @@ void AutoConnect::handleRequest(void) { delay(1000); } } + + // Handle the update behaviors for attached AutoConnectUpdate. + if (_update) + _update->handleUpdate(); } /** diff --git a/src/AutoConnect.h b/src/AutoConnect.h index b1e98b4..72b2c93 100644 --- a/src/AutoConnect.h +++ b/src/AutoConnect.h @@ -2,8 +2,8 @@ * Declaration of AutoConnect class and accompanying AutoConnectConfig class. * @file AutoConnect.h * @author hieromon@gmail.com - * @version 0.9.7 - * @date 2019-01-21 + * @version 0.9.9 + * @date 2019-05-04 * @copyright MIT license. */ @@ -32,6 +32,9 @@ using WebServerClass = WebServer; #include "AutoConnectPage.h" #include "AutoConnectCredential.h" #include "AutoConnectAux.h" +#include "AutoConnectUpdate.h" + +class AutoConnectUpdate; // Reference to avoid circular /**< A type to save established credential at WiFi.begin automatically. */ typedef enum AC_SAVECREDENTIAL { @@ -267,6 +270,9 @@ class AutoConnect { String _auxUri; /**< Last accessed AutoConnectAux */ String _prevUri; /**< Previous generated page uri */ + /** Available updater, only reset by AutoConnectUpdate::attach is valid */ + std::unique_ptr _update; + /** Saved configurations */ AutoConnectConfig _apConfig; struct station_config _credential; @@ -367,6 +373,7 @@ class AutoConnect { #endif friend class AutoConnectAux; + friend class AutoConnectUpdate; }; #endif // _AUTOCONNECT_H_ diff --git a/src/AutoConnectDefs.h b/src/AutoConnectDefs.h index dc35053..6416caf 100644 --- a/src/AutoConnectDefs.h +++ b/src/AutoConnectDefs.h @@ -2,8 +2,8 @@ * Predefined AutoConnect configuration parameters. * @file AutoConnectDefs.h * @author hieromon@gmail.com - * @version 0.9.7 - * @date 2018-11-17 + * @version 0.9.9 + * @date 2019-04-30 * @copyright MIT license. */ @@ -29,6 +29,9 @@ // Comment out the AUTOCONNECT_USE_JSON macro to detach the ArduinoJson. #define AUTOCONNECT_USE_JSON +// Indicator of whether to use the AutoConnectUpdate feature. +#define AUTOCONNECT_USE_UPDATE + // Predefined parameters // SSID that Captive portal started. #ifndef AUTOCONNECT_APID @@ -86,6 +89,9 @@ #define AUTOCONNECT_URI_RESET AUTOCONNECT_URI "/reset" #define AUTOCONNECT_URI_SUCCESS AUTOCONNECT_URI "/success" #define AUTOCONNECT_URI_FAIL AUTOCONNECT_URI "/fail" +#define AUTOCONNECT_URI_UPDATE AUTOCONNECT_URI "/update" +#define AUTOCONNECT_URI_UPDATE_ACT AUTOCONNECT_URI "/update_act" +#define AUTOCONNECT_URI_UPDATE_RESULT AUTOCONNECT_URI "/update_result" // Time-out limitation when AutoConnect::begin [ms] #ifndef AUTOCONNECT_TIMEOUT @@ -161,6 +167,31 @@ #define AUTOCONNECT_JSONPSRAM_SIZE (16* 1024) #endif // !AUTOCONNECT_JSONPSRAM_SIZE +// Available HTTP port number for the update +#ifndef AUTOCONNECT_UPDATE_PORT +#define AUTOCONNECT_UPDATE_PORT 8000 +#endif // !AUTOCONNECT_UPDATE_PORT + +// HTTP client timeout limitation for the update +#ifndef AUTOCONNECT_UPDATE_TIMEOUT +#define AUTOCONNECT_UPDATE_TIMEOUT 8000 +#endif // !AUTOCONNECT_UPDATE_TIMEOUT + +#ifndef AUTOCONNECT_UPDATE_DURATION +#define AUTOCONNECT_UPDATE_DURATION 180000 +#endif // !AUTOCONNECT_UPDATE_DURATION + +// URIs of the behaviors owned by the update server +#ifndef AUTOCONNECT_UPDATE_CATALOG +#define AUTOCONNECT_UPDATE_CATALOG "/_catalog" +#endif // !AUTOCONNECT_UPDATE_CATALOG +#ifndef AUTOCONNECT_UPDATE_DOWNLOAD +#define AUTOCONNECT_UPDATE_DOWNLOAD "/" +#endif // !AUTOCONNECT_UPDATE_DOWNLOAD +#ifndef AUTOCONNECT_UPDATE_CATALOG_JSONBUFFER_SIZE +#define AUTOCONNECT_UPDATE_CATALOG_JSONBUFFER_SIZE 2048 +#endif // !AUTOCONNECT_UPDATE_CATALOG_JSONBUFFER_SIZE + // Explicitly avoiding unused warning with token handler of PageBuilder #define AC_UNUSED(expr) do { (void)(expr); } while (0) diff --git a/src/AutoConnectElementJson.h b/src/AutoConnectElementJson.h index ed42401..1bbf936 100644 --- a/src/AutoConnectElementJson.h +++ b/src/AutoConnectElementJson.h @@ -11,7 +11,7 @@ #define _AUTOCONNECTELEMENTJSON_H_ #include "AutoConnectElementBasis.h" -#include +#include "AutoConnectJsonDefs.h" #define AUTOCONNECT_JSON_KEY_ACTION "action" #define AUTOCONNECT_JSON_KEY_ARRANGE "arrange" @@ -46,50 +46,6 @@ #define AUTOCONNECT_JSON_VALUE_SD "sd" #define AUTOCONNECT_JSON_VALUE_VERTICAL "vertical" -/** - * Make the Json types and functions consistent with the ArduinoJson - * version. These declarations share the following type definitions: - * - Difference between reference and proxy of JsonObject and JsonArray. - * - Difference of check whether the parsing succeeded or not. - * - The print function name difference. - * - The buffer class difference. - * - When PSRAM present, enables the buffer allocation it with ESP32 and - * supported version. - */ -#if ARDUINOJSON_VERSION_MAJOR<=5 -#define ARDUINOJSON_CREATEOBJECT(doc) doc.createObject() -#define ARDUINOJSON_CREATEARRAY(doc) doc.createArray() -#define ARDUINOJSON_PRETTYPRINT(doc, out) ({ size_t s = doc.prettyPrintTo(out); s; }) -#define ARDUINOJSON_PRINT(doc, out) ({ size_t s = doc.printTo(out); s; }) -using ArduinoJsonObject = JsonObject&; -using ArduinoJsonArray = JsonArray&; -using ArduinoJsonBuffer = DynamicJsonBuffer; -#define AUTOCONNECT_JSONBUFFER_PRIMITIVE_SIZE AUTOCONNECT_JSONBUFFER_SIZE -#else -#define ARDUINOJSON_CREATEOBJECT(doc) doc.to() -#define ARDUINOJSON_CREATEARRAY(doc) doc.to() -#define ARDUINOJSON_PRETTYPRINT(doc, out) ({ size_t s = serializeJsonPretty(doc, out); s; }) -#define ARDUINOJSON_PRINT(doc, out) ({ size_t s = serializeJson(doc, out); s; }) -using ArduinoJsonObject = JsonObject; -using ArduinoJsonArray = JsonArray; -#if defined(BOARD_HAS_PSRAM) && ((ARDUINOJSON_VERSION_MAJOR==6 && ARDUINOJSON_VERSION_MINOR>=10) || ARDUINOJSON_VERSION_MAJOR>6) -// JsonDocument is assigned to PSRAM by ArduinoJson's custom allocator. -struct SpiRamAllocatorST { - void* allocate(size_t size) { - return heap_caps_malloc(size, MALLOC_CAP_SPIRAM); - } - void deallocate(void* pointer) { - heap_caps_free(pointer); - } -}; -#define AUTOCONNECT_JSONBUFFER_PRIMITIVE_SIZE AUTOCONNECT_JSONPSRAM_SIZE -using ArduinoJsonBuffer = BasicJsonDocument; -#else -#define AUTOCONNECT_JSONBUFFER_PRIMITIVE_SIZE AUTOCONNECT_JSONDOCUMENT_SIZE -using ArduinoJsonBuffer = DynamicJsonDocument; -#endif -#endif - /** * AutoConnectAux element base with handling with JSON object. * Placed a raw text that can be added by user sketch. diff --git a/src/AutoConnectJsonDefs.h b/src/AutoConnectJsonDefs.h new file mode 100644 index 0000000..a596df2 --- /dev/null +++ b/src/AutoConnectJsonDefs.h @@ -0,0 +1,59 @@ +/** + * Wrapping definition to ensure version compatibility of ArduinoJson. + * @file AutoConnectJsonDefs.h + * @author hieromon@gmail.com + * @version 0.9.9 + * @date 2019-04-25 + * @copyright MIT license. + */ + +#ifndef _AUTOCONNECTJSONDEFS_H_ +#define _AUTOCONNECTJSONDEFS_H_ + +#include + +/** + * Make the Json types and functions consistent with the ArduinoJson + * version. These declarations share the following type definitions: + * - Difference between reference and proxy of JsonObject and JsonArray. + * - Difference of check whether the parsing succeeded or not. + * - The print function name difference. + * - The buffer class difference. + * - When PSRAM present, enables the buffer allocation it with ESP32 and + * supported version. + */ +#if ARDUINOJSON_VERSION_MAJOR<=5 +#define ARDUINOJSON_CREATEOBJECT(doc) doc.createObject() +#define ARDUINOJSON_CREATEARRAY(doc) doc.createArray() +#define ARDUINOJSON_PRETTYPRINT(doc, out) ({ size_t s = doc.prettyPrintTo(out); s; }) +#define ARDUINOJSON_PRINT(doc, out) ({ size_t s = doc.printTo(out); s; }) +using ArduinoJsonObject = JsonObject&; +using ArduinoJsonArray = JsonArray&; +using ArduinoJsonBuffer = DynamicJsonBuffer; +#define AUTOCONNECT_JSONBUFFER_PRIMITIVE_SIZE AUTOCONNECT_JSONBUFFER_SIZE +#else +#define ARDUINOJSON_CREATEOBJECT(doc) doc.to() +#define ARDUINOJSON_CREATEARRAY(doc) doc.to() +#define ARDUINOJSON_PRETTYPRINT(doc, out) ({ size_t s = serializeJsonPretty(doc, out); s; }) +#define ARDUINOJSON_PRINT(doc, out) ({ size_t s = serializeJson(doc, out); s; }) +using ArduinoJsonObject = JsonObject; +using ArduinoJsonArray = JsonArray; +#if defined(BOARD_HAS_PSRAM) && ((ARDUINOJSON_VERSION_MAJOR==6 && ARDUINOJSON_VERSION_MINOR>=10) || ARDUINOJSON_VERSION_MAJOR>6) +// JsonDocument is assigned to PSRAM by ArduinoJson's custom allocator. +struct SpiRamAllocatorST { + void* allocate(size_t size) { + return heap_caps_malloc(size, MALLOC_CAP_SPIRAM); + } + void deallocate(void* pointer) { + heap_caps_free(pointer); + } +}; +#define AUTOCONNECT_JSONBUFFER_PRIMITIVE_SIZE AUTOCONNECT_JSONPSRAM_SIZE +using ArduinoJsonBuffer = BasicJsonDocument; +#else +#define AUTOCONNECT_JSONBUFFER_PRIMITIVE_SIZE AUTOCONNECT_JSONDOCUMENT_SIZE +using ArduinoJsonBuffer = DynamicJsonDocument; +#endif +#endif + +#endif // _AUTOCONNECTJSONDEFS_H_ diff --git a/src/AutoConnectLabels.h b/src/AutoConnectLabels.h index 1591ed4..278262f 100644 --- a/src/AutoConnectLabels.h +++ b/src/AutoConnectLabels.h @@ -56,6 +56,11 @@ //#define AUTOCONNECT_MENULABEL_HOME "Main" #endif // !AUTOCONNECT_MENULABEL_HOME +// Menu item: Update +#ifndef AUTOCONNECT_MENULABEL_UPDATE +#define AUTOCONNECT_MENULABEL_UPDATE "Update" +#endif // !AUTOCONNECT_MENULABEL_UPDATE + // Button label: RESET #ifndef AUTOCONNECT_BUTTONLABEL_RESET #define AUTOCONNECT_BUTTONLABEL_RESET "RESET" diff --git a/src/AutoConnectUpdate.cpp b/src/AutoConnectUpdate.cpp new file mode 100644 index 0000000..914d48b --- /dev/null +++ b/src/AutoConnectUpdate.cpp @@ -0,0 +1,403 @@ +/** + * AutoConnectUpdate class implementation. + * @file AutoConnectUpdate.cpp + * @author hieromon@gmail.com + * @version 0.9.9 + * @date 2019-05-03 + * @copyright MIT license. + */ + +#include "AutoConnectUpdate.h" +#include "AutoConnectUpdatePage.h" +#include "AutoConnectJsonDefs.h" + +/** + * The AutoConnectUpdate class inherits from the HTTPupdate class. The + * update server corresponding to this class needs a simple script + * based on an HTTP server. It is somewhat different from the advanced + * updater script offered by Arduino core of ESP8266. + * The equipment required for the update server for the + * AutoConnectUpdate class is as follows: + * + * The catalog script: + * The update server URI /catalog is a script process that responds to + * queries from the AutoConnectUpdate class. The catalog script accepts + * the queries such as '/catalog?op=list&path='. + * - op: + * An op parameter speicies the query operation. In the current + * version, available query operation is a list only. + * - list: + * The query operation list responds with a list of available sketch + * binaries. The response of list is a directory list of file paths + * on the server specified by the path parameter and describes as a + * JSON document. Its JSON document is an array of JSON objects with + * the name and the type keys. For example, it is the following + * description: + * [ + * { + * "name": "somefoler", + * "type": "directory" + * }, + * { + * "name": "update.ino", + * "type": "file" + * }, + * { + * "name": "update.bin", + * "type": "bin" + * } + * ] + * - name: + * Name of file entry on the path. + * - type: + * The type of the file. It defines either directory, file or bin. + * The bin means that the file is a sketch binary, and the + * AutoConnectUpdate class recognizes only files type bin as + * available update files. + * - path: + * A path parameter specifies the path on the server storing + * available sketch binaries. + * + * Access to the path on the server: + * It should have access to the bin file. The update server needs to + * send a bin file with a content type of application/octet-stream via + * HTTP and also needs to attach an MD5 hash value to the x-MD5 header. + */ + +/** + * A destructor. Release the update processing dialogue page generated + * as AutoConnectAux. + */ +AutoConnectUpdate::~AutoConnectUpdate() { + _catalog.reset(nullptr); + _progress.reset(nullptr); + _result.reset(nullptr); + _WiFiClient.reset(nullptr); +} + +/** + * Attach the AutoConnectUpdate to the AutoConnect which constitutes + * the bedrock of the update process. This function creates dialog pages + * for update operation as an instance of AutoConnectAux and joins to + * the AutoConnect which is the bedrock of the process. + * @param portal A reference of AutoConnect + */ +void AutoConnectUpdate::attach(AutoConnect& portal) { + AutoConnectAux* updatePage; + + updatePage = new AutoConnectAux(String(FPSTR(_auxCatalog.uri)), String(FPSTR(_auxCatalog.title)), _auxCatalog.menu); + _buildAux(updatePage, &_auxCatalog, lengthOf(_elmCatalog)); + _catalog.reset(updatePage); + + updatePage = new AutoConnectAux(String(FPSTR(_auxProgress.uri)), String(FPSTR(_auxProgress.title)), _auxProgress.menu); + _buildAux(updatePage, &_auxProgress, lengthOf(_elmProgress)); + _progress.reset(updatePage); + + updatePage = new AutoConnectAux(String(FPSTR(_auxResult.uri)), String(FPSTR(_auxResult.title)), _auxResult.menu); + _buildAux(updatePage, &_auxResult, lengthOf(_elmResult)); + _result.reset(updatePage); + _catalog->on(std::bind(&AutoConnectUpdate::_onCatalog, this, std::placeholders::_1, std::placeholders::_2), AC_EXIT_AHEAD); + _result->on(std::bind(&AutoConnectUpdate::_onResult, this, std::placeholders::_1, std::placeholders::_2), AC_EXIT_AHEAD); + _progress->on(std::bind(&AutoConnectUpdate::_onUpdate, this, std::placeholders::_1, std::placeholders::_2), AC_EXIT_AHEAD); + + portal.join(*_catalog.get()); + portal.join(*_progress.get()); + portal.join(*_result.get()); + + _status = UPDATE_IDLE; + + // Attach this to the AutoConnectUpdate + portal._update.reset(this); + AC_DBG("AutoConnectUpdate attached\n"); + if (WiFi.status() == WL_CONNECTED) + enable(); +} + +/** + * Detach the update item from the current AutoConnect menu. + * AutoConnectUpdate still active. + */ +void AutoConnectUpdate::disable(void) { + if (_catalog) { + _catalog->menu(false); + if (_WiFiClient) + _WiFiClient.reset(nullptr); + AC_DBG("AutoConnectUpdate disabled\n"); + } +} + +/** + * Make AutoConnectUpdate class available by incorporating the update + * function into the menu. + */ +void AutoConnectUpdate::enable(void) { + if (_catalog) { + _catalog->menu(true); + _status = UPDATE_IDLE; + _period = 0; + AC_DBG("AutoConnectUpdate enabled\n"); + } +} + +void AutoConnectUpdate::handleUpdate(void) { + // Purge WiFiClient instances that have exceeded their retention + // period to avoid running out of memory. + if (_WiFiClient) { + if (millis() - _period > AUTOCONNECT_UPDATE_DURATION) + if (_status != UPDATE_START) { + _WiFiClient.reset(nullptr); + AC_DBG("Purged WifiClient due to duration expired.\n"); + } + } + + if (isEnable()) { + if (WiFi.status() == WL_CONNECTED) { + // Evaluate the processing status of AutoConnectUpdate and + // execute it accordingly. It is only this process point that + // requests update processing. + if (_status == UPDATE_START) + update(); + else if (_status == UPDATE_RESET) { + AC_DBG("Restart on %s updated...\n", _binName.c_str()); + ESP.restart(); + } + } + // If WiFi is not connected, disables the update menu. + // However, the AutoConnectUpdate class stills active. + else + disable(); + } +} + +/** + * Run the update function of HTTPUpdate that is the base class and + * fetch the result. + * @return AC_UPDATESTATUS_t + */ +AC_UPDATESTATUS_t AutoConnectUpdate::update(void) { + String uriBin = uri + '/' + _binName; + if (_binName.length()) { + AC_DBG("%s:%d/%s update in progress...", host.c_str(), port, uriBin.c_str()); + if (!_WiFiClient) { + _WiFiClient.reset(new WiFiClient); + _period = millis(); + } + t_httpUpdate_return ret = HTTPUpdateClass::update(*_WiFiClient, host, port, uriBin); + switch (ret) { + case HTTP_UPDATE_FAILED: + _status = UPDATE_FAIL; + AC_DBG_DUMB(" %s\n", getLastErrorString().c_str()); + break; + case HTTP_UPDATE_NO_UPDATES: + _status = UPDATE_IDLE; + AC_DBG_DUMB(" No available update\n"); + break; + case HTTP_UPDATE_OK: + _status = UPDATE_SUCCESS; + AC_DBG_DUMB(" completed\n"); + break; + } + _WiFiClient.reset(nullptr); + } + else { + AC_DBG("An update has not specified"); + _status = UPDATE_NOAVAIL; + } + return _status; +} + +/** + * Create the update operation pages using a predefined page structure + * with two structures as ACPage_t and ACElementProp_t which describe + * for AutoConnectAux configuration. + * This function receives instantiated AutoConnectAux, instantiates + * defined AutoConnectElements by ACPage_t, and configures it into + * received AutoConnectAux. + * @param aux An instantiated AutoConnectAux that will configure according to ACPage_t. + * @param page Pre-defined ACPage_t + * @param elementNum Number of AutoConnectElements to configure. + */ +void AutoConnectUpdate::_buildAux(AutoConnectAux* aux, const AutoConnectUpdate::ACPage_t* page, const size_t elementNum) { + for (size_t n = 0; n < elementNum; n++) { + if (page->element[n].type == AC_Element) { + AutoConnectElement* element = new AutoConnectElement; + element->name = String(FPSTR(page->element[n].name)); + if (page->element[n].value) + element->value = String(FPSTR(page->element[n].value)); + aux->add(reinterpret_cast(*element)); + } + else if (page->element[n].type == AC_Radio) { + AutoConnectRadio* element = new AutoConnectRadio; + element->name = String(FPSTR(page->element[n].name)); + aux->add(reinterpret_cast(*element)); + } + else if (page->element[n].type == AC_Submit) { + AutoConnectSubmit* element = new AutoConnectSubmit; + element->name = String(FPSTR(page->element[n].name)); + if (page->element[n].value) + element->value = String(FPSTR(page->element[n].value)); + if (page->element[n].peculiar) + element->uri = String(FPSTR(page->element[n].peculiar)); + aux->add(reinterpret_cast(*element)); + } + else if (page->element[n].type == AC_Text) { + AutoConnectText* element = new AutoConnectText; + element->name = String(FPSTR(page->element[n].name)); + if (page->element[n].value) + element->value = String(FPSTR(page->element[n].value)); + if (page->element[n].peculiar) + element->format = String(FPSTR(page->element[n].peculiar)); + aux->add(reinterpret_cast(*element)); + } + } +} + +/** + * Register only bin type file name as available sketch binary to + * AutoConnectRadio value based on the response from the update server. + * @param radio A reference to AutoConnectRadio + * @param responseBody JSON variant of a JSON document responded from the Update server + * @return Number of available sketch binaries + */ +size_t AutoConnectUpdate::_insertCatalog(AutoConnectRadio& radio, JsonVariant& responseBody) { + ArduinoJsonArray firmwares = responseBody.as(); + radio.empty(firmwares.size()); + for (ArduinoJsonObject entry : firmwares) + if (entry[F("type")] == "bin") + radio.add(entry[F("name")].as()); + return firmwares.size(); +} + +/** + * AUTOCONNECT_URI_UPDATE page handler. + * It queries the update server for cataloged sketch binary and + * displays the result on the page as an available updater list. + * The update execution button held by this page will be enabled only + * when there are any available updaters. + * @param catalog A reference of the AutoConnectAux as AUTOCONNECT_URI_UPDATE + * @param args A reference of the PageArgument of the PageBuilder + * @return Additional string to the page but it always null. + */ +String AutoConnectUpdate::_onCatalog(AutoConnectAux& catalog, PageArgument& args) { + AC_UNUSED(args); + HTTPClient httpClient; + + // Reallocate WiFiClient if it is not existence. + if (!_WiFiClient) { + _WiFiClient.reset(new WiFiClient); + _period = millis(); + } + + AutoConnectText& caption = catalog.getElement(String(F("caption"))); + AutoConnectRadio& firmwares = catalog.getElement(String(F("firmwares"))); + AutoConnectSubmit& submit = catalog.getElement(String(F("update"))); + firmwares.empty(); + submit.enable = false; + _binName = String(""); + + String qs = String(F(AUTOCONNECT_UPDATE_CATALOG)) + '?' + String(F("op=list&path=")) + uri; + AC_DBG("Update query %s:%d%s\n", host.c_str(), port, qs.c_str()); + + // Throw a query to the update server and parse the response JSON + // document. After that, display the bin type file name contained in + // its JSON document as available updaters to the page. + if (httpClient.begin(*_WiFiClient.get(), host, port, qs, false)) { + int responseCode = httpClient.GET(); + if (responseCode == HTTP_CODE_OK) { + + // The size of the JSON buffer is a fixed. It can be a problem + // when parsing with ArduinoJson V6. If memory insufficient has + // occurred during the parsing, increase this buffer size. + ArduinoJsonBuffer json(AUTOCONNECT_UPDATE_CATALOG_JSONBUFFER_SIZE); + + JsonVariant jb; + bool parse; + Stream& responseBody = httpClient.getStream(); +#if ARDUINOJSON_VERSION_MAJOR<=5 + jb = json.parse(responseBody); + parse = jb.success(); +#else + DeserializationError err = deserializeJson(json, responseBody); + parse = !(err == true); + if (parse) + jb = json.as(); +#endif + if (parse) { + caption.value = String(F("

Available firmwares

")); + JsonVariant firmwareList = json.as(); + if (_insertCatalog(firmwares, firmwareList) > 0) + submit.enable = true; + } + else + caption.value = String(F("Invalid catalog list:")) + String(err.c_str()); +#if defined(AC_DEBUG) + AC_DBG("Update server responds catalog list\n"); + ARDUINOJSON_PRINT(jb, AC_DEBUG_PORT); + AC_DBG_DUMB("\n"); +#endif + } + else { + caption.value = String(F("Update server responds (")) + String(responseCode) + String("):"); + caption.value += HTTPClient::errorToString(responseCode); + } + httpClient.end(); + } + else + caption.value = String(F("http failed connect to ")) + host + String(':') + String(port); + + return String(""); +} + +/** + * AUTOCONNECT_URI_UPDATE_ACT page handler + * Only display a dialog indicating that the update is in progress. + * @param progress A reference of the AutoConnectAux as AUTOCONNECT_URI_UPDATE_ACT + * @param args A reference of the PageArgument of the PageBuilder + * @return Additional string to the page but it always null. + */ +String AutoConnectUpdate::_onUpdate(AutoConnectAux& progress, PageArgument& args) { + AC_UNUSED(args); + AutoConnectText& flash = progress.getElement(String(F("flash"))); + _binName = _catalog->getElement(String(F("firmwares"))).value(); + flash.value = _binName; + _status = UPDATE_START; + return String(""); +} + +/** + * AUTOCONNECT_URI_UPDATE_RESULT page handler + * Display the result of the update function of HTTPUpdate class. + * @param result A reference of the AutoConnectAux as AUTOCONNECT_URI_UPDATE_RESULT + * @param args A reference of the PageArgument of the PageBuilder + * @return Additional string to the page but it always null. + */ +String AutoConnectUpdate::_onResult(AutoConnectAux& result, PageArgument& args) { + AC_UNUSED(args); + String resForm; + String resColor; + bool restart = false; + + switch (_status) { + case UPDATE_SUCCESS: + resForm = String(F(" sucessfully updated. restart...")); + resColor = String(F("blue")); + restart = true; + break; + case UPDATE_FAIL: + resForm = String(F(" failed.")); + resForm += String(F("
")) + getLastErrorString(); + resColor = String(F("red")); + break; + default: + resForm = String(F("No available update.")); + resColor = String(F("red")); + break; + } + AutoConnectText& resultElm = result.getElement(String(F("status"))); + resultElm.value = _binName + resForm; + resultElm.style = String(F("font-size:120%;color:")) + resColor; + result.getElement(String(F("restart"))).enable = restart; + if (restart) + _status = UPDATE_RESET; + return String(""); +} diff --git a/src/AutoConnectUpdate.h b/src/AutoConnectUpdate.h new file mode 100644 index 0000000..1acfe85 --- /dev/null +++ b/src/AutoConnectUpdate.h @@ -0,0 +1,139 @@ +/** + * Declaration of AutoConnectUpdate class. + * The AutoConnectUpdate class is a class for updating sketch binary + * via OTA and inherits the HTTPUpdate class of the arduino core. + * It declares the class implementations of both core libraries as + * HTTPUpdateClass to absorb differences between ESP8266 and ESP32 + * class definitions. + * The AutoConnectUpdate class add ons three features to the HTTPupdate + * class. + * 1. Dialog pages for operating the update. + * The dialog pages are all AutoConnectAux, and they select available + * sketch binary, display during update processing, and display + * update results. + * 2. Daialog pages handler + * In the dialog page, AUTOCONNECT_URI_UPDATE, AUTOCONNECT_URI_UPDATE_ACT, + * AUTOCONNECT_URI_UPDATE_RESULT are assigned and there is a page + * handler for each. + * 3. Attach to the AutoConnect. + * Attaching the AutoConnectUpdate class to AutoConnect makes the + * sketch binary update function available, and the operation dialog + * pages are incorporated into the AutoConnect menu. + * @file AutoConnectUpdate.h + * @author hieromon@gmail.com + * @version 0.9.9 + * @date 2019-05-03 + * @copyright MIT license. + */ + +#ifndef _AUTOCONNECTUPDATE_H_ +#define _AUTOCONNECTUPDATE_H_ + +#include +#define NO_GLOBAL_HTTPUPDATE +#if defined(ARDUINO_ARCH_ESP8266) +#include +#include +using HTTPUpdateClass = ESP8266HTTPUpdate; +#elif defined(ARDUINO_ARCH_ESP32) +#include +#include +using HTTPUpdateClass = HTTPUpdate; +#endif +#include "AutoConnectDefs.h" +#if defined(AUTOCONNECT_USE_UPDATE) +#ifndef AUTOCONNECT_USE_JSON +#define AUTOCONNECT_USE_JSON +#endif +#endif +#include "AutoConnect.h" + +// Support LED flashing only the board with built-in LED. +#ifdef LED_BUILTIN +#define UPDATE_SETLED(s) do {setLedPin(LED_BUILTIN, s);} while(0) +#else +#define UPDATE_SETLED(s) do {} while(0) +#endif + +// Indicate an update process loop +typedef enum AC_UPDATESTATUS { + UPDATE_RESET, /**< Update process ended, need to reset */ + UPDATE_IDLE, /**< Update process has not started */ + UPDATE_START, /**< Update process has been started */ + UPDATE_SUCCESS, /**< Update successfully completed */ + UPDATE_NOAVAIL, /**< No available update */ + UPDATE_FAIL /**< Update fails */ +} AC_UPDATESTATUS_t; + +class AutoConnectUpdate : public HTTPUpdateClass { + public: + explicit AutoConnectUpdate(const String& host = String(""), const uint16_t port = AUTOCONNECT_UPDATE_PORT, const String& uri = String("."), const int timeout = AUTOCONNECT_UPDATE_TIMEOUT) + : HTTPUpdateClass(timeout), host(host), port(port), uri(uri), _status(UPDATE_IDLE), _binName(String()), _period(0) { + UPDATE_SETLED(LOW); /**< LED blinking during the update that is the default. */ + rebootOnUpdate(false); /**< Default reboot mode */ + } + AutoConnectUpdate(AutoConnect& portal, const String& host = String(""), const uint16_t port = AUTOCONNECT_UPDATE_PORT, const String& uri = String("."), const int timeout = AUTOCONNECT_UPDATE_TIMEOUT) + : HTTPUpdateClass(timeout), host(host), port(port), uri(uri), _status(UPDATE_IDLE), _binName(String()), _period(0) { + UPDATE_SETLED(LOW); + rebootOnUpdate(false); + attach(portal); + } + ~AutoConnectUpdate(); + void attach(AutoConnect& portal); /**< Attach the update class to AutoConnect */ + void enable(void); /**< Enable the AutoConnectUpdate */ + void disable(void); /**< Disable the AutoConnectUpdate */ + void handleUpdate(void); /**< Behaves the update process */ + bool isEnable(void) { return _catalog ? _catalog->isMenu() : false; } /**< Returns current updater effectiveness */ + AC_UPDATESTATUS_t status(void) { return _status; } /**< reports the current update behavior status */ + AC_UPDATESTATUS_t update(void); /**< behaves update */ + + String host; /**< Available URL of Update Server */ + uint16_t port; /**< Port number of the update server */ + String uri; /**< The path on the update server that contains the sketch binary to be updated */ + + protected: + // Attribute definition of the element to be placed on the update page. + typedef struct { + const ACElement_t type; + const char* name; /**< Name to assign to AutoConenctElement */ + const char* value; /**< Value owned by an element */ + const char* peculiar; /**< Specific ornamentation for the element */ + } ACElementProp_t; + + // Attributes to treat included update pages as AutoConnectAux. + typedef struct { + const char* uri; /**< URI for the page */ + const char* title; /**< Menut title of update page */ + const bool menu; /**< Whether to display in menu */ + const ACElementProp_t* element; + } ACPage_t; + + template constexpr + size_t lengthOf(T(&)[N]) noexcept { return N; } + void _buildAux(AutoConnectAux* aux, const AutoConnectUpdate::ACPage_t* page, const size_t elementNum); + String _onCatalog(AutoConnectAux& catalog, PageArgument& args); + String _onUpdate(AutoConnectAux& update, PageArgument& args); + String _onResult(AutoConnectAux& result, PageArgument& args); + size_t _insertCatalog(AutoConnectRadio& radio, JsonVariant & responseBody); + + std::unique_ptr _catalog; /**< A catalog page for internally generated update binaries */ + std::unique_ptr _progress; /**< An update in-progress page */ + std::unique_ptr _result; /**< A update result page */ + + private: + AC_UPDATESTATUS_t _status; + String _binName; /**< .bin name to update */ + unsigned long _period; /**< Duration of WiFiClient holding */ + std::unique_ptr _WiFiClient; /**< Provide to HTTPUpdate class */ + + static const ACPage_t _auxCatalog PROGMEM; + static const ACElementProp_t _elmCatalog[] PROGMEM; + + static const ACPage_t _auxProgress PROGMEM; + static const ACElementProp_t _elmProgress[] PROGMEM; + + static const ACPage_t _auxResult PROGMEM; + static const ACElementProp_t _elmResult[] PROGMEM; +}; + +#endif // _AUTOCONNECTUPDATE_H_ diff --git a/src/AutoConnectUpdatePage.h b/src/AutoConnectUpdatePage.h new file mode 100644 index 0000000..44912a3 --- /dev/null +++ b/src/AutoConnectUpdatePage.h @@ -0,0 +1,46 @@ +/** + * Define dialogs to operate sketch binary updates operated by the + * AutoConnectUpdate class. + * @file AutoConnectUpdatePage.h + * @author hieromon@gmail.com + * @version 0.9.9 + * @date 2019-05-04 + * @copyright MIT license. + */ + +#ifndef _AUTOCONNECTUPDATEPAGE_H +#define _AUTOCONNECTUPDATEPAGE_H + +// Define the AUTOCONNECT_URI_UPDATE page to select the sketch binary +// for update and order update execution. +const AutoConnectUpdate::ACElementProp_t AutoConnectUpdate::_elmCatalog[] PROGMEM = { + { AC_Text, "caption", nullptr, nullptr }, + { AC_Radio, "firmwares", nullptr, nullptr }, + { AC_Submit, "update", "UPDATE", AUTOCONNECT_URI_UPDATE_ACT } +}; +const AutoConnectUpdate::ACPage_t AutoConnectUpdate::_auxCatalog PROGMEM = { + AUTOCONNECT_URI_UPDATE, "Update", false, AutoConnectUpdate::_elmCatalog +}; + +// Define the AUTOCONNECT_URI_UPDATE_ACT page to display during the +// update process. +const AutoConnectUpdate::ACElementProp_t AutoConnectUpdate::_elmProgress[] PROGMEM = { + { AC_Element, "spinner", "", nullptr }, + { AC_Text, "caption", "Update start...", "
%s
" }, + { AC_Text, "flash", nullptr, "
%s
" }, + { AC_Element, "inprogress", "", nullptr } +}; +const AutoConnectUpdate::ACPage_t AutoConnectUpdate::_auxProgress PROGMEM = { + AUTOCONNECT_URI_UPDATE_ACT, "Update", false, AutoConnectUpdate::_elmProgress +}; + +// Definition of the AUTOCONNECT_URI_UPDATE_RESULT page to notify update results +const AutoConnectUpdate::ACElementProp_t AutoConnectUpdate::_elmResult[] PROGMEM = { + { AC_Text, "status", nullptr, nullptr }, + { AC_Element, "restart", "", nullptr } +}; +const AutoConnectUpdate::ACPage_t AutoConnectUpdate::_auxResult PROGMEM = { + AUTOCONNECT_URI_UPDATE_RESULT, "Update", false, AutoConnectUpdate::_elmResult +}; + +#endif // _AUTOCONNECTUPDATEPAGE_H diff --git a/src/updateserver/updateserver.py b/src/updateserver/updateserver.py new file mode 100644 index 0000000..b70f041 --- /dev/null +++ b/src/updateserver/updateserver.py @@ -0,0 +1,168 @@ +#!python3.* + +"""update server. +""" + +import argparse +import hashlib +import http.server +import json +import logging +import os +import re +import urllib.parse + + +class UpdateHttpServer: + def __init__(self, port, bind, catalog_dir): + def handler(*args): + UpdateHTTPRequestHandler(catalog_dir, *args) + httpd = http.server.HTTPServer((bind, port), handler) + sa = httpd.socket.getsockname() + logger.info('http server starting {0}:{1} {2}'.format(sa[0], sa[1], catalog_dir)) + try: + httpd.serve_forever() + except KeyboardInterrupt: + logger.debug('Shutting down...') + httpd.socket.close() + + +class UpdateHTTPRequestHandler(http.server.BaseHTTPRequestHandler): + def __init__(self, catalog_dir, *args): + self.catalog_dir = catalog_dir + http.server.BaseHTTPRequestHandler.__init__(self, *args) + + def do_GET(self): + request_path = urllib.parse.urlparse(self.path) + if request_path.path == '/_catalog': + err = '' + query = urllib.parse.urlparse(self.path).query + try: + op = urllib.parse.parse_qs(query)['op'][0] + if op == 'list': + try: + path = urllib.parse.parse_qs(query)['path'][0] + except KeyError: + path = '.' + self.__send_dir(path) + result = True + else: + err = '{0} unknown operation'.format(op) + result = False + except KeyError: + err = '{0} invaid catalog request'.format(self.path) + result = False + if not result: + logger.info(err) + self.send_response(http.HTTPStatus.FORBIDDEN, err) + self.end_headers() + else: + self.__send_file(self.path) + + def __check_header(self): + ex_headers_templ = ['x-*-STA-MAC', 'x-*-AP-MAC', 'x-*-FREE-SPACE', 'x-*-SKETCH-SIZE', 'x-*-SKETCH-MD5', 'x-*-CHIP-SIZE', 'x-*-SDK-VERSION'] + ex_headers = [] + ua = re.match('(ESP8266|ESP32)-http-Update', self.headers.get('User-Agent')) + if ua: + arch = ua.group().split('-')[0] + ex_headers = list(map(lambda x: x.replace('*', arch), ex_headers_templ)) + else: + logger.info('User-Agent {0} is not HTTPUpdate'.format(ua)) + return False + for ex_header in ex_headers: + if ex_header not in self.headers: + logger.info('Missing header {0} to identify a legitimate request'.format(ex_header)) + return False + return True + + def __send_file(self, path): + if not self.__check_header(): + self.send_response(http.HTTPStatus.FORBIDDEN, 'The request available only from ESP8266 or ESP32 http updater.') + self.end_headers() + return + + filename = os.path.join(self.catalog_dir, path.lstrip('/')) + logger.debug('Request file:{0}'.format(filename)) + try: + fsize = os.path.getsize(filename) + self.send_response(http.HTTPStatus.OK) + self.send_header('Content-Type', 'application/octet-stream') + self.send_header('Content-Disposition', 'attachment; filename=' + os.path.basename(filename)) + self.send_header('Content-Length', fsize) + self.send_header('x-MD5', get_MD5(filename)) + self.end_headers() + f = open(filename, 'rb') + self.wfile.write(f.read()) + f.close() + except Exception as e: + err = str(e) + logger.error(err) + self.send_response(http.HTTPStatus.INTERNAL_SERVER_ERROR, err) + self.end_headers() + + def __send_dir(self, path): + content = dir_json(path) + d = json.dumps(content).encode('UTF-8', 'replace') + logger.debug(d) + self.send_response(http.HTTPStatus.OK) + self.send_header('Content-Type', 'application/json') + self.send_header('Content-Length', str(len(d))) + self.end_headers() + self.wfile.write(d) + + +def dir_json(path): + d = list() + for entry in os.listdir(path): + e = {'name': entry} + if os.path.isdir(entry): + e['type'] = "directory" + else: + e['type'] = "file" + if os.path.splitext(entry)[1] == '.bin': + try: + f = open(os.path.join(path, entry), 'rb') + c = f.read(1) + f.close() + except Exception as e: + logger.info(str(e)) + c = b'\x00' + if c == b'\xe9': + e['type'] = "bin" + d.append(e) + return d + + +def get_MD5(filename): + try: + f = open(filename, 'rb') + bin_file = f.read() + f.close() + md5 = hashlib.md5(bin_file).hexdigest() + return md5 + except Exception as e: + logger.error(str(e)) + return None + + +def run(port=8000, bind='127.0.0.1', catalog_dir='', log_level=logging.INFO): + logging.basicConfig(level=log_level) + UpdateHttpServer(port, bind, catalog_dir) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('--port', '-p', action='store', default=8000, type=int, + help='Port number [default:8000]') + parser.add_argument('--bind', '-b', action='store', default='127.0.0.1', + help='Specifies address to which it should bind. [default:127.0.0.1]') + parser.add_argument('--catalog', '-d', action='store', default=os.getcwd(), + help='Catalog directory') + parser.add_argument('--log', '-l', action='store', default='INFO', + help='Logging level') + args = parser.parse_args() + loglevel = getattr(logging, args.log.upper(), None) + if not isinstance(loglevel, int): + raise ValueError('Invalid log level: %s' % args.log) + logger = logging.getLogger(__name__) + run(args.port, args.bind, args.catalog, loglevel)