|
|
|
/**
|
|
|
|
* AutoConnectUpdate class implementation.
|
|
|
|
* @file AutoConnectUpdate.cpp
|
|
|
|
* @author hieromon@gmail.com
|
|
|
|
* @version 0.9.9
|
|
|
|
* @date 2019-05-03
|
|
|
|
* @copyright MIT license.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include <functional>
|
|
|
|
#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<AutoConnectElement&>(*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<AutoConnectElement&>(*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<AutoConnectElement&>(*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<AutoConnectText&>(*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<JsonArray>();
|
|
|
|
radio.empty(firmwares.size());
|
|
|
|
for (ArduinoJsonObject entry : firmwares)
|
|
|
|
if (entry[F("type")] == "bin")
|
|
|
|
radio.add(entry[F("name")].as<String>());
|
|
|
|
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<AutoConnectText>(String(F("caption")));
|
|
|
|
AutoConnectRadio& firmwares = catalog.getElement<AutoConnectRadio>(String(F("firmwares")));
|
|
|
|
AutoConnectSubmit& submit = catalog.getElement<AutoConnectSubmit>(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<JsonVariant>();
|
|
|
|
#endif
|
|
|
|
if (parse) {
|
|
|
|
caption.value = String(F("<h3>Available firmwares</h3>"));
|
|
|
|
JsonVariant firmwareList = json.as<JsonVariant>();
|
|
|
|
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<AutoConnectText>(String(F("flash")));
|
|
|
|
_binName = _catalog->getElement<AutoConnectRadio>(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("<br>")) + getLastErrorString();
|
|
|
|
resColor = String(F("red"));
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
resForm = String(F("<br>No available update."));
|
|
|
|
resColor = String(F("red"));
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
AutoConnectText& resultElm = result.getElement<AutoConnectText>(String(F("status")));
|
|
|
|
resultElm.value = _binName + resForm;
|
|
|
|
resultElm.style = String(F("font-size:120%;color:")) + resColor;
|
|
|
|
result.getElement<AutoConnectElement>(String(F("restart"))).enable = restart;
|
|
|
|
if (restart)
|
|
|
|
_status = UPDATE_RESET;
|
|
|
|
return String("");
|
|
|
|
}
|