/** * AutoConnectUpdate class implementation. * @file AutoConnectUpdate.cpp * @author hieromon@gmail.com * @version 0.9.9 * @date 2019-05-03 * @copyright MIT license. */ #include #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; #ifdef ARDUINO_ARCH_ESP32 Update.onProgress(std::bind(&AutoConnectUpdate::_inProgress, this, std::placeholders::_1, std::placeholders::_2)); #endif // 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(""); }