diff --git a/src/AutoConnectDefs.h b/src/AutoConnectDefs.h index b02fc0e..a29ac80 100644 --- a/src/AutoConnectDefs.h +++ b/src/AutoConnectDefs.h @@ -3,7 +3,7 @@ * @file AutoConnectDefs.h * @author hieromon@gmail.com * @version 0.9.9 - * @date 2019-04-30 + * @date 2019-05-14 * @copyright MIT license. */ @@ -200,4 +200,18 @@ // Explicitly avoiding unused warning with token handler of PageBuilder #define AC_UNUSED(expr) do { (void)(expr); } while (0) +// Generates a template that determines whether the class owns the +// specified member function. +// The purpose of this macro is to avoid the use of invalid member +// functions due to differences in the version of the library which +// AutoConnect depends on. +#define AC_HAS_FUNC(func) \ +template<typename T> \ +struct has_func_##func { \ + static auto check(...) -> decltype(std::false_type()); \ + template<typename U> \ + static auto check(U&) -> decltype(static_cast<decltype(U::func)*>(&U::func), std::true_type()); \ + enum : bool { value = decltype(check(std::declval<T&>()))::value }; \ +} + #endif // _AUTOCONNECTDEFS_H_ diff --git a/src/AutoConnectUpdate.cpp b/src/AutoConnectUpdate.cpp index 4f4d8f2..3d1bdea 100644 --- a/src/AutoConnectUpdate.cpp +++ b/src/AutoConnectUpdate.cpp @@ -3,11 +3,12 @@ * @file AutoConnectUpdate.cpp * @author hieromon@gmail.com * @version 0.9.9 - * @date 2019-05-03 + * @date 2019-05-14 * @copyright MIT license. */ #include <functional> +#include <type_traits> #include "AutoConnectUpdate.h" #include "AutoConnectUpdatePage.h" #include "AutoConnectJsonDefs.h" @@ -65,6 +66,40 @@ * HTTP and also needs to attach an MD5 hash value to the x-MD5 header. */ +/** + * The following two templates absorb callbacks that are enabled/disabled + * by the Update library version in the core. + * The old UpdateClass of the ESP8266/ESP32 arduino core does not have + * the onProgress function for registering callbacks. These templates + * avoid the onProgress calls on older library versions. + * In versions where the update function callback is disabled, the + * dialog on the client browser does not show the progress of the update. + */ +#if defined(ARDUINO_ARCH_ESP8266) +using UpdateVariedClass = UpdaterClass; +#elif defined(ARDUINO_ARCH_ESP32) +using UpdateVariedClass = UpdateClass; +#endif + +namespace AutoConnectUtil { +AC_HAS_FUNC(onProgress); + +template<typename T> +typename std::enable_if<AutoConnectUtil::has_func_onProgress<T>::value, AutoConnectUpdate::AC_UPDATEDIALOG_t>::type onProgress(const T& updater, std::function<void(size_t, size_t)> fn) { + updater.onProgress(fn); + AC_DBG("Callback enabled\n"); + return AutoConnectUpdate::UPDATEDIALOG_METER; +} + +template<typename T> +typename std::enable_if<!AutoConnectUtil::has_func_onProgress<T>::value, AutoConnectUpdate::AC_UPDATEDIALOG_t>::type onProgress(const T& updater, std::function<void(size_t, size_t)> fn) { + (void)(updater); + (void)(fn); + AC_DBG("Callback disabled\n"); + return AutoConnectUpdate::UPDATEDIALOG_LOADER; +} +} + /** * A destructor. Release the update processing dialogue page generated * as AutoConnectAux. @@ -108,9 +143,29 @@ void AutoConnectUpdate::attach(AutoConnect& portal) { _status = UPDATE_IDLE; -#ifdef ARDUINO_ARCH_ESP32 - Update.onProgress(std::bind(&AutoConnectUpdate::_inProgress, this, std::placeholders::_1, std::placeholders::_2)); -#endif + // Register the callback to inform the update progress + _dialog = AutoConnectUtil::onProgress<UpdateVariedClass>(Update, std::bind(&AutoConnectUpdate::_inProgress, this, std::placeholders::_1, std::placeholders::_2)); + // Update.onProgress(std::bind(&AutoConnectUpdate::_inProgress, this, std::placeholders::_1, std::placeholders::_2)); + // _dialog = UPDATEDIALOG_METER; + + // Adjust the client dialog pattern according to the callback validity + // of the UpdateClass. + AutoConnectElement* loader = _progress->getElement(String(F("loader"))); + AutoConnectElement* progress_meter = _progress->getElement(String(F("progress_meter"))); + AutoConnectElement* inprogress_meter = _progress->getElement(String(F("inprogress_meter"))); + AutoConnectElement* progress_loader = _progress->getElement(String(F("progress_loader"))); + AutoConnectElement* inprogress_loader = _progress->getElement(String(F("inprogress_loader"))); + switch (_dialog) { + case UPDATEDIALOG_LOADER: + progress_meter->enable =false; + inprogress_meter->enable = false; + break; + case UPDATEDIALOG_METER: + loader->enable = false; + progress_loader->enable =false; + inprogress_loader->enable = false; + break; + } // Attach this to the AutoConnectUpdate portal._update.reset(this); @@ -161,8 +216,22 @@ void AutoConnectUpdate::handleUpdate(void) { // 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(); + if (_status == UPDATE_START) { + _ws->loop(); // Crawl the connection request. + unsigned long tm = millis(); + while (_ws->connectedClients() <= 0) { + if (millis() - tm > AUTOCONNECT_UPDATE_TIMEOUT) { + AC_DBG("WebSocket client connection timeout, update ignored\n"); + break; + } + _ws->loop(); // Crawl the connection request. + } + // Launch the update + if (_ws->connectedClients()) + update(); + else + _status = UPDATE_IDLE; + } else if (_status == UPDATE_RESET) { AC_DBG("Restart on %s updated...\n", _binName.c_str()); ESP.restart(); @@ -181,10 +250,6 @@ void AutoConnectUpdate::handleUpdate(void) { * @return AC_UPDATESTATUS_t */ AC_UPDATESTATUS_t AutoConnectUpdate::update(void) { - // Crawl queued requests. - if (_ws) - _ws->loop(); - // Start update String uriBin = uri + '/' + _binName; if (_binName.length()) { @@ -198,6 +263,7 @@ AC_UPDATESTATUS_t AutoConnectUpdate::update(void) { case HTTP_UPDATE_FAILED: _status = UPDATE_FAIL; AC_DBG_DUMB(" %s\n", getLastErrorString().c_str()); + AC_DBG("update returns HTTP_UPDATE_FAILED\n"); break; case HTTP_UPDATE_NO_UPDATES: _status = UPDATE_IDLE; @@ -210,11 +276,7 @@ AC_UPDATESTATUS_t AutoConnectUpdate::update(void) { } _WiFiClient.reset(nullptr); // Request the client to close the WebSocket. - if (_ws) { - String cmdClose = String("#e"); - _ws->sendTXT(_wsClient, cmdClose); - _ws->loop(); - } + _ws->sendTXT(_wsClient, "#e", 2); } else { AC_DBG("An update has not specified"); @@ -375,26 +437,23 @@ String AutoConnectUpdate::_onCatalog(AutoConnectAux& catalog, PageArgument& args String AutoConnectUpdate::_onUpdate(AutoConnectAux& progress, PageArgument& args) { AC_UNUSED(args); // launch the WebSocket server - WebSocketsServer* ws = new WebSocketsServer(AUTOCONNECT_WEBSOCKETPORT); - if (ws) { - ws->begin(); - ws->onEvent(std::bind(&AutoConnectUpdate::_wsEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + _ws.reset(new WebSocketsServer(AUTOCONNECT_WEBSOCKETPORT)); + if (_ws) { + _ws->begin(); + _ws->onEvent(std::bind(&AutoConnectUpdate::_wsEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); } - else { + else AC_DBG("WebSocketsServer allocation failed\n"); - } - _ws.reset(ws); // Constructs the dialog page. - AutoConnectText& binName = progress.getElement<AutoConnectText>(String(F("binname"))); + AutoConnectElement* binName = progress.getElement(String(F("binname"))); _binName = _catalog->getElement<AutoConnectRadio>(String(F("firmwares"))).value(); - binName.value = _binName; - AutoConnectText& url = progress.getElement<AutoConnectText>(String("url")); - url.value = host + ':' + port; - AutoConnectElement& inprogress = progress.getElement<AutoConnectElement>(String(F("inprogress"))); - String js = inprogress.value; - js.replace(String(F("#wsserver#")), WiFi.localIP().toString() + ':' + AUTOCONNECT_WEBSOCKETPORT); - inprogress.value = js; + binName->value = _binName; + AutoConnectElement* url = progress.getElement(String("url")); + url->value = host + ':' + port; + AutoConnectElement* wsurl = progress.getElement(String(F("wsurl"))); + wsurl->value = "ws://" + WiFi.localIP().toString() + ':' + AUTOCONNECT_WEBSOCKETPORT; + AC_DBG("Cast WS %s\n", wsurl->value.c_str()); _status = UPDATE_START; return String(""); } @@ -412,6 +471,11 @@ String AutoConnectUpdate::_onResult(AutoConnectAux& result, PageArgument& args) String resColor; bool restart = false; + if (_ws) { + _ws->close(); + _ws.reset(nullptr); + } + switch (_status) { case UPDATE_SUCCESS: resForm = String(F(" sucessfully updated. restart...")); @@ -443,16 +507,11 @@ void AutoConnectUpdate::_inProgress(size_t amount, size_t size) { _binSize = size; String payload = "#p," + String(_amount) + ':' + String(_binSize); _ws->sendTXT(_wsClient, payload); - _ws->loop(); } } void AutoConnectUpdate::_wsEvent(uint8_t client, WStype_t event, uint8_t* payload, size_t length) { - AC_DBG("WS event(%d)\n", event); + AC_DBG("WS client:%d event(%d)\n", client, event); if (event == WStype_CONNECTED) _wsClient = client; - else if (event == WStype_DISCONNECTED) { - if (client == _wsClient) - _ws.reset(nullptr); - } } diff --git a/src/AutoConnectUpdate.h b/src/AutoConnectUpdate.h index 30894d5..879d908 100644 --- a/src/AutoConnectUpdate.h +++ b/src/AutoConnectUpdate.h @@ -22,7 +22,7 @@ * @file AutoConnectUpdate.h * @author hieromon@gmail.com * @version 0.9.9 - * @date 2019-05-03 + * @date 2019-05-14 * @copyright MIT license. */ @@ -92,6 +92,12 @@ class AutoConnectUpdate : public HTTPUpdateClass { 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 */ + // + typedef enum { + UPDATEDIALOG_LOADER, + UPDATEDIALOG_METER + } AC_UPDATEDIALOG_t; + protected: // Attribute definition of the element to be placed on the update page. typedef struct { @@ -129,7 +135,8 @@ class AutoConnectUpdate : public HTTPUpdateClass { size_t _binSize; /**< Updater binary size */ private: - AC_UPDATESTATUS_t _status; + AC_UPDATEDIALOG_t _dialog; /**< The type of updating dialog displayed on the client */ + AC_UPDATESTATUS_t _status; /**< Status of update processing during the cycle of receiving a request */ String _binName; /**< .bin name to update */ unsigned long _period; /**< Duration of WiFiClient holding for the connection with the update server */ std::unique_ptr<WiFiClient> _WiFiClient; /**< Provide to HTTPUpdate class */ diff --git a/src/AutoConnectUpdatePage.h b/src/AutoConnectUpdatePage.h index b0a4527..663f578 100644 --- a/src/AutoConnectUpdatePage.h +++ b/src/AutoConnectUpdatePage.h @@ -4,7 +4,7 @@ * @file AutoConnectUpdatePage.h * @author hieromon@gmail.com * @version 0.9.9 - * @date 2019-05-04 + * @date 2019-05-14 * @copyright MIT license. */ @@ -25,12 +25,22 @@ const AutoConnectUpdate::ACPage_t AutoConnectUpdate::_auxCatalog PROGMEM = { // Define the AUTOCONNECT_URI_UPDATE_ACT page to display during the // update process. const AutoConnectUpdate::ACElementProp_t AutoConnectUpdate::_elmProgress[] PROGMEM = { - { AC_Element, "caption", "<div style=\"display:inline-block\"", nullptr }, - { AC_Text, "binname", nullptr, "%s from " }, - { AC_Text, "url", nullptr, "%s</div>" }, - { AC_Element, "progress", "<div id=\"progress\">Updating... <meter min=\"0\"></meter></div>", nullptr }, + { AC_Element, "loader", "<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_Element, "c1", "<div style=\"display:inline-block\">", nullptr }, + { AC_Element, "binname", nullptr, nullptr }, + { AC_Element, "c2", " from ", nullptr }, + { AC_Element, "url", "</dv>", nullptr }, + { AC_Element, "c3", "<div id=\"progress\">Updating...<span style=\"display:inline-block;vertical-align:middle;margin-left:7px\">", nullptr }, + { AC_Element, "progress_meter", "<meter min=\"0\" />", nullptr }, + { AC_Element, "progress_loader", "<div class=\"loader\" />", nullptr }, + { AC_Element, "c4", "</span></div></div>", nullptr }, { AC_Text, "status", nullptr, nullptr }, - { AC_Element, "inprogress", "<script type='text/javascript'>var ws;window.onload=function(){ws=new WebSocket('ws://'+'#wsserver#');ws.onopen=function(e){ws.onmessage=function(e){var pl=e.data.split(',');if(pl[0]=='#p'){var iv=pl[1].split(':');var pb=document.getElementById('progress').getElementsByTagName('meter');pb[0].setAttribute('value',iv[0]);pb[0].setAttribute('max',iv[1]);}else if(pl[0]=='#e'){location.href='" AUTOCONNECT_URI_UPDATE_RESULT "';}};ws.onclose=function(e){console.log('WS close('+e.code+') '+e.reason);};};ws.onerror=function(e){console.log(e);document.getElementById('status').textContent='Connection failed.';};};window.onbeforeunload=function(){ws.close();};</script>", nullptr } + { AC_Element, "c5", "<script type=\"text/javascript\">var ws;window.onload=function(){ws=new WebSocket('", nullptr }, + { AC_Element, "wsurl", nullptr, nullptr }, + { AC_Element, "c6", "');ws.onopen=function(){ws.onmessage=function(e){var pl=e.data.split(',');if(pl[0]=='#e'){location.href='/_ac/update_result';}else if(pl[0]=='#p'){incr(pl[1]);}};};ws.onclose=function(e){console.log('WS close('+e.code+')'+e.reason);if(e.code!=1000){document.getElementById('status').textContent='WebSocket connection closed. ('+e.code+')';}};ws.onerror=function(e){if(ws.readyState==1){document.getElementById('status').textContent='WebSocket '+e.type;}};};window.onbeforeunload=function(){ws.close();};", nullptr }, + { AC_Element, "inprogress_meter", "function incr(pv){var iv=pv.split(':');var pb=document.getElementById('progress').getElementsByTagName('meter');pb[0].setAttribute('value',iv[0]);pb[0].setAttribute('max',iv[1]);}", nullptr }, + { AC_Element, "inprogress_loader", "function incr(pv){}", nullptr }, + { AC_Element, "c7", "</script>", nullptr }, }; const AutoConnectUpdate::ACPage_t AutoConnectUpdate::_auxProgress PROGMEM = { AUTOCONNECT_URI_UPDATE_ACT, "Update", false, AutoConnectUpdate::_elmProgress diff --git a/src/AutoConnectUploadImpl.h b/src/AutoConnectUploadImpl.h index baf935e..2073e61 100644 --- a/src/AutoConnectUploadImpl.h +++ b/src/AutoConnectUploadImpl.h @@ -2,8 +2,8 @@ * The default upload handler implementation. * @file AutoConnectUploadImpl.h * @author hieromon@gmail.com - * @version 0.9.8 - * @date 2019-03-19 + * @version 0.9.9 + * @date 2019-05-14 * @copyright MIT license. */ @@ -39,6 +39,20 @@ typedef SDFile SDFileT; #include "AutoConnectDefs.h" #include "AutoConnectUpload.h" +namespace AutoConnectUtil { +AC_HAS_FUNC(end); + +template<typename T> +typename std::enable_if<AutoConnectUtil::has_func_end<T>::value, void>::type end(const T* media) { + media->end(); +} + +template<typename T> +typename std::enable_if<!AutoConnectUtil::has_func_end<T>::value, void>::type end(const T* media) { + (void)(media); +} +} + /** * Handles the default upload process depending on the upload status. * This handler function supports the status of UPLOAD_FILE_START, @@ -133,9 +147,7 @@ class AutoConnectUploadSD : public AutoConnectUploadHandler { void _close(void) override { if (_file) _file.close(); -#if defined(ARDUINO_ARCH_ESP32) || (defined(ARDUINO_ARCH_ESP8266) && (!defined(ARDUINO_ESP8266_RELEASE_2_4_0) && !defined(ARDUINO_ESP8266_RELEASE_2_4_1) && !defined(ARDUINO_ESP8266_RELEASE_2_4_2))) - _media->end(); -#endif + AutoConnectUtil::end<SDClassT>(_media); } private: