From a93f254eef454f286fe1ff3817f3f15c9083c1f5 Mon Sep 17 00:00:00 2001 From: Max Prokhorov Date: Tue, 20 Aug 2019 01:12:09 +0300 Subject: [PATCH 01/13] util: make tspk http client a generic class --- code/espurna/config/prototypes.h | 6 + code/espurna/libs/Http.h | 310 ++++++++++++++++++++++++++++++ code/espurna/ota_asynctcp.ino | 95 ++++------ code/espurna/thinkspeak.ino | 311 +++++++++++++------------------ 4 files changed, 486 insertions(+), 236 deletions(-) create mode 100644 code/espurna/libs/Http.h diff --git a/code/espurna/config/prototypes.h b/code/espurna/config/prototypes.h index 265a14f6c6..f410b3b69d 100644 --- a/code/espurna/config/prototypes.h +++ b/code/espurna/config/prototypes.h @@ -325,6 +325,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..5007f8f437 --- /dev/null +++ b/code/espurna/libs/Http.h @@ -0,0 +1,310 @@ + +#pragma once + +#include +#include + +#ifndef ASYNC_HTTP_DEBUG +#define ASYNC_HTTP_DEBUG(...) //DEBUG_PORT.printf(__VA_ARGS__) +#endif + +// +const char HTTP_REQUEST_TEMPLATE[] PROGMEM = + "%s %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: %u\r\n" + "\r\n"; + +struct AsyncHttpError { + + 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 { + + public: + + AsyncClient client; + + 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_f = std::function; + + 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_f on_body; + + String method; + String path; + + String host; + uint16_t port; + + String data; // TODO: generic data source, feed chunks of (bytes, len) and call us back when done + + uint32_t ts; + uint32_t timeout = 5000; + + bool connected = false; + bool connecting = false; + + protected: + + static AsyncHttpError _timeoutError(AsyncHttpError::error_t error, const __FlashStringHelper* message, uint32_t ts) { + String data; + data.reserve(32); + data += 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->data = ""; + 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, F("Network timeout after"), time)); + // TODO: close connection when acks are missing? + } + + static void _onPoll(void* http_ptr, AsyncClient*) { + 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, F("No response after"), diff)); + http->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) http->on_body(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); + + const int headers_len = + strlen_P(HTTP_REQUEST_TEMPLATE) + + http->method.length() + + http->host.length() + + http->path.length() + + 32; + char* headers = (char *) malloc(headers_len + 1); + + if (!headers) { + ASYNC_HTTP_DEBUG("err | alloc %u fail\n", headers_len + 1); + client->close(true); + return; + } + + int res = snprintf_P(headers, headers_len + 1, + HTTP_REQUEST_TEMPLATE, + http->method.c_str(), + http->path.c_str(), + http->host.c_str(), + http->data.length() + ); + if (res >= (headers_len + 1)) { + ASYNC_HTTP_DEBUG("err | res>=len :: %u>=%u\n", res, headers_len + 1); + free(headers); + client->close(true); + return; + } + + client->write(headers); + free(headers); + // TODO: streaming data source instead of using a simple String + // TODO: move to onPoll, ->add(data) and ->send() until it can't (returns 0), then repeat + client->write(http->data.c_str()); + + } + + 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)}); + } + + 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); + } + ~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; + this->path = path; + this->ts = millis(); + + 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/ota_asynctcp.ino b/code/espurna/ota_asynctcp.ino index e01ca23625..276983a593 100644 --- a/code/espurna/ota_asynctcp.ino +++ b/code/espurna/ota_asynctcp.ino @@ -15,22 +15,15 @@ Copyright (C) 2016-2019 by Xose Pérez #if TERMINAL_SUPPORT || OTA_MQTT_SUPPORT #include +#include "libs/Http.h" #include "libs/URL.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; +//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")); @@ -45,22 +38,23 @@ void _otaClientOnDisconnect(void *s, AsyncClient *c) { } 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) { @@ -69,21 +63,18 @@ void _otaClientOnData(void * arg, AsyncClient * c, void * data, size_t len) { #ifdef DEBUG_PORT Update.printError(DEBUG_PORT); #endif - c->close(true); + 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) { + if (Update.write(data, len) != len) { #ifdef DEBUG_PORT Update.printError(DEBUG_PORT); #endif - c->close(true); + http->client.close(true); return; } } @@ -95,17 +86,17 @@ void _otaClientOnData(void * arg, AsyncClient * c, void * data, size_t len) { } -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,10 +105,7 @@ 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) { @@ -127,45 +115,34 @@ void _otaClientFrom(const String& url) { 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->onDisconnect(_otaClientOnDisconnect, nullptr); - _ota_client->onTimeout(_otaClientOnTimeout, nullptr); - _ota_client->onData(_otaClientOnData, nullptr); - _ota_client->onConnect(_otaClientOnConnect, nullptr); + _ota_client->on_status = _otaOnStatus; + _ota_client->on_error = _otaOnError; + + _ota_client->on_body = _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/thinkspeak.ino b/code/espurna/thinkspeak.ino index c53cb30396..677235a821 100644 --- a/code/espurna/thinkspeak.ino +++ b/code/espurna/thinkspeak.ino @@ -8,6 +8,8 @@ Copyright (C) 2019 by Xose Pérez #if THINGSPEAK_SUPPORT +#include "libs/Http.h" + #if THINGSPEAK_USE_ASYNC #include #else @@ -16,32 +18,32 @@ Copyright (C) 2019 by Xose Pérez #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 = 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 +92,104 @@ 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 -}; - -tspk_state_t _tspk_client_state = tspk_state_t::NONE; -unsigned long _tspk_client_ts = 0; -constexpr const unsigned long THINGSPEAK_CLIENT_TIMEOUT = 5000; +AsyncHttp* _tspk_client = nullptr; -void _tspkInitClient() { +void _tspkFlushAgain() { + DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-enqueuing %u more time(s)\n"), _tspk_state.tries); + _tspk_state.flush = true; +} - _tspk_client = new AsyncClient(); - - _tspk_client->onDisconnect([](void * s, AsyncClient * client) { - DEBUG_MSG_P(PSTR("[THINGSPEAK] Disconnected\n")); - _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); +void _tspkOnBody(AsyncHttp* http, uint8_t* data, size_t len) { - _tspk_client->onData([](void * arg, AsyncClient * client, void * response, size_t len) { + 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); + } - char * p = nullptr; + DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %u\n"), code); - do { + if (0 != code) { + _tspk_state.sent = true; + _tspkClearQueue(); + } +} - p = nullptr; +void _tspkOnDisconnected(AsyncHttp* http) { + DEBUG_MSG_P(PSTR("[THINGSPEAK] Disconnected\n")); + _tspk_state.last = millis(); + if (!_tspk_state.sent && _tspk_state.tries) _tspkFlushAgain(); + if (_tspk_state.tries) --_tspk_state.tries; +} - 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; - } +bool _tspkOnStatus(AsyncHttp* http, const unsigned int code) { + if (code == 200) return true; - unsigned int code = (p) ? atoi(&p[4]) : 0; - DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %u\n"), code); + DEBUG_MSG_P(PSTR("[THINGSPEAK] HTTP server response code %u\n"), code); + http->client.close(true); + return false; +} - if ((0 == code) && _tspk_tries) { - _tspk_flush = true; - DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-enqueuing %u more time(s)\n"), _tspk_tries); - } else { - _tspkClearQueue(); - } +void _tspkOnError(AsyncHttp* http, const AsyncHttpError& error) { + DEBUG_MSG_P(PSTR("[THINGSPEAK] %s\n"), error.data.c_str()); +} - client->close(true); +void _tspkOnConnected(AsyncHttp* http) { + DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%u\n"), http->host.c_str(), http->port); - _tspk_client_state = tspk_state_t::NONE; - } - } + #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 - } while (_tspk_client_state != tspk_state_t::NONE); + // Note: always replacing old data in case of retry + http->data = _tspkPrepareData(_tspk_queue); - }, nullptr); + DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), http->path.c_str(), http->data.c_str()); +} - _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; - client->write(headers); - client->write(_tspk_data.c_str()); + _tspk_client->timeout = THINGSPEAK_CLIENT_TIMEOUT; + _tspk_client->on_status = _tspkOnStatus; + _tspk_client->on_error = _tspkOnError; - }, nullptr); + _tspk_client->on_body = _tspkOnBody; } 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 +212,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 +232,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 = millis(); + if ((0 == code) && _tspk_state.tries) { + _tspkFlushAgain(); + --_tspk_state.tries; } else { _tspkClearQueue(); } @@ -300,51 +250,56 @@ 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); + String elem; + elem.reserve(8 + strlen(payload)); + elem += "field"; + elem += int(index + 1); + elem += '='; + elem += payload; + _tspk_queue[--index] = 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 < THINGSPEAK_MIN_INTERVAL) return; + + #if THINGSPEAK_USE_ASYNC + if (_tspk_client->busy()) return; + #endif + + _tspk_state.last = 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 +327,7 @@ bool tspkEnqueueMeasurement(unsigned char index, const char * payload) { } void tspkFlush() { - _tspk_flush = true; + _tspk_state.flush = true; } bool tspkEnabled() { @@ -381,6 +336,8 @@ bool tspkEnabled() { void tspkSetup() { + _tspk_queue.resize(THINGSPEAK_FIELDS, String()); + _tspkConfigure(); #if WEB_SUPPORT From a8feed6ffceb565189d2fee12f0db4b4a839ec20 Mon Sep 17 00:00:00 2001 From: Max Prokhorov Date: Wed, 18 Sep 2019 17:50:22 +0300 Subject: [PATCH 02/13] ota: move common functions into a module --- code/espurna/ota_asynctcp.ino | 41 ++++++++--------------- code/espurna/ota_base.h | 8 +++++ code/espurna/ota_base.ino | 62 +++++++++++++++++++++++++++++++++++ code/espurna/web.ino | 39 ++++++++-------------- 4 files changed, 98 insertions(+), 52 deletions(-) create mode 100644 code/espurna/ota_base.h create mode 100644 code/espurna/ota_base.ino diff --git a/code/espurna/ota_asynctcp.ino b/code/espurna/ota_asynctcp.ino index 276983a593..2bef50ac5b 100644 --- a/code/espurna/ota_asynctcp.ino +++ b/code/espurna/ota_asynctcp.ino @@ -17,28 +17,18 @@ Copyright (C) 2016-2019 by Xose Pérez #include #include "libs/Http.h" #include "libs/URL.h" +#include "ota_base.h" std::unique_ptr _ota_client = nullptr; unsigned long _ota_size = 0; -bool _ota_connected = false; -//std::unique_ptr _ota_url = nullptr; 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; } @@ -58,29 +48,26 @@ 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 + if (!otaBegin()) { http->client.close(true); return; } } - if (!Update.hasError()) { - if (Update.write(data, len) != len) { - #ifdef DEBUG_PORT - Update.printError(DEBUG_PORT); - #endif - http->client.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); @@ -110,7 +97,7 @@ void _otaClientOnConnect(AsyncHttp* http) { void _otaClientFrom(const String& url) { - if (_ota_connected) { + if (_ota_client && _ota_client->connected) { DEBUG_MSG_P(PSTR("[OTA] Already connected\n")); return; } 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/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); } } From ff85e9ff497703128c3cba01bea9413f6f40041b Mon Sep 17 00:00:00 2001 From: Max Prokhorov Date: Wed, 18 Sep 2019 17:51:00 +0300 Subject: [PATCH 03/13] util: parse schema-less urls as http --- code/espurna/libs/URL.h | 2 ++ 1 file changed, 2 insertions(+) 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") { From 12709118bfff32e71bc71000a01413cd14710b45 Mon Sep 17 00:00:00 2001 From: Max Prokhorov Date: Wed, 18 Sep 2019 17:51:38 +0300 Subject: [PATCH 04/13] ota: disable async mode when running arduino_ota --- code/espurna/ota_arduinoota.ino | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 From d9f481a60b1d91215f6cc993356d185bb2c5d63f Mon Sep 17 00:00:00 2001 From: Max Prokhorov Date: Wed, 18 Sep 2019 17:52:14 +0300 Subject: [PATCH 05/13] util: include Updater.h --- code/espurna/config/prototypes.h | 1 + 1 file changed, 1 insertion(+) diff --git a/code/espurna/config/prototypes.h b/code/espurna/config/prototypes.h index f410b3b69d..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 From c24e401fddc8eda008097a1af57352d0d44f2c66 Mon Sep 17 00:00:00 2001 From: Max Prokhorov Date: Thu, 19 Sep 2019 16:56:47 +0300 Subject: [PATCH 06/13] exprerimental: remove String from client, delegate data management to modules --- code/espurna/libs/Http.h | 68 ++++++++++++++++++++++++++++------- code/espurna/ota_asynctcp.ino | 2 +- code/espurna/thinkspeak.ino | 36 ++++++++++++++----- code/platformio.ini | 4 +-- 4 files changed, 85 insertions(+), 25 deletions(-) diff --git a/code/espurna/libs/Http.h b/code/espurna/libs/Http.h index 5007f8f437..277fe7576d 100644 --- a/code/espurna/libs/Http.h +++ b/code/espurna/libs/Http.h @@ -8,6 +8,7 @@ #define ASYNC_HTTP_DEBUG(...) //DEBUG_PORT.printf(__VA_ARGS__) #endif +// TODO: customizable headers // const char HTTP_REQUEST_TEMPLATE[] PROGMEM = "%s %s HTTP/1.1\r\n" @@ -56,6 +57,11 @@ class AsyncHttp { AsyncClient client; + enum cfg_t { + HTTP_SEND = 1 << 0, + HTTP_RECV = 1 << 1 + }; + enum class state_t : uint8_t { NONE, HEADERS, @@ -66,8 +72,11 @@ class AsyncHttp { using on_status_f = std::function; using on_disconnected_f = std::function; using on_error_f = std::function; - using on_body_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; @@ -77,7 +86,8 @@ class AsyncHttp { on_status_f on_status; on_error_f on_error; - on_body_f on_body; + on_body_recv_f on_body_recv; + on_body_send_f on_body_send; String method; String path; @@ -85,14 +95,24 @@ class AsyncHttp { String host; uint16_t port; - String data; // TODO: generic data source, feed chunks of (bytes, len) and call us back when done - uint32_t ts; uint32_t timeout = 5000; bool connected = false; bool connecting = false; + // TODO: since we are single threaded, no need to buffer anything and we can directly use client->add with anything right in the body_send callback + // buuut... this exposes asyncclient to the modules, maybe this needs a simple cbuf periodically flushing the data and this method simply filling it + // (ref: AsyncTCPBuffer class in ESPAsyncTCP or ESPAsyncWebServer chuncked response callback) + void trySend() { + if (!client.canSend()) return; + if (!on_body_send) { + client.close(true); + return; + } + on_body_send(this, &client); + } + protected: static AsyncHttpError _timeoutError(AsyncHttpError::error_t error, const __FlashStringHelper* message, uint32_t ts) { @@ -107,7 +127,6 @@ class AsyncHttp { static void _onDisconnect(void* http_ptr, AsyncClient*) { AsyncHttp* http = static_cast(http_ptr); if (http->on_disconnected) http->on_disconnected(http); - http->data = ""; http->ts = 0; http->connected = false; http->connecting = false; @@ -120,15 +139,14 @@ class AsyncHttp { AsyncHttp* http = static_cast(http_ptr); http->last_error = AsyncHttpError::NETWORK_TIMEOUT; if (http->on_error) http->on_error(http, _timeoutError(AsyncHttpError::NETWORK_TIMEOUT, F("Network timeout after"), time)); - // TODO: close connection when acks are missing? } - static void _onPoll(void* http_ptr, AsyncClient*) { + 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, F("No response after"), diff)); - http->client.close(true); + client->close(true); } } @@ -208,7 +226,7 @@ class AsyncHttp { } ASYNC_HTTP_DEBUG("ok | body len %u!\n", len); - if (http->on_body) http->on_body(http, (uint8_t*) response, len); + if (http->on_body_recv) http->on_body_recv(http, (uint8_t*) response, len); return; } } @@ -232,6 +250,18 @@ class AsyncHttp { + http->host.length() + http->path.length() + 32; + + int data_len = 0; + if (http->cfg & HTTP_SEND) { + if (!http->on_body_send) { + ASYNC_HTTP_DEBUG("err | no send_body callback set\n"); + client->close(true); + return; + } + // XXX: ...class instead of this multi-function? + data_len = http->on_body_send(http, nullptr); + } + char* headers = (char *) malloc(headers_len + 1); if (!headers) { @@ -245,7 +275,7 @@ class AsyncHttp { http->method.c_str(), http->path.c_str(), http->host.c_str(), - http->data.length() + data_len ); if (res >= (headers_len + 1)) { ASYNC_HTTP_DEBUG("err | res>=len :: %u>=%u\n", res, headers_len + 1); @@ -256,10 +286,8 @@ class AsyncHttp { client->write(headers); free(headers); - // TODO: streaming data source instead of using a simple String - // TODO: move to onPoll, ->add(data) and ->send() until it can't (returns 0), then repeat - client->write(http->data.c_str()); + if (http->cfg & HTTP_SEND) http->trySend(); } static void _onError(void* http_ptr, AsyncClient* client, err_t err) { @@ -267,6 +295,12 @@ class AsyncHttp { 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->cfg & HTTP_SEND) http->trySend(); + } + public: AsyncHttp() { client.onDisconnect(_onDisconnect, this); @@ -275,6 +309,7 @@ class AsyncHttp { client.onData(_onData, this); client.onConnect(_onConnect, this); client.onError(_onError, this); + client.onAck(_onAck, this); } ~AsyncHttp() = default; @@ -290,6 +325,13 @@ class AsyncHttp { this->path = path; this->ts = millis(); + // Treat every method as GET (receive-only), exception for POST / PUT to send data out + this->cfg = HTTP_RECV; + if (this->method.equals("POST") || this->method.equals("PUT")) { + if (!this->on_body_send) return false; + this->cfg = HTTP_SEND | HTTP_RECV; + } + bool status = false; #if ASYNC_TCP_SSL_ENABLED diff --git a/code/espurna/ota_asynctcp.ino b/code/espurna/ota_asynctcp.ino index 2bef50ac5b..11c264bd60 100644 --- a/code/espurna/ota_asynctcp.ino +++ b/code/espurna/ota_asynctcp.ino @@ -118,7 +118,7 @@ void _otaClientFrom(const String& url) { _ota_client->on_status = _otaOnStatus; _ota_client->on_error = _otaOnError; - _ota_client->on_body = _otaClientOnBody; + _ota_client->on_body_recv = _otaClientOnBody; } #if ASYNC_TCP_SSL_ENABLED diff --git a/code/espurna/thinkspeak.ino b/code/espurna/thinkspeak.ino index 677235a821..bcf9661f00 100644 --- a/code/espurna/thinkspeak.ino +++ b/code/espurna/thinkspeak.ino @@ -8,10 +8,9 @@ Copyright (C) 2019 by Xose Pérez #if THINGSPEAK_SUPPORT -#include "libs/Http.h" - #if THINGSPEAK_USE_ASYNC #include +#include "libs/Http.h" #else #include #endif @@ -95,13 +94,36 @@ void _tspkWebSocketOnConnected(JsonObject& root) { #if THINGSPEAK_USE_ASYNC AsyncHttp* _tspk_client = nullptr; +String _tspk_data; void _tspkFlushAgain() { DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-enqueuing %u more time(s)\n"), _tspk_state.tries); _tspk_state.flush = true; } -void _tspkOnBody(AsyncHttp* http, uint8_t* data, size_t len) { +// TODO: maybe http object can keep a context containing the data +// however, it should not be restricted to string datatype +int _tspkOnBodySend(AsyncHttp* http, AsyncClient* client) { + if (!client) { + _tspk_data = _tspkPrepareData(_tspk_queue); + return _tspk_data.length(); + } + + 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 = ""; + } + + return data_len; +} + +void _tspkOnBodyRecv(AsyncHttp* http, uint8_t* data, size_t len) { unsigned int code = 0; if (len) { @@ -154,11 +176,6 @@ void _tspkOnConnected(AsyncHttp* http) { } } #endif - - // Note: always replacing old data in case of retry - http->data = _tspkPrepareData(_tspk_queue); - - DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), http->path.c_str(), http->data.c_str()); } constexpr const unsigned long THINGSPEAK_CLIENT_TIMEOUT = 5000; @@ -174,7 +191,8 @@ void _tspkInitClient() { _tspk_client->on_status = _tspkOnStatus; _tspk_client->on_error = _tspkOnError; - _tspk_client->on_body = _tspkOnBody; + _tspk_client->on_body_recv = _tspkOnBodyRecv; + _tspk_client->on_body_send = _tspkOnBodySend; } 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 From 83013ca5d38352579a1c60323c50028ac73292cf Mon Sep 17 00:00:00 2001 From: Max Prokhorov Date: Mon, 30 Sep 2019 09:25:41 +0300 Subject: [PATCH 07/13] fix tspk retry counter --- code/espurna/thinkspeak.ino | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/code/espurna/thinkspeak.ino b/code/espurna/thinkspeak.ino index bcf9661f00..f7d9a9d5c1 100644 --- a/code/espurna/thinkspeak.ino +++ b/code/espurna/thinkspeak.ino @@ -24,7 +24,7 @@ std::vector _tspk_queue; struct tspk_state_t { bool flush = false; bool sent = false; - unsigned long last = 0; + unsigned long last_flush = 0; unsigned char tries = THINGSPEAK_TRIES; } _tspk_state; @@ -144,8 +144,12 @@ void _tspkOnBodyRecv(AsyncHttp* http, uint8_t* data, size_t len) { void _tspkOnDisconnected(AsyncHttp* http) { DEBUG_MSG_P(PSTR("[THINGSPEAK] Disconnected\n")); - _tspk_state.last = millis(); - if (!_tspk_state.sent && _tspk_state.tries) _tspkFlushAgain(); + _tspk_state.last_flush = millis(); + if (!_tspk_state.sent && _tspk_state.tries) { + _tspkFlushAgain(); + } else { + _tspkClearQueue(); + } if (_tspk_state.tries) --_tspk_state.tries; } @@ -250,7 +254,7 @@ void _tspkPost() { DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %u\n"), code); _tspk_client.stop(); - _tspk_state.last = millis(); + _tspk_state.last_flush = millis(); if ((0 == code) && _tspk_state.tries) { _tspkFlushAgain(); --_tspk_state.tries; @@ -281,13 +285,16 @@ void _tspkConfigure() { void _tspkEnqueue(unsigned char index, const char * payload) { DEBUG_MSG_P(PSTR("[THINGSPEAK] Enqueuing field #%u with value %s\n"), index, 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] = elem; + _tspk_queue[--index] = std::move(elem); + } void _tspkClearQueue() { @@ -302,13 +309,13 @@ void _tspkClearQueue() { void _tspkFlush() { if (!_tspk_state.flush) return; - if (millis() - _tspk_state.last < THINGSPEAK_MIN_INTERVAL) return; + if (millis() - _tspk_state.last_flush < THINGSPEAK_MIN_INTERVAL) return; #if THINGSPEAK_USE_ASYNC if (_tspk_client->busy()) return; #endif - _tspk_state.last = millis(); + _tspk_state.last_flush = millis(); _tspk_state.sent = false; _tspk_state.flush = false; From e271cb915dd26907bf45b49ef914255bccac99aa Mon Sep 17 00:00:00 2001 From: Max Prokhorov Date: Mon, 30 Sep 2019 10:24:29 +0300 Subject: [PATCH 08/13] structured headers instead of plain text formatting --- code/espurna/libs/Http.h | 294 +++++++++++++++++++++++++++++++-------- 1 file changed, 239 insertions(+), 55 deletions(-) diff --git a/code/espurna/libs/Http.h b/code/espurna/libs/Http.h index 277fe7576d..3fcaff9f52 100644 --- a/code/espurna/libs/Http.h +++ b/code/espurna/libs/Http.h @@ -8,16 +8,163 @@ #define ASYNC_HTTP_DEBUG(...) //DEBUG_PORT.printf(__VA_ARGS__) #endif -// TODO: customizable headers -// -const char HTTP_REQUEST_TEMPLATE[] PROGMEM = - "%s %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: %u\r\n" - "\r\n"; +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"; +}; + +struct AsyncHttpHeader { + + using header_t = std::pair; + + private: + + const String _key; + const String _value; + header_t _kv; + + public: + + AsyncHttpHeader(const char* key, const char* value) : + _key(FPSTR(key)), + _value(FPSTR(value)), + _kv(_key, _value) + {} + + AsyncHttpHeader(const String& key, const String& value) : + _key(key), + _value(value), + _kv(_key, _value) + {} + + AsyncHttpHeader(const AsyncHttpHeader& other) : + _key(other._key), + _value(other._value), + _kv(_key, _value) + {} + + const header_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) + ); + } + +}; + +struct 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 header_t& header) { + _headers.push_back(header); + } + + 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 (strcmp_P(key, header.key()) == 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(); + } + +}; struct AsyncHttpError { @@ -53,6 +200,9 @@ struct AsyncHttpError { class AsyncHttp { + constexpr const size_t DEFAULT_TIMEOUT = 5000; + constexpr const size_t DEFAULT_PATH_BUFSIZE = 256; + public: AsyncClient client; @@ -92,18 +242,23 @@ class AsyncHttp { String method; String path; + // WebRequest.cpp + //LinkedList headers; + //std::vector headers; + AsyncHttpHeaders headers; + String host; uint16_t port; uint32_t ts; - uint32_t timeout = 5000; + uint32_t timeout = DEFAULT_TIMEOUT; bool connected = false; bool connecting = false; - // TODO: since we are single threaded, no need to buffer anything and we can directly use client->add with anything right in the body_send callback + // 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 - // (ref: AsyncTCPBuffer class in ESPAsyncTCP or ESPAsyncWebServer chuncked response callback) void trySend() { if (!client.canSend()) return; if (!on_body_send) { @@ -113,6 +268,29 @@ class AsyncHttp { on_body_send(this, &client); } + bool trySendHeaders() { + 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); + } + } + client.send(); + } + + return false; + } + + protected: static AsyncHttpError _timeoutError(AsyncHttpError::error_t error, const __FlashStringHelper* message, uint32_t ts) { @@ -126,7 +304,7 @@ class AsyncHttp { static void _onDisconnect(void* http_ptr, AsyncClient*) { AsyncHttp* http = static_cast(http_ptr); - if (http->on_disconnected) http->on_disconnected(http); + if (http->on_disconnected) http->on_disconnected(http); http->ts = 0; http->connected = false; http->connecting = false; @@ -244,50 +422,41 @@ class AsyncHttp { if (http->on_connected) http->on_connected(http); - const int headers_len = - strlen_P(HTTP_REQUEST_TEMPLATE) - + http->method.length() - + http->host.length() - + http->path.length() - + 32; - - int data_len = 0; - if (http->cfg & HTTP_SEND) { - if (!http->on_body_send) { - ASYNC_HTTP_DEBUG("err | no send_body callback set\n"); - client->close(true); - return; + { + size_t data_len = 0; + if (http->cfg & HTTP_SEND) { + if (!http->on_body_send) { + ASYNC_HTTP_DEBUG("err | no send_body callback set\n"); + client->close(true); + return; + } + // XXX: ...class instead of this multi-function? + data_len = http->on_body_send(http, nullptr); + char data_buf[22]; + snprintf(data_buf, sizeof(data_buf), "%u", data_len); + http->headers.add({Headers::CONTENT_LENGTH, data_buf}); } - // XXX: ...class instead of this multi-function? - data_len = http->on_body_send(http, nullptr); } - char* headers = (char *) malloc(headers_len + 1); + { + 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 (!headers) { - ASYNC_HTTP_DEBUG("err | alloc %u fail\n", headers_len + 1); - client->close(true); - return; - } + if ((res < 0) || (static_cast(res) > sizeof(buf))) { + ASYNC_HTTP_DEBUG("err | could not print initial line\n"); + client->close(true); + return; + } - int res = snprintf_P(headers, headers_len + 1, - HTTP_REQUEST_TEMPLATE, - http->method.c_str(), - http->path.c_str(), - http->host.c_str(), - data_len - ); - if (res >= (headers_len + 1)) { - ASYNC_HTTP_DEBUG("err | res>=len :: %u>=%u\n", res, headers_len + 1); - free(headers); - client->close(true); - return; + client->add(buf, res); } - client->write(headers); - free(headers); - - if (http->cfg & HTTP_SEND) http->trySend(); + if (http->trySendHeaders()) { + if (http->cfg & HTTP_SEND) http->trySend(); + } } static void _onError(void* http_ptr, AsyncClient* client, err_t err) { @@ -298,11 +467,15 @@ class AsyncHttp { static void _onAck(void* http_ptr, AsyncClient* client, size_t, uint32_t) { AsyncHttp* http = static_cast(http_ptr); http->ts = millis(); - if (http->cfg & HTTP_SEND) http->trySend(); + if (http->trySendHeaders()) { + if (http->cfg & HTTP_SEND) http->trySend(); + } } + public: - AsyncHttp() { + AsyncHttp() + { client.onDisconnect(_onDisconnect, this); client.onTimeout(_onTimeout, this); client.onPoll(_onPoll, this); @@ -326,14 +499,25 @@ class AsyncHttp { 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) return false; this->cfg = HTTP_SEND | HTTP_RECV; + headers_size += 2; } - bool status = false; + headers.reserve(headers_size); + headers.clear(); + + headers.add({Headers::HOST, this->host.c_str()}); + headers.add({Headers::USER_AGENT, "ESPurna"}); + headers.add({Headers::CONNECTION, "close"}); + if (this->cfg & HTTP_SEND) { + headers.add({Headers::CONTENT_TYPE, "application/x-www-form-urlencoded"}); + } + bool status = false; #if ASYNC_TCP_SSL_ENABLED status = client.connect(this->host.c_str(), this->port, use_ssl); #else @@ -341,7 +525,7 @@ class AsyncHttp { #endif this->connecting = status; - + if (!status) { client.close(true); } From b41c27b66f81575925740c5ed17144c898bfeb78 Mon Sep 17 00:00:00 2001 From: Max Prokhorov Date: Mon, 30 Sep 2019 14:56:52 +0300 Subject: [PATCH 09/13] header obj cannot contain progmem pointers --- code/espurna/libs/Http.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/espurna/libs/Http.h b/code/espurna/libs/Http.h index 3fcaff9f52..5bb4ea2b77 100644 --- a/code/espurna/libs/Http.h +++ b/code/espurna/libs/Http.h @@ -113,7 +113,7 @@ struct AsyncHttpHeaders { bool has(const char* key) { for (const auto& header : _headers) { - if (strcmp_P(key, header.key()) == 0) return true; + if (strncmp(key, header.key(), header.keyLength()) == 0) return true; } return false; } From 7d9e13235e0da756b804fdd409514755b4d60146 Mon Sep 17 00:00:00 2001 From: Max Prokhorov Date: Mon, 30 Sep 2019 15:47:37 +0300 Subject: [PATCH 10/13] split data cb into two, manage content-type header via cb --- code/espurna/libs/Http.h | 71 +++++++++++++++++++++++++------------ code/espurna/thinkspeak.ino | 16 +++++---- 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/code/espurna/libs/Http.h b/code/espurna/libs/Http.h index 5bb4ea2b77..cfbcaeca36 100644 --- a/code/espurna/libs/Http.h +++ b/code/espurna/libs/Http.h @@ -16,15 +16,17 @@ namespace Headers { PROGMEM const char CONTENT_LENGTH[] = "Content-Length"; }; -struct AsyncHttpHeader { +class AsyncHttpHeader { - using header_t = std::pair; + public: + + using key_value_t = std::pair; private: const String _key; const String _value; - header_t _kv; + key_value_t _kv; public: @@ -46,7 +48,7 @@ struct AsyncHttpHeader { _kv(_key, _value) {} - const header_t& get() const { + const key_value_t& get() const { return _kv; } @@ -74,7 +76,7 @@ struct AsyncHttpHeader { }; -struct AsyncHttpHeaders { +class AsyncHttpHeaders { using header_t = AsyncHttpHeader; using headers_t = std::vector; @@ -166,7 +168,9 @@ struct AsyncHttpHeaders { }; -struct AsyncHttpError { +class AsyncHttpError { + + public: enum error_t { EMPTY, @@ -200,8 +204,8 @@ struct AsyncHttpError { class AsyncHttp { - constexpr const size_t DEFAULT_TIMEOUT = 5000; - constexpr const size_t DEFAULT_PATH_BUFSIZE = 256; + constexpr static const size_t DEFAULT_TIMEOUT = 5000; + constexpr static const size_t DEFAULT_PATH_BUFSIZE = 256; public: @@ -224,7 +228,7 @@ class AsyncHttp { using on_error_f = std::function; using on_body_recv_f = std::function; - using on_body_send_f = std::function; + using on_body_send_f = std::function; int cfg = HTTP_RECV; state_t state = state_t::NONE; @@ -237,7 +241,8 @@ class AsyncHttp { on_error_f on_error; on_body_recv_f on_body_recv; - on_body_send_f on_body_send; + on_body_send_f on_body_send_prepare; + on_body_send_f on_body_send_data; String method; String path; @@ -261,11 +266,11 @@ class AsyncHttp { // 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) { + if (!on_body_send_data) { client.close(true); return; } - on_body_send(this, &client); + on_body_send_data(this, &client); } bool trySendHeaders() { @@ -425,20 +430,24 @@ class AsyncHttp { { size_t data_len = 0; if (http->cfg & HTTP_SEND) { - if (!http->on_body_send) { - ASYNC_HTTP_DEBUG("err | no send_body callback set\n"); + 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; } - // XXX: ...class instead of this multi-function? - data_len = http->on_body_send(http, nullptr); - char data_buf[22]; + 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"), @@ -495,14 +504,33 @@ class AsyncHttp { 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) return false; + 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; } @@ -511,11 +539,8 @@ class AsyncHttp { headers.clear(); headers.add({Headers::HOST, this->host.c_str()}); - headers.add({Headers::USER_AGENT, "ESPurna"}); - headers.add({Headers::CONNECTION, "close"}); - if (this->cfg & HTTP_SEND) { - headers.add({Headers::CONTENT_TYPE, "application/x-www-form-urlencoded"}); - } + headers.add({Headers::USER_AGENT, F("ESPurna")}); + headers.add({Headers::CONNECTION, F("close")}); bool status = false; #if ASYNC_TCP_SSL_ENABLED diff --git a/code/espurna/thinkspeak.ino b/code/espurna/thinkspeak.ino index f7d9a9d5c1..d12f9e66d5 100644 --- a/code/espurna/thinkspeak.ino +++ b/code/espurna/thinkspeak.ino @@ -103,12 +103,14 @@ void _tspkFlushAgain() { // TODO: maybe http object can keep a context containing the data // however, it should not be restricted to string datatype -int _tspkOnBodySend(AsyncHttp* http, AsyncClient* client) { - if (!client) { - _tspk_data = _tspkPrepareData(_tspk_queue); - return _tspk_data.length(); - } +size_t _tspkOnBodySendPrepare(AsyncHttp* http, AsyncClient* client) { + http->headers.add({Headers::CONTENT_TYPE, F("application/x-www-form-urlencoded")}); + _tspk_data = _tspkPrepareData(_tspk_queue); + return _tspk_data.length(); +} + +size_t _tspkOnBodySend(AsyncHttp* http, AsyncClient* client) { const size_t data_len = _tspk_data.length(); if (!data_len || (client->space() < data_len)) { return 0; @@ -196,7 +198,9 @@ void _tspkInitClient() { _tspk_client->on_error = _tspkOnError; _tspk_client->on_body_recv = _tspkOnBodyRecv; - _tspk_client->on_body_send = _tspkOnBodySend; + + _tspk_client->on_body_send_prepare = _tspkOnBodySendPrepare; + _tspk_client->on_body_send_data = _tspkOnBodySend; } From 904a38bde78c0030b61857a98908ea785c36cc7c Mon Sep 17 00:00:00 2001 From: Max Prokhorov Date: Mon, 30 Sep 2019 15:55:28 +0300 Subject: [PATCH 11/13] fix travis --- code/espurna/libs/Http.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/code/espurna/libs/Http.h b/code/espurna/libs/Http.h index cfbcaeca36..99b64e702b 100644 --- a/code/espurna/libs/Http.h +++ b/code/espurna/libs/Http.h @@ -1,7 +1,14 @@ #pragma once +#include #include +#include +#include +#include +#include + +#include #include #ifndef ASYNC_HTTP_DEBUG From 1114d3dfb02c98fc10a72e3f02de21913fc679a3 Mon Sep 17 00:00:00 2001 From: Max Prokhorov Date: Mon, 30 Sep 2019 23:42:07 +0300 Subject: [PATCH 12/13] use pstr for headers --- code/espurna/libs/Http.h | 24 ++++++++++++------------ code/espurna/thinkspeak.ino | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/code/espurna/libs/Http.h b/code/espurna/libs/Http.h index 99b64e702b..5ce81e504f 100644 --- a/code/espurna/libs/Http.h +++ b/code/espurna/libs/Http.h @@ -43,18 +43,18 @@ class AsyncHttpHeader { _kv(_key, _value) {} - AsyncHttpHeader(const String& key, const String& value) : - _key(key), - _value(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; } @@ -108,8 +108,8 @@ class AsyncHttpHeaders { _last(std::numeric_limits::max()) {} - void add(const header_t& header) { - _headers.push_back(header); + void add(const char* key, const char* value) { + _headers.emplace_back(key, value); } size_t size() { @@ -449,7 +449,7 @@ class AsyncHttp { } char data_buf[16]; snprintf(data_buf, sizeof(data_buf), "%u", data_len); - http->headers.add({Headers::CONTENT_LENGTH, data_buf}); + http->headers.add(Headers::CONTENT_LENGTH, data_buf); } } @@ -545,9 +545,9 @@ class AsyncHttp { headers.reserve(headers_size); headers.clear(); - headers.add({Headers::HOST, this->host.c_str()}); - headers.add({Headers::USER_AGENT, F("ESPurna")}); - headers.add({Headers::CONNECTION, F("close")}); + 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 diff --git a/code/espurna/thinkspeak.ino b/code/espurna/thinkspeak.ino index d12f9e66d5..17d47d4771 100644 --- a/code/espurna/thinkspeak.ino +++ b/code/espurna/thinkspeak.ino @@ -105,7 +105,7 @@ void _tspkFlushAgain() { // however, it should not be restricted to string datatype size_t _tspkOnBodySendPrepare(AsyncHttp* http, AsyncClient* client) { - http->headers.add({Headers::CONTENT_TYPE, F("application/x-www-form-urlencoded")}); + http->headers.add(Headers::CONTENT_TYPE, PSTR("application/x-www-form-urlencoded")); _tspk_data = _tspkPrepareData(_tspk_queue); return _tspk_data.length(); } From 92e22e07077fef1271fe2e7daa01b89e894f2dc8 Mon Sep 17 00:00:00 2001 From: Max Prokhorov Date: Tue, 1 Oct 2019 00:45:19 +0300 Subject: [PATCH 13/13] continiously send headers --- code/espurna/libs/Http.h | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/code/espurna/libs/Http.h b/code/espurna/libs/Http.h index 5ce81e504f..763bdc4d9c 100644 --- a/code/espurna/libs/Http.h +++ b/code/espurna/libs/Http.h @@ -281,34 +281,41 @@ class AsyncHttp { } bool trySendHeaders() { - if (headers.done()) return true; + do { + if (!client.canSend()) return false; + if (headers.done()) return true; - const auto& string = headers.current(); - const auto len = string.length(); + const auto& string = headers.current(); + const auto len = string.length(); - if (!len) { - return true; - } + 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); + if (client.space() >= (len + 2)) { + if (client.add(string.c_str(), len)) { + if (!headers.next().length()) { + client.add("\r\n", 2); + break; + } + continue; } } - client.send(); - } + } while (true); + + client.send(); return false; + } protected: - static AsyncHttpError _timeoutError(AsyncHttpError::error_t error, const __FlashStringHelper* message, uint32_t ts) { + static AsyncHttpError _timeoutError(AsyncHttpError::error_t error, const char* message, uint32_t ts) { String data; data.reserve(32); - data += message; + data += FPSTR(message); data += ' '; data += String(ts); return {error, data}; @@ -328,14 +335,14 @@ class AsyncHttp { AsyncHttp* http = static_cast(http_ptr); http->last_error = AsyncHttpError::NETWORK_TIMEOUT; - if (http->on_error) http->on_error(http, _timeoutError(AsyncHttpError::NETWORK_TIMEOUT, F("Network timeout after"), time)); + 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, F("No response after"), diff)); + if (http->on_error) http->on_error(http, _timeoutError(AsyncHttpError::REQUEST_TIMEOUT, PSTR("No response after"), diff)); client->close(true); } }