diff --git a/code/espurna/config/prototypes.h b/code/espurna/config/prototypes.h index 265a14f6c6..79d7bbf41d 100644 --- a/code/espurna/config/prototypes.h +++ b/code/espurna/config/prototypes.h @@ -237,6 +237,7 @@ void mqttSendStatus(); // OTA // ----------------------------------------------------------------------------- +#include #include #if OTA_CLIENT == OTA_CLIENT_ASYNCTCP @@ -325,6 +326,12 @@ void settingsProcessConfig(const settings_cfg_list_t& config, settings_filter_t Stream & terminalSerial(); #endif +// ----------------------------------------------------------------------------- +// Thingspeak +// ----------------------------------------------------------------------------- +class AsyncHttp; +struct AsyncHttpError; + // ----------------------------------------------------------------------------- // Utils // ----------------------------------------------------------------------------- diff --git a/code/espurna/libs/Http.h b/code/espurna/libs/Http.h new file mode 100644 index 0000000000..763bdc4d9c --- /dev/null +++ b/code/espurna/libs/Http.h @@ -0,0 +1,575 @@ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +#ifndef ASYNC_HTTP_DEBUG +#define ASYNC_HTTP_DEBUG(...) //DEBUG_PORT.printf(__VA_ARGS__) +#endif + +namespace Headers { + PROGMEM const char HOST[] = "Host"; + PROGMEM const char USER_AGENT[] = "User-Agent"; + PROGMEM const char CONNECTION[] = "Connection"; + PROGMEM const char CONTENT_TYPE[] = "Content-Type"; + PROGMEM const char CONTENT_LENGTH[] = "Content-Length"; +}; + +class AsyncHttpHeader { + + public: + + using key_value_t = std::pair; + + private: + + const String _key; + const String _value; + key_value_t _kv; + + public: + + AsyncHttpHeader(const char* key, const char* value) : + _key(FPSTR(key)), + _value(FPSTR(value)), + _kv(_key, _value) + {} + + AsyncHttpHeader(const AsyncHttpHeader& other) : + _key(other._key), + _value(other._value), + _kv(_key, _value) + {} + + AsyncHttpHeader(const AsyncHttpHeader&& other) : + _key(std::move(other._key)), + _value(std::move(other._value)), + _kv(_key, _value) + {} + + const key_value_t& get() const { + return _kv; + } + + const char* key() const { + return _key.c_str(); + } + + const char* value() const { + return _value.c_str(); + } + + size_t keyLength() const { + return _key.length(); + } + + size_t valueLength() const { + return _value.length(); + } + + bool operator ==(const AsyncHttpHeader& header) { + return ( + (header._key == _key) && (header._value == _value) + ); + } + +}; + +class AsyncHttpHeaders { + + using header_t = AsyncHttpHeader; + using headers_t = std::vector; + + private: + + headers_t _headers; + size_t _index; + size_t _last; + String _value; + + public: + + AsyncHttpHeaders() : + _index(0), + _last(std::numeric_limits::max()) + {} + + AsyncHttpHeaders(headers_t& headers) : + _headers(headers), + _index(0), + _last(std::numeric_limits::max()) + {} + + void add(const char* key, const char* value) { + _headers.emplace_back(key, value); + } + + size_t size() { + return _headers.size(); + } + + void reserve(size_t size) { + _headers.reserve(size); + } + + bool has(const char* key) { + for (const auto& header : _headers) { + if (strncmp(key, header.key(), header.keyLength()) == 0) return true; + } + return false; + } + + String& current() { + if (_last == _index) return _value; + if (_headers.size() && (_index < _headers.size())) { + const auto& current = _headers.at(_index); + _value.reserve( + current.keyLength() + + current.valueLength() + + strlen(": \r\n") + ); + + _value = current.key(); + _value += ": "; + _value += current.value(); + _value += "\r\n"; + } else { + _value = ""; + } + + _last = _index; + + return _value; + } + + String& next() { + ++_index; + return current(); + } + + bool done() { + return (_index >= _headers.size()); + } + + void clear() { + _index = 0; + _last = std::numeric_limits::max(); + _headers.clear(); + } + + headers_t::const_iterator begin() { + return _headers.begin(); + } + + headers_t::const_iterator end() { + return _headers.end(); + } + +}; + +class AsyncHttpError { + + public: + + enum error_t { + EMPTY, + CLIENT_ERROR, + REQUEST_TIMEOUT, + NETWORK_TIMEOUT, + }; + + const error_t error; + const String data; + + AsyncHttpError() : + error(EMPTY), data(0) + {} + + AsyncHttpError(AsyncHttpError&) = default; + + AsyncHttpError(const error_t& err, const String& data) : + error(err), data(data) + {} + + bool operator==(const error_t& err) const { + return err == error; + } + + bool operator==(const AsyncHttpError& obj) const { + return obj.error == error; + } + +}; + +class AsyncHttp { + + constexpr static const size_t DEFAULT_TIMEOUT = 5000; + constexpr static const size_t DEFAULT_PATH_BUFSIZE = 256; + + public: + + AsyncClient client; + + enum cfg_t { + HTTP_SEND = 1 << 0, + HTTP_RECV = 1 << 1 + }; + + enum class state_t : uint8_t { + NONE, + HEADERS, + BODY + }; + + using on_connected_f = std::function; + using on_status_f = std::function; + using on_disconnected_f = std::function; + using on_error_f = std::function; + + using on_body_recv_f = std::function; + using on_body_send_f = std::function; + + int cfg = HTTP_RECV; + state_t state = state_t::NONE; + AsyncHttpError::error_t last_error; + + on_connected_f on_connected; + on_disconnected_f on_disconnected; + + on_status_f on_status; + on_error_f on_error; + + on_body_recv_f on_body_recv; + on_body_send_f on_body_send_prepare; + on_body_send_f on_body_send_data; + + String method; + String path; + + // WebRequest.cpp + //LinkedList headers; + //std::vector headers; + AsyncHttpHeaders headers; + + String host; + uint16_t port; + + uint32_t ts; + uint32_t timeout = DEFAULT_TIMEOUT; + + bool connected = false; + bool connecting = false; + + // TODO ref: https://github.com/xoseperez/espurna/pull/1909#issuecomment-533319480 + // since LWIP_NETIF_TX_SINGLE_PBUF is enabled, no need to buffer anything and we can directly use client->add with non-persistent data + // buuut... this exposes asyncclient to the modules, maybe this needs a simple cbuf periodically flushing the data and this method simply filling it + void trySend() { + if (!client.canSend()) return; + if (!on_body_send_data) { + client.close(true); + return; + } + on_body_send_data(this, &client); + } + + bool trySendHeaders() { + do { + if (!client.canSend()) return false; + if (headers.done()) return true; + + const auto& string = headers.current(); + const auto len = string.length(); + + if (!len) { + return true; + } + + if (client.space() >= (len + 2)) { + if (client.add(string.c_str(), len)) { + if (!headers.next().length()) { + client.add("\r\n", 2); + break; + } + continue; + } + } + + } while (true); + + client.send(); + return false; + + } + + + protected: + + static AsyncHttpError _timeoutError(AsyncHttpError::error_t error, const char* message, uint32_t ts) { + String data; + data.reserve(32); + data += FPSTR(message); + data += ' '; + data += String(ts); + return {error, data}; + } + + static void _onDisconnect(void* http_ptr, AsyncClient*) { + AsyncHttp* http = static_cast(http_ptr); + if (http->on_disconnected) http->on_disconnected(http); + http->ts = 0; + http->connected = false; + http->connecting = false; + http->state = AsyncHttp::state_t::NONE; + } + + static void _onTimeout(void* http_ptr, AsyncClient* client, uint32_t time) { + client->close(true); + + AsyncHttp* http = static_cast(http_ptr); + http->last_error = AsyncHttpError::NETWORK_TIMEOUT; + if (http->on_error) http->on_error(http, _timeoutError(AsyncHttpError::NETWORK_TIMEOUT, PSTR("Network timeout after"), time)); + } + + static void _onPoll(void* http_ptr, AsyncClient* client) { + AsyncHttp* http = static_cast(http_ptr); + const auto diff = millis() - http->ts; + if (diff > http->timeout) { + if (http->on_error) http->on_error(http, _timeoutError(AsyncHttpError::REQUEST_TIMEOUT, PSTR("No response after"), diff)); + client->close(true); + } + } + + static void _onData(void* http_ptr, AsyncClient* client, void* response, size_t len) { + + AsyncHttp* http = static_cast(http_ptr); + http->ts = millis(); + + char * p = nullptr; + + do { + + p = nullptr; + + switch (http->state) { + case AsyncHttp::state_t::NONE: + { + if (len < strlen("HTTP/1.1 ... OK") + 1) { + ASYNC_HTTP_DEBUG("err | not enough len\n"); + client->close(true); + return; + } + p = strnstr(reinterpret_cast(response), "HTTP/1.", len); + if (!p) { + ASYNC_HTTP_DEBUG("err | not http\n"); + client->close(true); + return; + } + + p += strlen("HTTP/1."); + if ((p[0] != '1') && (p[0] != '0')) { + ASYNC_HTTP_DEBUG("err | not http/1.1 or http/1.0 c=%c\n", p[0]); + client->close(true); + return; + } + + p += 2; // ' ' + char buf[4] = { + p[0], p[1], p[2], '\0' + }; + ASYNC_HTTP_DEBUG("log | buf code=%s\n", buf); + + unsigned int code = atoi(buf); + if (http->on_status && !http->on_status(http, code)) { + ASYNC_HTTP_DEBUG("cb err | http code=%u\n", code); + return; + } + + http->state = AsyncHttp::state_t::HEADERS; + continue; + } + case AsyncHttp::state_t::HEADERS: + { + // TODO: for now, simply skip all headers and go directly to the body + p = strnstr(reinterpret_cast(response), "\r\n\r\n", len); + if (!p) { + ASYNC_HTTP_DEBUG("wait | headers not in first %u...\n", len); + return; + } + ASYNC_HTTP_DEBUG("ok | p=%p response=%p diff=%u len=%u\n", + p, response, (p - ((char*)response)), len + ); + size_t end = p - ((char*)response) + 4; + if (len - end > len) { + client->close(true); + return; + } + response = ((char*)response) + end; + len -= end; + http->state = AsyncHttp::state_t::BODY; + } + case AsyncHttp::state_t::BODY: + { + if (!len) { + ASYNC_HTTP_DEBUG("wait | len is 0\n"); + return; + } + ASYNC_HTTP_DEBUG("ok | body len %u!\n", len); + + if (http->on_body_recv) http->on_body_recv(http, (uint8_t*) response, len); + return; + } + } + + } while (http->state != AsyncHttp::state_t::NONE); + + } + + static void _onConnect(void* http_ptr, AsyncClient * client) { + AsyncHttp* http = static_cast(http_ptr); + + http->ts = millis(); + http->connected = true; + http->connecting = false; + + if (http->on_connected) http->on_connected(http); + + { + size_t data_len = 0; + if (http->cfg & HTTP_SEND) { + if (!http->on_body_send_prepare || !http->on_body_send_data) { + ASYNC_HTTP_DEBUG("err | no body_send_data/_prepare callbacks set\n"); + client->close(true); + return; + } + data_len = http->on_body_send_prepare(http, client); + if (!data_len) { + ASYNC_HTTP_DEBUG("xxx | chunked encoding not implemented!\n"); + client->close(true); + } + char data_buf[16]; + snprintf(data_buf, sizeof(data_buf), "%u", data_len); + http->headers.add(Headers::CONTENT_LENGTH, data_buf); + } + } + + { + // XXX: current path limit is 256 - 16 = 240 chars (including leading slash) + char buf[DEFAULT_PATH_BUFSIZE] = {0}; + int res = snprintf_P( + buf, sizeof(buf), PSTR("%s %s HTTP/1.1\r\n"), + http->method.c_str(), http->path.c_str() + ); + + if ((res < 0) || (static_cast(res) > sizeof(buf))) { + ASYNC_HTTP_DEBUG("err | could not print initial line\n"); + client->close(true); + return; + } + + client->add(buf, res); + } + + if (http->trySendHeaders()) { + if (http->cfg & HTTP_SEND) http->trySend(); + } + } + + static void _onError(void* http_ptr, AsyncClient* client, err_t err) { + AsyncHttp* http = static_cast(http_ptr); + if (http->on_error) http->on_error(http, {AsyncHttpError::CLIENT_ERROR, client->errorToString(err)}); + } + + static void _onAck(void* http_ptr, AsyncClient* client, size_t, uint32_t) { + AsyncHttp* http = static_cast(http_ptr); + http->ts = millis(); + if (http->trySendHeaders()) { + if (http->cfg & HTTP_SEND) http->trySend(); + } + } + + + public: + AsyncHttp() + { + client.onDisconnect(_onDisconnect, this); + client.onTimeout(_onTimeout, this); + client.onPoll(_onPoll, this); + client.onData(_onData, this); + client.onConnect(_onConnect, this); + client.onError(_onError, this); + client.onAck(_onAck, this); + } + ~AsyncHttp() = default; + + bool busy() { + return connecting || connected; + } + + bool connect(const char* method, const char* host, uint16_t port, const char* path, bool use_ssl = false) { + + this->method = method; + this->host = host; + this->port = port; + + // XXX: current path limit is 256 - 16 = 240 chars (including leading slash) + this->path = path; + if (!this->path.length() || (this->path[0] != '/')) { + ASYNC_HTTP_DEBUG("err | empty path / no leading slash\n"); + return false; + } + + if (this->path.length() > (DEFAULT_PATH_BUFSIZE - 1)) { + ASYNC_HTTP_DEBUG("err | cannot handle path larger than %u\n", DEFAULT_PATH_BUFSIZE - 1); + return false; + } + + this->ts = millis(); + + // Treat every method as GET (receive-only), exception for POST / PUT to send data out + size_t headers_size = 3; + this->cfg = HTTP_RECV; + if (this->method.equals("POST") || this->method.equals("PUT")) { + if (!this->on_body_send_prepare) { + ASYNC_HTTP_DEBUG("err | on_body_send_prepare is required for POST / PUT requests\n"); + return false; + } + if (!this->on_body_send_data) { + ASYNC_HTTP_DEBUG("err | on_body_send_data is required for POST / PUT requests\n"); + return false; + } + this->cfg = HTTP_SEND | HTTP_RECV; + headers_size += 2; + } + + headers.reserve(headers_size); + headers.clear(); + + headers.add(Headers::HOST, this->host.c_str()); + headers.add(Headers::USER_AGENT, PSTR("ESPurna")); + headers.add(Headers::CONNECTION, PSTR("close")); + + bool status = false; + #if ASYNC_TCP_SSL_ENABLED + status = client.connect(this->host.c_str(), this->port, use_ssl); + #else + status = client.connect(this->host.c_str(), this->port); + #endif + + this->connecting = status; + + if (!status) { + client.close(true); + } + + return status; + } + +}; diff --git a/code/espurna/libs/URL.h b/code/espurna/libs/URL.h index a51e54fd39..7d0f69ba1d 100644 --- a/code/espurna/libs/URL.h +++ b/code/espurna/libs/URL.h @@ -30,6 +30,8 @@ void URL::init(String url) { if (index > 0) { this->protocol = url.substring(0, index); url.remove(0, (index + 3)); + } else { + this->protocol = "http"; } if (this->protocol == "http") { diff --git a/code/espurna/ota_arduinoota.ino b/code/espurna/ota_arduinoota.ino index 78664421aa..2b6721e908 100644 --- a/code/espurna/ota_arduinoota.ino +++ b/code/espurna/ota_arduinoota.ino @@ -30,6 +30,10 @@ void _arduinoOtaOnStart() { // Disabling EEPROM rotation to prevent writing to EEPROM after the upgrade eepromRotate(false); + // Avoid triggering wdt on write operations, allow implicit yield() calls + // (async mode could be enabled when using ota_base methods, currently used by web and asynctcp ota) + Update.runAsync(false); + // Because ArduinoOTA is synchronous, force backup right now instead of waiting for the next loop() eepromBackup(0); @@ -55,7 +59,7 @@ void _arduinoOtaOnEnd() { void _arduinoOtaOnProgress(unsigned int progress, unsigned int total) { // Removed to avoid websocket ping back during upgrade (see #1574) - // TODO: implement as separate from debugging message + // TODO: implement as percentage progress message, separate from debug log? #if WEB_SUPPORT if (wsConnected()) return; #endif diff --git a/code/espurna/ota_asynctcp.ino b/code/espurna/ota_asynctcp.ino index e01ca23625..11c264bd60 100644 --- a/code/espurna/ota_asynctcp.ino +++ b/code/espurna/ota_asynctcp.ino @@ -15,97 +15,75 @@ Copyright (C) 2016-2019 by Xose Pérez #if TERMINAL_SUPPORT || OTA_MQTT_SUPPORT #include +#include "libs/Http.h" #include "libs/URL.h" +#include "ota_base.h" -std::unique_ptr _ota_client = nullptr; +std::unique_ptr _ota_client = nullptr; unsigned long _ota_size = 0; -bool _ota_connected = false; -std::unique_ptr _ota_url = nullptr; -const char OTA_REQUEST_TEMPLATE[] PROGMEM = - "GET %s HTTP/1.1\r\n" - "Host: %s\r\n" - "User-Agent: ESPurna\r\n" - "Connection: close\r\n" - "Content-Type: application/x-www-form-urlencoded\r\n" - "Content-Length: 0\r\n\r\n\r\n"; - -void _otaClientOnDisconnect(void *s, AsyncClient *c) { +void _otaClientOnDisconnect(AsyncHttp* http) { DEBUG_MSG_P(PSTR("\n")); - if (Update.end(true)){ - DEBUG_MSG_P(PSTR("[OTA] Success: %u bytes\n"), _ota_size); - deferredReset(100, CUSTOM_RESET_OTA); - } else { - #ifdef DEBUG_PORT - Update.printError(DEBUG_PORT); - #endif - eepromRotate(true); - } + otaEnd(_ota_size, CUSTOM_RESET_OTA); DEBUG_MSG_P(PSTR("[OTA] Disconnected\n")); - _ota_connected = false; - _ota_url = nullptr; - _ota_client = nullptr; - } -void _otaClientOnTimeout(void *s, AsyncClient *c, uint32_t time) { - _ota_connected = false; - _ota_url = nullptr; - _ota_client->close(true); +void _otaOnError(AsyncHttp* http, const AsyncHttpError& error) { + DEBUG_MSG_P(PSTR("[OTA] %s\n"), error.data.c_str()); } -void _otaClientOnData(void * arg, AsyncClient * c, void * data, size_t len) { +bool _otaOnStatus(AsyncHttp* http, const unsigned int code) { + if (code == 200) return true; + + DEBUG_MSG_P(PSTR("[OTA] HTTP server response code %u\n"), code); + http->client.close(true); + return false; +} - char * p = (char *) data; +void _otaClientOnBody(AsyncHttp* http, uint8_t* data, size_t len) { if (_ota_size == 0) { - Update.runAsync(true); - if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) { - #ifdef DEBUG_PORT - Update.printError(DEBUG_PORT); - #endif - c->close(true); + if (!otaBegin()) { + http->client.close(true); return; } - p = strstr((char *)data, "\r\n\r\n") + 4; - len = len - (p - (char *) data); - } - if (!Update.hasError()) { - if (Update.write((uint8_t *) p, len) != len) { - #ifdef DEBUG_PORT - Update.printError(DEBUG_PORT); - #endif - c->close(true); - return; - } + if (!otaWrite(data, len)) { + http->client.close(true); + return; } _ota_size += len; - DEBUG_MSG_P(PSTR("[OTA] Progress: %u bytes\r"), _ota_size); + + // Removed to avoid websocket ping back during upgrade (see #1574) + // TODO: implement as percentage progress message, separate from debug log? + #if WEB_SUPPORT + if (!wsConnected()) + #endif + otaDebugProgress(_ota_size); delay(0); } -void _otaClientOnConnect(void *arg, AsyncClient *client) { +void _otaClientOnConnect(AsyncHttp* http) { #if ASYNC_TCP_SSL_ENABLED int check = getSetting("otaScCheck", OTA_SECURE_CLIENT_CHECK).toInt(); - if ((check == SECURE_CLIENT_CHECK_FINGERPRINT) && (443 == _ota_url->port)) { + if ((check == SECURE_CLIENT_CHECK_FINGERPRINT) && (443 == http->port)) { uint8_t fp[20] = {0}; sslFingerPrintArray(getSetting("otafp", OTA_FINGERPRINT).c_str(), fp); - SSL * ssl = _ota_client->getSSL(); + SSL * ssl = http->client.getSSL(); if (ssl_match_fingerprint(ssl, fp) != SSL_OK) { DEBUG_MSG_P(PSTR("[OTA] Warning: certificate fingerpint doesn't match\n")); - client->close(true); + http->client.close(true); return; } } @@ -114,58 +92,44 @@ void _otaClientOnConnect(void *arg, AsyncClient *client) { // Disabling EEPROM rotation to prevent writing to EEPROM after the upgrade eepromRotate(false); - DEBUG_MSG_P(PSTR("[OTA] Downloading %s\n"), _ota_url->path.c_str()); - char buffer[strlen_P(OTA_REQUEST_TEMPLATE) + _ota_url->path.length() + _ota_url->host.length()]; - snprintf_P(buffer, sizeof(buffer), OTA_REQUEST_TEMPLATE, _ota_url->path.c_str(), _ota_url->host.c_str()); - client->write(buffer); + DEBUG_MSG_P(PSTR("[OTA] Downloading %s\n"), http->path.c_str()); } void _otaClientFrom(const String& url) { - if (_ota_connected) { + if (_ota_client && _ota_client->connected) { DEBUG_MSG_P(PSTR("[OTA] Already connected\n")); return; } - _ota_size = 0; - - if (_ota_url) _ota_url = nullptr; - _ota_url = std::make_unique(url); - /* - DEBUG_MSG_P(PSTR("[OTA] proto:%s host:%s port:%u path:%s\n"), - _ota_url->protocol.c_str(), - _ota_url->host.c_str(), - _ota_url->port, - _ota_url->path.c_str() - ); - */ + URL ota_url(url); // we only support HTTP - if ((!_ota_url->protocol.equals("http")) && (!_ota_url->protocol.equals("https"))) { + if ((!ota_url.protocol.equals("http")) && (!ota_url.protocol.equals("https"))) { DEBUG_MSG_P(PSTR("[OTA] Incorrect URL specified\n")); - _ota_url = nullptr; return; } if (!_ota_client) { - _ota_client = std::make_unique(); - } + _ota_client = std::make_unique(); + _ota_client->on_connected = _otaClientOnConnect; + _ota_client->on_disconnected = _otaClientOnDisconnect; + + _ota_client->on_status = _otaOnStatus; + _ota_client->on_error = _otaOnError; - _ota_client->onDisconnect(_otaClientOnDisconnect, nullptr); - _ota_client->onTimeout(_otaClientOnTimeout, nullptr); - _ota_client->onData(_otaClientOnData, nullptr); - _ota_client->onConnect(_otaClientOnConnect, nullptr); + _ota_client->on_body_recv = _otaClientOnBody; + } #if ASYNC_TCP_SSL_ENABLED - _ota_connected = _ota_client->connect(_ota_url->host.c_str(), _ota_url->port, 443 == _ota_url->port); + bool connected = _ota_client->connect("GET", ota_url.host.c_str(), ota_url.port, ota_url.path.c_str(), 443 == ota_url.port); #else - _ota_connected = _ota_client->connect(_ota_url->host.c_str(), _ota_url->port); + bool connected = _ota_client->connect("GET", ota_url.host.c_str(), ota_url.port, ota_url.path.c_str()); #endif - if (!_ota_connected) { + if (!connected) { DEBUG_MSG_P(PSTR("[OTA] Connection failed\n")); - _ota_url = nullptr; - _ota_client->close(true); + _ota_client->client.close(true); } } diff --git a/code/espurna/ota_base.h b/code/espurna/ota_base.h new file mode 100644 index 0000000000..811c4b83fc --- /dev/null +++ b/code/espurna/ota_base.h @@ -0,0 +1,8 @@ +#pragma once + +void otaDebugProgress(unsigned int bytes); +void otaDebugError(); + +bool otaBegin(); +bool otaWrite(uint8_t* data, size_t len); +bool otaEnd(size_t size, unsigned char reset_reason = 0); diff --git a/code/espurna/ota_base.ino b/code/espurna/ota_base.ino new file mode 100644 index 0000000000..d8e520fc3d --- /dev/null +++ b/code/espurna/ota_base.ino @@ -0,0 +1,62 @@ +/* + +OTA base functions + +Copyright (C) 2016-2019 by Xose Pérez + +*/ + +#include +#include + +void otaDebugProgress(unsigned int bytes) { + DEBUG_MSG_P(PSTR("[UPGRADE] Progress: %u bytes\r"), bytes); +} + +void otaDebugError() { + StreamString out; + out.reserve(48); + Update.printError(out); + DEBUG_MSG_P(PSTR("[OTA] Updater error: %s\n"), out.c_str()); +} + +bool otaBegin() { + // Disabling EEPROM rotation to prevent writing to EEPROM after the upgrade + eepromRotate(false); + + // Disabling implicit yield() within UpdaterClass write operations + Update.runAsync(true); + + // TODO: use ledPin and ledOn, disable led module temporarily + const bool result = Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000); + if (!result) otaDebugError(); + + return result; +} + +bool otaWrite(uint8_t* data, size_t len) { + bool result = false; + + if (!Update.hasError()) { + result = (Update.write(data, len) == len); + if (!result) otaDebugError(); + } + + return result; +} + + +bool otaEnd(size_t size, unsigned char reset_reason) { + const bool result = Update.end(true); + + if (result) { + DEBUG_MSG_P(PSTR("[OTA] Success: %u bytes\n"), size); + if (reset_reason) deferredReset(100, reset_reason); + } else { + DEBUG_MSG_P(PSTR("[OTA] Error: %u bytes\n"), size); + otaDebugError(); + eepromRotate(true); + } + + return result; +} diff --git a/code/espurna/thinkspeak.ino b/code/espurna/thinkspeak.ino index c53cb30396..17d47d4771 100644 --- a/code/espurna/thinkspeak.ino +++ b/code/espurna/thinkspeak.ino @@ -10,38 +10,39 @@ Copyright (C) 2019 by Xose Pérez #if THINGSPEAK_USE_ASYNC #include +#include "libs/Http.h" #else #include #endif #define THINGSPEAK_DATA_BUFFER_SIZE 256 -const char THINGSPEAK_REQUEST_TEMPLATE[] PROGMEM = - "POST %s HTTP/1.1\r\n" - "Host: %s\r\n" - "User-Agent: ESPurna\r\n" - "Connection: close\r\n" - "Content-Type: application/x-www-form-urlencoded\r\n" - "Content-Length: %d\r\n\r\n"; - bool _tspk_enabled = false; bool _tspk_clear = false; -char * _tspk_queue[THINGSPEAK_FIELDS] = {NULL}; -String _tspk_data; - -bool _tspk_flush = false; -unsigned long _tspk_last_flush = 0; -unsigned char _tspk_tries = THINGSPEAK_TRIES; - -#if THINGSPEAK_USE_ASYNC -AsyncClient * _tspk_client; -bool _tspk_connecting = false; -bool _tspk_connected = false; -#endif +std::vector _tspk_queue; +struct tspk_state_t { + bool flush = false; + bool sent = false; + unsigned long last_flush = 0; + unsigned char tries = THINGSPEAK_TRIES; +} _tspk_state; // ----------------------------------------------------------------------------- +String _tspkPrepareData(const std::vector& fields) { + String result; + result.reserve(128); + for (const auto& field : fields) { + if (!field.length()) continue; + result.concat(field); + result += '&'; + } + result.concat("apikey="); + result.concat(getSetting("tspkKey", THINGSPEAK_APIKEY).c_str()); + return result; +} + #if BROKER_SUPPORT void _tspkBrokerCallback(const unsigned char type, const char * topic, unsigned char id, const char * payload) { @@ -90,156 +91,131 @@ void _tspkWebSocketOnConnected(JsonObject& root) { #endif -void _tspkConfigure() { - _tspk_clear = getSetting("tspkClear", THINGSPEAK_CLEAR_CACHE).toInt() == 1; - _tspk_enabled = getSetting("tspkEnabled", THINGSPEAK_ENABLED).toInt() == 1; - if (_tspk_enabled && (getSetting("tspkKey").length() == 0)) { - _tspk_enabled = false; - setSetting("tspkEnabled", 0); - } - if (_tspk_enabled && !_tspk_client) _tspkInitClient(); -} - #if THINGSPEAK_USE_ASYNC -enum class tspk_state_t : uint8_t { - NONE, - HEADERS, - BODY -}; +AsyncHttp* _tspk_client = nullptr; +String _tspk_data; -tspk_state_t _tspk_client_state = tspk_state_t::NONE; -unsigned long _tspk_client_ts = 0; -constexpr const unsigned long THINGSPEAK_CLIENT_TIMEOUT = 5000; +void _tspkFlushAgain() { + DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-enqueuing %u more time(s)\n"), _tspk_state.tries); + _tspk_state.flush = true; +} -void _tspkInitClient() { +// TODO: maybe http object can keep a context containing the data +// however, it should not be restricted to string datatype - _tspk_client = new AsyncClient(); +size_t _tspkOnBodySendPrepare(AsyncHttp* http, AsyncClient* client) { + http->headers.add(Headers::CONTENT_TYPE, PSTR("application/x-www-form-urlencoded")); + _tspk_data = _tspkPrepareData(_tspk_queue); + return _tspk_data.length(); +} - _tspk_client->onDisconnect([](void * s, AsyncClient * client) { - DEBUG_MSG_P(PSTR("[THINGSPEAK] Disconnected\n")); +size_t _tspkOnBodySend(AsyncHttp* http, AsyncClient* client) { + const size_t data_len = _tspk_data.length(); + if (!data_len || (client->space() < data_len)) { + return 0; + } + + if (data_len == client->add(_tspk_data.c_str(), data_len)) { + DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), http->path.c_str(), _tspk_data.c_str()); + client->send(); _tspk_data = ""; - _tspk_client_ts = 0; - _tspk_last_flush = millis(); - _tspk_connected = false; - _tspk_connecting = false; - _tspk_client_state = tspk_state_t::NONE; - }, nullptr); - - _tspk_client->onTimeout([](void * s, AsyncClient * client, uint32_t time) { - DEBUG_MSG_P(PSTR("[THINGSPEAK] Network timeout after %ums\n"), time); - client->close(true); - }, nullptr); - - _tspk_client->onPoll([](void * s, AsyncClient * client) { - uint32_t ts = millis() - _tspk_client_ts; - if (ts > THINGSPEAK_CLIENT_TIMEOUT) { - DEBUG_MSG_P(PSTR("[THINGSPEAK] No response after %ums\n"), ts); - client->close(true); - } - }, nullptr); + } - _tspk_client->onData([](void * arg, AsyncClient * client, void * response, size_t len) { + return data_len; +} - char * p = nullptr; +void _tspkOnBodyRecv(AsyncHttp* http, uint8_t* data, size_t len) { - do { + unsigned int code = 0; + if (len) { + char buf[16] = {0}; + len = std::min(len, sizeof(buf) - 1); + memcpy(buf, data, len); + buf[len] = '\0'; + code = atoi(buf); + } - p = nullptr; + DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %u\n"), code); - switch (_tspk_client_state) { - case tspk_state_t::NONE: - { - p = strnstr(reinterpret_cast(response), "HTTP/1.1 200 OK", len); - if (!p) { - client->close(true); - return; - } - _tspk_client_state = tspk_state_t::HEADERS; - continue; - } - case tspk_state_t::HEADERS: - { - p = strnstr(reinterpret_cast(response), "\r\n\r\n", len); - if (!p) return; - _tspk_client_state = tspk_state_t::BODY; - } - case tspk_state_t::BODY: - { - if (!p) { - p = strnstr(reinterpret_cast(response), "\r\n\r\n", len); - if (!p) return; - } + if (0 != code) { + _tspk_state.sent = true; + _tspkClearQueue(); + } +} - unsigned int code = (p) ? atoi(&p[4]) : 0; - DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %u\n"), code); +void _tspkOnDisconnected(AsyncHttp* http) { + DEBUG_MSG_P(PSTR("[THINGSPEAK] Disconnected\n")); + _tspk_state.last_flush = millis(); + if (!_tspk_state.sent && _tspk_state.tries) { + _tspkFlushAgain(); + } else { + _tspkClearQueue(); + } + if (_tspk_state.tries) --_tspk_state.tries; +} - if ((0 == code) && _tspk_tries) { - _tspk_flush = true; - DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-enqueuing %u more time(s)\n"), _tspk_tries); - } else { - _tspkClearQueue(); - } +bool _tspkOnStatus(AsyncHttp* http, const unsigned int code) { + if (code == 200) return true; - client->close(true); + DEBUG_MSG_P(PSTR("[THINGSPEAK] HTTP server response code %u\n"), code); + http->client.close(true); + return false; +} - _tspk_client_state = tspk_state_t::NONE; - } - } +void _tspkOnError(AsyncHttp* http, const AsyncHttpError& error) { + DEBUG_MSG_P(PSTR("[THINGSPEAK] %s\n"), error.data.c_str()); +} - } while (_tspk_client_state != tspk_state_t::NONE); +void _tspkOnConnected(AsyncHttp* http) { + DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%u\n"), http->host.c_str(), http->port); - }, nullptr); + #if THINGSPEAK_USE_SSL + { + uint8_t fp[20] = {0}; + sslFingerPrintArray(THINGSPEAK_FINGERPRINT, fp); + SSL * ssl = AsyncHttp->client.getSSL(); + if (ssl_match_fingerprint(ssl, fp) != SSL_OK) { + DEBUG_MSG_P(PSTR("[THINGSPEAK] Warning: certificate fingerpint doesn't match\n")); + http->client.close(true); + return; + } + } + #endif +} - _tspk_client->onConnect([](void * arg, AsyncClient * client) { +constexpr const unsigned long THINGSPEAK_CLIENT_TIMEOUT = 5000; - _tspk_connected = true; - _tspk_connecting = false; +void _tspkInitClient() { - DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%u\n"), THINGSPEAK_HOST, THINGSPEAK_PORT); + _tspk_client = new AsyncHttp(); - #if THINGSPEAK_USE_SSL - uint8_t fp[20] = {0}; - sslFingerPrintArray(THINGSPEAK_FINGERPRINT, fp); - SSL * ssl = _tspk_client->getSSL(); - if (ssl_match_fingerprint(ssl, fp) != SSL_OK) { - DEBUG_MSG_P(PSTR("[THINGSPEAK] Warning: certificate doesn't match\n")); - } - #endif - - DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), THINGSPEAK_URL, _tspk_data.c_str()); - char headers[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + strlen(THINGSPEAK_URL) + strlen(THINGSPEAK_HOST) + 1]; - snprintf_P(headers, sizeof(headers), - THINGSPEAK_REQUEST_TEMPLATE, - THINGSPEAK_URL, - THINGSPEAK_HOST, - _tspk_data.length() - ); + _tspk_client->on_connected = _tspkOnConnected; + _tspk_client->on_disconnected = _tspkOnDisconnected; + + _tspk_client->timeout = THINGSPEAK_CLIENT_TIMEOUT; + _tspk_client->on_status = _tspkOnStatus; + _tspk_client->on_error = _tspkOnError; - client->write(headers); - client->write(_tspk_data.c_str()); + _tspk_client->on_body_recv = _tspkOnBodyRecv; - }, nullptr); + _tspk_client->on_body_send_prepare = _tspkOnBodySendPrepare; + _tspk_client->on_body_send_data = _tspkOnBodySend; } void _tspkPost() { - if (_tspk_connected || _tspk_connecting) return; - - _tspk_client_ts = millis(); + if (_tspk_client->busy()) return; #if SECURE_CLIENT == SECURE_CLIENT_AXTLS - bool connected = _tspk_client->connect(THINGSPEAK_HOST, THINGSPEAK_PORT, THINGSPEAK_USE_SSL); + bool connected = _tspk_client->connect("POST", THINGSPEAK_HOST, THINGSPEAK_PORT, THINGSPEAK_URL, THINGSPEAK_USE_SSL); #else - bool connected = _tspk_client->connect(THINGSPEAK_HOST, THINGSPEAK_PORT); + bool connected = _tspk_client->connect("POST", THINGSPEAK_HOST, THINGSPEAK_PORT, THINGSPEAK_URL); #endif - _tspk_connecting = connected; - if (!connected) { DEBUG_MSG_P(PSTR("[THINGSPEAK] Connection failed\n")); - _tspk_client->close(true); } } @@ -262,17 +238,17 @@ void _tspkPost() { DEBUG_MSG_P(PSTR("[THINGSPEAK] Warning: certificate doesn't match\n")); } - DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), THINGSPEAK_URL, _tspk_data.c_str()); - char headers[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + strlen(THINGSPEAK_URL) + strlen(THINGSPEAK_HOST) + 1]; + const String data = _tspkPrepareData(_tspk_queue); + char headers[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + strlen(THINGSPEAK_URL) + strlen(THINGSPEAK_HOST) + 32]; snprintf_P(headers, sizeof(headers), THINGSPEAK_REQUEST_TEMPLATE, THINGSPEAK_URL, THINGSPEAK_HOST, - _tspk_data.length() + data.length() ); _tspk_client.print(headers); - _tspk_client.print(_tspk_data); + _tspk_client.print(data); nice_delay(100); @@ -282,10 +258,10 @@ void _tspkPost() { DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %u\n"), code); _tspk_client.stop(); - _tspk_last_flush = millis(); - if ((0 == code) && _tspk_tries) { - _tspk_flush = true; - DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-enqueuing %u more time(s)\n"), _tspk_tries); + _tspk_state.last_flush = millis(); + if ((0 == code) && _tspk_state.tries) { + _tspkFlushAgain(); + --_tspk_state.tries; } else { _tspkClearQueue(); } @@ -300,51 +276,59 @@ void _tspkPost() { #endif // THINGSPEAK_USE_ASYNC +void _tspkConfigure() { + _tspk_clear = getSetting("tspkClear", THINGSPEAK_CLEAR_CACHE).toInt() == 1; + _tspk_enabled = getSetting("tspkEnabled", THINGSPEAK_ENABLED).toInt() == 1; + if (_tspk_enabled && (getSetting("tspkKey", THINGSPEAK_APIKEY).length() == 0)) { + _tspk_enabled = false; + setSetting("tspkEnabled", 0); + } + if (_tspk_enabled && !_tspk_client) _tspkInitClient(); +} + + void _tspkEnqueue(unsigned char index, const char * payload) { DEBUG_MSG_P(PSTR("[THINGSPEAK] Enqueuing field #%u with value %s\n"), index, payload); - --index; - if (_tspk_queue[index] != NULL) free(_tspk_queue[index]); - _tspk_queue[index] = strdup(payload); + _tspk_state.tries = THINGSPEAK_TRIES; + + String elem; + elem.reserve(8 + strlen(payload)); + elem += "field"; + elem += int(index + 1); + elem += '='; + elem += payload; + _tspk_queue[--index] = std::move(elem); + } void _tspkClearQueue() { - _tspk_tries = THINGSPEAK_TRIES; + _tspk_state.tries = THINGSPEAK_TRIES; if (_tspk_clear) { - for (unsigned char id=0; id 0) _tspk_data.concat("&"); - char buf[32] = {0}; - snprintf_P(buf, sizeof(buf), PSTR("field%u=%s"), (id + 1), _tspk_queue[id]); - _tspk_data.concat(buf); - } - } + if (!_tspk_state.flush) return; + if (millis() - _tspk_state.last_flush < THINGSPEAK_MIN_INTERVAL) return; + + #if THINGSPEAK_USE_ASYNC + if (_tspk_client->busy()) return; + #endif + + _tspk_state.last_flush = millis(); + _tspk_state.sent = false; + _tspk_state.flush = false; // POST data if any - if (_tspk_data.length()) { - _tspk_data.concat("&api_key="); - _tspk_data.concat(getSetting("tspkKey")); - --_tspk_tries; - _tspkPost(); + for (const auto& elem : _tspk_queue) { + if (elem.length()) { + _tspkPost(); + break; + } } } @@ -372,7 +356,7 @@ bool tspkEnqueueMeasurement(unsigned char index, const char * payload) { } void tspkFlush() { - _tspk_flush = true; + _tspk_state.flush = true; } bool tspkEnabled() { @@ -381,6 +365,8 @@ bool tspkEnabled() { void tspkSetup() { + _tspk_queue.resize(THINGSPEAK_FIELDS, String()); + _tspkConfigure(); #if WEB_SUPPORT diff --git a/code/espurna/web.ino b/code/espurna/web.ino index 3c80fd2cc5..78ef714fd3 100644 --- a/code/espurna/web.ino +++ b/code/espurna/web.ino @@ -42,6 +42,8 @@ Copyright (C) 2016-2019 by Xose Pérez #include "static/server.key.h" #endif // SECURE_CLIENT == SECURE_CLIENT_AXTLS & WEB_SSL_ENABLED +#include "ota_base.h" + // ----------------------------------------------------------------------------- AsyncWebServer * _server; @@ -319,41 +321,28 @@ void _onUpgradeFile(AsyncWebServerRequest *request, String filename, size_t inde } if (!index) { - - // Disabling EEPROM rotation to prevent writing to EEPROM after the upgrade - eepromRotate(false); - - DEBUG_MSG_P(PSTR("[UPGRADE] Start: %s\n"), filename.c_str()); - Update.runAsync(true); - if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) { - #ifdef DEBUG_PORT - Update.printError(DEBUG_PORT); - #endif + if (!otaBegin()) { + DEBUG_MSG_P(PSTR("[OTA] Start: %s\n"), filename.c_str()); + request->send(500); + return; } - } - if (!Update.hasError()) { - if (Update.write(data, len) != len) { - #ifdef DEBUG_PORT - Update.printError(DEBUG_PORT); - #endif - } + if (!otaWrite(data, len)) { + request->send(500); + return; } if (final) { - if (Update.end(true)){ - DEBUG_MSG_P(PSTR("[UPGRADE] Success: %u bytes\n"), index + len); - } else { - #ifdef DEBUG_PORT - Update.printError(DEBUG_PORT); - #endif + if (!otaEnd(index + len)) { + request->send(500); + return; } } else { // Removed to avoid websocket ping back during upgrade (see #1574) - // TODO: implement as separate from debugging message + // TODO: implement as percentage progress message, separate from debug log? if (wsConnected()) return; - DEBUG_MSG_P(PSTR("[UPGRADE] Progress: %u bytes\r"), index + len); + otaDebugProgress(index + len); } } diff --git a/code/platformio.ini b/code/platformio.ini index 7975140ff5..c369428b83 100644 --- a/code/platformio.ini +++ b/code/platformio.ini @@ -243,8 +243,8 @@ upload_flags = ${common.ota_upload_flags} board = ${common.board_4m} build_flags = ${common.build_flags_4m1m} -DNODEMCU_LOLIN -DDEBUG_FAUXMO=Serial -DNOWSAUTH -[env:nodemcu-lolin-252] -platform = ${common.arduino_core_2_5_2} +[env:nodemcu-lolin-git] +platform = ${common.arduino_core_git} board = ${common.board_4m} build_flags = ${common.build_flags_4m1m} -DNODEMCU_LOLIN -DDEBUG_FAUXMO=Serial -DNOWSAUTH