Supports AutoConnectUpdate

pull/123/head
Hieromon Ikasamo 6 years ago
parent 481c8a41eb
commit 507248a4ca
  1. 49
      examples/Update/Update.ino
  2. 2
      library.json
  3. 2
      library.properties
  4. 33
      src/AutoConnect.cpp
  5. 11
      src/AutoConnect.h
  6. 35
      src/AutoConnectDefs.h
  7. 46
      src/AutoConnectElementJson.h
  8. 59
      src/AutoConnectJsonDefs.h
  9. 5
      src/AutoConnectLabels.h
  10. 403
      src/AutoConnectUpdate.cpp
  11. 139
      src/AutoConnectUpdate.h
  12. 46
      src/AutoConnectUpdatePage.h
  13. 168
      src/updateserver/updateserver.py

@ -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.&ensp;";
content += AUTOCONNECT_LINK(COG_24);
server.send(200, "text/html", content);
});
portal.begin();
update.attach(portal);
}
void loop() {
portal.handleClient();
}

@ -25,6 +25,6 @@
"espressif8266",
"espressif32"
],
"version": "0.9.8",
"version": "0.9.9",
"license": "MIT"
}

@ -1,5 +1,5 @@
name=AutoConnect
version=0.9.8
version=0.9.9
author=Hieromon Ikasamo <hieromon@gmail.com>
maintainer=Hieromon Ikasamo <hieromon@gmail.com>
sentence=ESP8266/ESP32 WLAN configuration at runtime with web interface.

@ -2,8 +2,8 @@
* AutoConnect class implementation.
* @file AutoConnect.cpp
* @author hieromon@gmail.com
* @version 0.9.7
* @date 2019-01-21
* @version 0.9.9
* @date 2019-05-04
* @copyright MIT license.
*/
@ -59,6 +59,11 @@ void AutoConnect::_initialize(void) {
#endif
_aux.release();
_auxUri = String("");
// Prepare to attach the updater.
// If an updater is attached, handleClient includes an update process
// in the handleClient behavior.
_update.reset(nullptr);
}
/**
@ -154,9 +159,14 @@ bool AutoConnect::begin(const char* ssid, const char* passphrase, unsigned long
}
_currentHostIP = WiFi.localIP();
// Rushing into the portal.
if (!cs) {
// End first begin process, the captive portal specific process starts here.
if (cs) {
// Activate AutoConnectUpdate if it is attached and incorporate it into the AutoConnect menu.
if (_update)
_update->enable();
}
// Rushing into the portal.
else {
// The captive portal is effective at the autoRise is valid only.
if (_apConfig.autoRise) {
@ -324,6 +334,9 @@ void AutoConnect::end(void) {
break;
}
}
// Release the updater
_update.reset(nullptr);
}
/**
@ -463,6 +476,11 @@ void AutoConnect::handleRequest(void) {
}
else
AC_DBG("%s has no BSSID, saving is unavailable\n", reinterpret_cast<const char*>(_credential.ssid));
// Activate AutoConnectUpdate if it is attached and incorporate
// it into the AutoConnect menu.
if (_update)
_update->enable();
}
else {
_currentHostIP = WiFi.softAPIP();
@ -488,7 +506,6 @@ void AutoConnect::handleRequest(void) {
if (_rfDisconnect) {
// Disconnect from the current AP.
// _waitForEndTransmission();
_stopPortal();
_disconnectWiFi(false);
while (WiFi.status() == WL_CONNECTED) {
@ -505,6 +522,10 @@ void AutoConnect::handleRequest(void) {
delay(1000);
}
}
// Handle the update behaviors for attached AutoConnectUpdate.
if (_update)
_update->handleUpdate();
}
/**

@ -2,8 +2,8 @@
* Declaration of AutoConnect class and accompanying AutoConnectConfig class.
* @file AutoConnect.h
* @author hieromon@gmail.com
* @version 0.9.7
* @date 2019-01-21
* @version 0.9.9
* @date 2019-05-04
* @copyright MIT license.
*/
@ -32,6 +32,9 @@ using WebServerClass = WebServer;
#include "AutoConnectPage.h"
#include "AutoConnectCredential.h"
#include "AutoConnectAux.h"
#include "AutoConnectUpdate.h"
class AutoConnectUpdate; // Reference to avoid circular
/**< A type to save established credential at WiFi.begin automatically. */
typedef enum AC_SAVECREDENTIAL {
@ -267,6 +270,9 @@ class AutoConnect {
String _auxUri; /**< Last accessed AutoConnectAux */
String _prevUri; /**< Previous generated page uri */
/** Available updater, only reset by AutoConnectUpdate::attach is valid */
std::unique_ptr<AutoConnectUpdate> _update;
/** Saved configurations */
AutoConnectConfig _apConfig;
struct station_config _credential;
@ -367,6 +373,7 @@ class AutoConnect {
#endif
friend class AutoConnectAux;
friend class AutoConnectUpdate;
};
#endif // _AUTOCONNECT_H_

@ -2,8 +2,8 @@
* Predefined AutoConnect configuration parameters.
* @file AutoConnectDefs.h
* @author hieromon@gmail.com
* @version 0.9.7
* @date 2018-11-17
* @version 0.9.9
* @date 2019-04-30
* @copyright MIT license.
*/
@ -29,6 +29,9 @@
// Comment out the AUTOCONNECT_USE_JSON macro to detach the ArduinoJson.
#define AUTOCONNECT_USE_JSON
// Indicator of whether to use the AutoConnectUpdate feature.
#define AUTOCONNECT_USE_UPDATE
// Predefined parameters
// SSID that Captive portal started.
#ifndef AUTOCONNECT_APID
@ -86,6 +89,9 @@
#define AUTOCONNECT_URI_RESET AUTOCONNECT_URI "/reset"
#define AUTOCONNECT_URI_SUCCESS AUTOCONNECT_URI "/success"
#define AUTOCONNECT_URI_FAIL AUTOCONNECT_URI "/fail"
#define AUTOCONNECT_URI_UPDATE AUTOCONNECT_URI "/update"
#define AUTOCONNECT_URI_UPDATE_ACT AUTOCONNECT_URI "/update_act"
#define AUTOCONNECT_URI_UPDATE_RESULT AUTOCONNECT_URI "/update_result"
// Time-out limitation when AutoConnect::begin [ms]
#ifndef AUTOCONNECT_TIMEOUT
@ -161,6 +167,31 @@
#define AUTOCONNECT_JSONPSRAM_SIZE (16* 1024)
#endif // !AUTOCONNECT_JSONPSRAM_SIZE
// Available HTTP port number for the update
#ifndef AUTOCONNECT_UPDATE_PORT
#define AUTOCONNECT_UPDATE_PORT 8000
#endif // !AUTOCONNECT_UPDATE_PORT
// HTTP client timeout limitation for the update
#ifndef AUTOCONNECT_UPDATE_TIMEOUT
#define AUTOCONNECT_UPDATE_TIMEOUT 8000
#endif // !AUTOCONNECT_UPDATE_TIMEOUT
#ifndef AUTOCONNECT_UPDATE_DURATION
#define AUTOCONNECT_UPDATE_DURATION 180000
#endif // !AUTOCONNECT_UPDATE_DURATION
// URIs of the behaviors owned by the update server
#ifndef AUTOCONNECT_UPDATE_CATALOG
#define AUTOCONNECT_UPDATE_CATALOG "/_catalog"
#endif // !AUTOCONNECT_UPDATE_CATALOG
#ifndef AUTOCONNECT_UPDATE_DOWNLOAD
#define AUTOCONNECT_UPDATE_DOWNLOAD "/"
#endif // !AUTOCONNECT_UPDATE_DOWNLOAD
#ifndef AUTOCONNECT_UPDATE_CATALOG_JSONBUFFER_SIZE
#define AUTOCONNECT_UPDATE_CATALOG_JSONBUFFER_SIZE 2048
#endif // !AUTOCONNECT_UPDATE_CATALOG_JSONBUFFER_SIZE
// Explicitly avoiding unused warning with token handler of PageBuilder
#define AC_UNUSED(expr) do { (void)(expr); } while (0)

@ -11,7 +11,7 @@
#define _AUTOCONNECTELEMENTJSON_H_
#include "AutoConnectElementBasis.h"
#include <ArduinoJson.h>
#include "AutoConnectJsonDefs.h"
#define AUTOCONNECT_JSON_KEY_ACTION "action"
#define AUTOCONNECT_JSON_KEY_ARRANGE "arrange"
@ -46,50 +46,6 @@
#define AUTOCONNECT_JSON_VALUE_SD "sd"
#define AUTOCONNECT_JSON_VALUE_VERTICAL "vertical"
/**
* Make the Json types and functions consistent with the ArduinoJson
* version. These declarations share the following type definitions:
* - Difference between reference and proxy of JsonObject and JsonArray.
* - Difference of check whether the parsing succeeded or not.
* - The print function name difference.
* - The buffer class difference.
* - When PSRAM present, enables the buffer allocation it with ESP32 and
* supported version.
*/
#if ARDUINOJSON_VERSION_MAJOR<=5
#define ARDUINOJSON_CREATEOBJECT(doc) doc.createObject()
#define ARDUINOJSON_CREATEARRAY(doc) doc.createArray()
#define ARDUINOJSON_PRETTYPRINT(doc, out) ({ size_t s = doc.prettyPrintTo(out); s; })
#define ARDUINOJSON_PRINT(doc, out) ({ size_t s = doc.printTo(out); s; })
using ArduinoJsonObject = JsonObject&;
using ArduinoJsonArray = JsonArray&;
using ArduinoJsonBuffer = DynamicJsonBuffer;
#define AUTOCONNECT_JSONBUFFER_PRIMITIVE_SIZE AUTOCONNECT_JSONBUFFER_SIZE
#else
#define ARDUINOJSON_CREATEOBJECT(doc) doc.to<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
/**
* AutoConnectAux element base with handling with JSON object.
* Placed a raw text that can be added by user sketch.

@ -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_

@ -56,6 +56,11 @@
//#define AUTOCONNECT_MENULABEL_HOME "Main"
#endif // !AUTOCONNECT_MENULABEL_HOME
// Menu item: Update
#ifndef AUTOCONNECT_MENULABEL_UPDATE
#define AUTOCONNECT_MENULABEL_UPDATE "Update"
#endif // !AUTOCONNECT_MENULABEL_UPDATE
// Button label: RESET
#ifndef AUTOCONNECT_BUTTONLABEL_RESET
#define AUTOCONNECT_BUTTONLABEL_RESET "RESET"

@ -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…
Cancel
Save