parent
481c8a41eb
commit
507248a4ca
@ -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 <ESP8266WiFi.h> |
||||||
|
#include <ESP8266WebServer.h> |
||||||
|
using WebServerClass = ESP8266WebServer; |
||||||
|
#elif defined(ARDUINO_ARCH_ESP32) |
||||||
|
#include <WiFi.h> |
||||||
|
#include <WebServer.h> |
||||||
|
using WebServerClass = WebServer; |
||||||
|
#endif |
||||||
|
#include <AutoConnect.h> |
||||||
|
|
||||||
|
#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(); |
||||||
|
} |
@ -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 <ArduinoJson.h> |
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<JsonObject>() |
||||||
|
#define ARDUINOJSON_CREATEARRAY(doc) doc.to<JsonArray>() |
||||||
|
#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<SpiRamAllocatorST>; |
||||||
|
#else |
||||||
|
#define AUTOCONNECT_JSONBUFFER_PRIMITIVE_SIZE AUTOCONNECT_JSONDOCUMENT_SIZE |
||||||
|
using ArduinoJsonBuffer = DynamicJsonDocument; |
||||||
|
#endif |
||||||
|
#endif |
||||||
|
|
||||||
|
#endif // _AUTOCONNECTJSONDEFS_H_
|
@ -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<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("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(""); |
||||||
|
} |
@ -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 <memory> |
||||||
|
#define NO_GLOBAL_HTTPUPDATE |
||||||
|
#if defined(ARDUINO_ARCH_ESP8266) |
||||||
|
#include <ESP8266HTTPClient.h> |
||||||
|
#include <ESP8266httpUpdate.h> |
||||||
|
using HTTPUpdateClass = ESP8266HTTPUpdate; |
||||||
|
#elif defined(ARDUINO_ARCH_ESP32) |
||||||
|
#include <HTTPClient.h> |
||||||
|
#include <HTTPUpdate.h> |
||||||
|
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<typename T, size_t N> 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<AutoConnectAux> _catalog; /**< A catalog page for internally generated update binaries */ |
||||||
|
std::unique_ptr<AutoConnectAux> _progress; /**< An update in-progress page */
|
||||||
|
std::unique_ptr<AutoConnectAux> _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> _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_
|
@ -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", "<style>.loader {border:2px solid #f3f3f3;border-radius:50%;border-top:2px solid #555;width:12px;height:12px;-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg)}}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}</style>", nullptr }, |
||||||
|
{ AC_Text, "caption", "Update start...", "<div style=\"font-size:120%%;font-weight:bold\">%s</div>" }, |
||||||
|
{ AC_Text, "flash", nullptr, "<div style=\"margin-top:18px\">%s<span style=\"display:inline-block;vertical-align:middle;margin-left:7px\"><div class=\"loader\"></div></span></div>" }, |
||||||
|
{ AC_Element, "inprogress", "<script type=\"text/javascript\">setTimeout(\"location.href='" AUTOCONNECT_URI_UPDATE_RESULT "'\",1000*15);</script>", 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", "<script type=\"text/javascript\">setTimeout(\"location.href='" AUTOCONNECT_URI "'\",1000*5);</script>", nullptr } |
||||||
|
}; |
||||||
|
const AutoConnectUpdate::ACPage_t AutoConnectUpdate::_auxResult PROGMEM = { |
||||||
|
AUTOCONNECT_URI_UPDATE_RESULT, "Update", false, AutoConnectUpdate::_elmResult |
||||||
|
}; |
||||||
|
|
||||||
|
#endif // _AUTOCONNECTUPDATEPAGE_H
|
@ -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) |
Loading…
Reference in new issue