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