Browse Source

Thingspeak: cleanup (#2140)

* Thingspeak: cleanup

* fix sync client config, refactoring

* revert changes in tspk config

* inheritance

* fix empty wificlient
pull/2142/head
Max Prokhorov 4 years ago
committed by GitHub
parent
commit
80bff31d8a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 253 additions and 80 deletions
  1. +21
    -3
      code/espurna/config/general.h
  2. +1
    -0
      code/espurna/espurna.ino
  3. +13
    -0
      code/espurna/libs/AsyncClientHelpers.h
  4. +14
    -2
      code/espurna/libs/URL.h
  5. +42
    -0
      code/espurna/static/digicert_high_assurance_pem.h
  6. +26
    -0
      code/espurna/thingspeak.h
  7. +134
    -75
      code/espurna/thinkspeak.ino
  8. +2
    -0
      code/test/build/extra/secure_client.h

+ 21
- 3
code/espurna/config/general.h View File

@ -1411,29 +1411,47 @@
// Not clearing it will result in latest values for each field being sent every time
#endif
#ifndef THINGSPEAK_USE_ASYNC
#define THINGSPEAK_USE_ASYNC 1 // Use AsyncClient instead of WiFiClientSecure
#endif
// THINGSPEAK OVER SSL
// Using THINGSPEAK over SSL works well but generates problems with the web interface,
// so you should compile it with WEB_SUPPORT to 0.
// When THINGSPEAK_USE_ASYNC is 1, requires EspAsyncTCP to be built with ASYNC_TCP_SSL_ENABLED=1 and ESP8266 Arduino Core >= 2.4.0.
// When THINGSPEAK_USE_ASYNC is 0, requires Arduino Core >= 2.6.0 and SECURE_CLIENT_BEARSSL
#ifndef THINGSPEAK_USE_SSL
#define THINGSPEAK_USE_SSL 0 // Use secure connection
#endif
#ifndef THINGSPEAK_SECURE_CLIENT_CHECK
#define THINGSPEAK_SECURE_CLIENT_CHECK SECURE_CLIENT_CHECK
#endif
#ifndef THINGSPEAK_SECURE_CLIENT_MFLN
#define THINGSPEAK_SECURE_CLIENT_MFLN SECURE_CLIENT_MFLN
#endif
#ifndef THINGSPEAK_FINGERPRINT
#define THINGSPEAK_FINGERPRINT "78 60 18 44 81 35 BF DF 77 84 D4 0A 22 0D 9B 4E 6C DC 57 2C"
#endif
#ifndef THINGSPEAK_ADDRESS
#if THINGSPEAK_USE_SSL
#define THINGSPEAK_ADDRESS "https://api.thingspeak.com/update"
#else
#define THINGSPEAK_ADDRESS "http://api.thingspeak.com/update"
#endif
#define THINGSPEAK_MIN_INTERVAL 15000 // Minimum interval between POSTs (in millis)
#define THINGSPEAK_FIELDS 8 // Number of fields
#endif // ifndef THINGSPEAK_ADDRESS
#ifndef THINGSPEAK_TRIES
#define THINGSPEAK_TRIES 3 // Number of tries when sending data (minimum 1)
#endif
#define THINGSPEAK_MIN_INTERVAL 15000 // Minimum interval between POSTs (in millis)
#define THINGSPEAK_FIELDS 8 // Number of fields
// -----------------------------------------------------------------------------
// SCHEDULER
// -----------------------------------------------------------------------------


+ 1
- 0
code/espurna/espurna.ino View File

@ -59,6 +59,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "web.h"
#include "ws.h"
#include "libs/URL.h"
#include "libs/HeapStats.h"
using void_callback_f = void (*)();


+ 13
- 0
code/espurna/libs/AsyncClientHelpers.h View File

@ -0,0 +1,13 @@
// -----------------------------------------------------------------------------
// AsyncClient helpers
// -----------------------------------------------------------------------------
#pragma once
enum class AsyncClientState {
Disconnected,
Connecting,
Connected
};

+ 14
- 2
code/espurna/libs/URL.h View File

@ -10,6 +10,7 @@
class URL {
public:
URL();
URL(const String&);
String protocol;
@ -18,11 +19,21 @@ class URL {
uint16_t port;
private:
String buffer;
void _parse(String);
};
URL::URL() :
protocol(),
host(),
path(),
port(0)
{}
URL::URL(const String& url) : buffer(url) {
URL::URL(const String& string) {
_parse(string);
}
void URL::_parse(String buffer) {
// cut the protocol part
int index = buffer.indexOf("://");
@ -65,3 +76,4 @@ URL::URL(const String& url) : buffer(url) {
}
}

+ 42
- 0
code/espurna/static/digicert_high_assurance_pem.h View File

@ -0,0 +1,42 @@
// https://api.thingspeak.com root issuer
// Issuer: C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert High Assurance EV Root CA
// Validity
// Not Before: Oct 22 12:00:00 2013 GMT
// Not After : Oct 22 12:00:00 2028 GMT
// Subject: C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert SHA2 High Assurance Server CA
#pragma once
#include <pgmspace.h>
const char PROGMEM _ssl_digicert_high_assurance_ev_root_ca[] = R"EOF(
-----BEGIN CERTIFICATE-----
MIIEsTCCA5mgAwIBAgIQBOHnpNxc8vNtwCtCuF0VnzANBgkqhkiG9w0BAQsFADBs
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
ZSBFViBSb290IENBMB4XDTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowcDEL
MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
LmRpZ2ljZXJ0LmNvbTEvMC0GA1UEAxMmRGlnaUNlcnQgU0hBMiBIaWdoIEFzc3Vy
YW5jZSBTZXJ2ZXIgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2
4C/CJAbIbQRf1+8KZAayfSImZRauQkCbztyfn3YHPsMwVYcZuU+UDlqUH1VWtMIC
Kq/QmO4LQNfE0DtyyBSe75CxEamu0si4QzrZCwvV1ZX1QK/IHe1NnF9Xt4ZQaJn1
itrSxwUfqJfJ3KSxgoQtxq2lnMcZgqaFD15EWCo3j/018QsIJzJa9buLnqS9UdAn
4t07QjOjBSjEuyjMmqwrIw14xnvmXnG3Sj4I+4G3FhahnSMSTeXXkgisdaScus0X
sh5ENWV/UyU50RwKmmMbGZJ0aAo3wsJSSMs5WqK24V3B3aAguCGikyZvFEohQcft
bZvySC/zA/WiaJJTL17jAgMBAAGjggFJMIIBRTASBgNVHRMBAf8ECDAGAQH/AgEA
MA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw
NAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2Vy
dC5jb20wSwYDVR0fBEQwQjBAoD6gPIY6aHR0cDovL2NybDQuZGlnaWNlcnQuY29t
L0RpZ2lDZXJ0SGlnaEFzc3VyYW5jZUVWUm9vdENBLmNybDA9BgNVHSAENjA0MDIG
BFUdIAAwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQ
UzAdBgNVHQ4EFgQUUWj/kK8CB3U8zNllZGKiErhZcjswHwYDVR0jBBgwFoAUsT7D
aQP4v0cB1JgmGggC72NkK8MwDQYJKoZIhvcNAQELBQADggEBABiKlYkD5m3fXPwd
aOpKj4PWUS+Na0QWnqxj9dJubISZi6qBcYRb7TROsLd5kinMLYBq8I4g4Xmk/gNH
E+r1hspZcX30BJZr01lYPf7TMSVcGDiEo+afgv2MW5gxTs14nhr9hctJqvIni5ly
/D6q1UEL2tU2ob8cbkdJf17ZSHwD2f2LSaCYJkJA69aSEaRkCldUxPUd1gJea6zu
xICaEnL6VpPX/78whQYwvwt/Tv9XBZ0k7YXDK/umdaisLRbvfXknsuvCnQsH6qqF
0wGjIChBWUMo0oHjqvbsezt3tkBigAVBRQHvFwY+3sAzm2fTYS5yh+Rp/BIAV0Ae
cPUeybQ=
-----END CERTIFICATE-----
)EOF";

+ 26
- 0
code/espurna/thingspeak.h View File

@ -0,0 +1,26 @@
/*
THINGSPEAK MODULE
Copyright (C) 2019 by Xose Pérez <xose dot perez at gmail dot com>
*/
#pragma once
#if THINGSPEAK_SUPPORT
#if THINGSPEAK_USE_ASYNC
#include <ESPAsyncTCP.h>
#endif
constexpr const size_t tspkDataBufferSize = 256;
bool tspkEnqueueRelay(unsigned char index, bool status);
bool tspkEnqueueMeasurement(unsigned char index, const char * payload);
void tspkFlush();
bool tspkEnabled();
void tspkSetup();
#endif // THINGSPEAK_SUPPORT == 1

+ 134
- 75
code/espurna/thinkspeak.ino View File

@ -8,16 +8,24 @@ Copyright (C) 2019 by Xose Pérez <xose dot perez at gmail dot com>
#if THINGSPEAK_SUPPORT
#include <memory>
#include "broker.h"
#include "thingspeak.h"
#include "libs/URL.h"
#include "libs/SecureClientHelpers.h"
#include "libs/AsyncClientHelpers.h"
#if THINGSPEAK_USE_ASYNC
#include <ESPAsyncTCP.h>
#if SECURE_CLIENT != SECURE_CLIENT_NONE
#if THINGSPEAK_SECURE_CLIENT_INCLUDE_CA
#include "static/thingspeak_client_trusted_root_ca.h"
#else
#include <ESP8266WiFi.h>
#include "static/digicert_high_assurance_pem.h"
#define _tspk_client_trusted_root_ca _ssl_digicert_high_assurance_ev_root_ca
#endif
#define THINGSPEAK_DATA_BUFFER_SIZE 256
#endif // SECURE_CLIENT != SECURE_CLIENT_NONE
const char THINGSPEAK_REQUEST_TEMPLATE[] PROGMEM =
"POST %s HTTP/1.1\r\n"
@ -37,19 +45,32 @@ bool _tspk_flush = false;
unsigned long _tspk_last_flush = 0;
unsigned char _tspk_tries = THINGSPEAK_TRIES;
class AsyncThingspeak : public AsyncClient
{
public:
#if THINGSPEAK_USE_ASYNC
class AsyncThingspeak : public AsyncClient {
public:
URL address;
AsyncThingspeak(const String& _url) : address(_url) { };
bool connect() {
#if ASYNC_TCP_SSL_ENABLED && THINGSPEAK_USE_SSL
return AsyncClient::connect(address.host.c_str(), address.port, true);
#else
return AsyncClient::connect(address.host.c_str(), address.port);
#endif
}
bool connect(const String& url) {
address = url;
return connect();
}
};
AsyncThingspeak * _tspk_client;
AsyncThingspeak* _tspk_client = nullptr;
AsyncClientState _tspk_state = AsyncClientState::Disconnected;
#if THINGSPEAK_USE_ASYNC
bool _tspk_connecting = false;
bool _tspk_connected = false;
#endif
#endif // THINGSPEAK_USE_ASYNC == 1
// -----------------------------------------------------------------------------
@ -105,7 +126,10 @@ void _tspkConfigure() {
_tspk_enabled = false;
setSetting("tspkEnabled", 0);
}
if (_tspk_enabled && !_tspk_client) _tspkInitClient(getSetting("tspkAddress", THINGSPEAK_ADDRESS));
#if THINGSPEAK_USE_ASYNC
if (_tspk_enabled && !_tspk_client) _tspkInitClient(getSetting("tspkAddress", THINGSPEAK_ADDRESS));
#endif
}
#if THINGSPEAK_USE_ASYNC
@ -129,8 +153,7 @@ void _tspkInitClient(const String& _url) {
_tspk_data = "";
_tspk_client_ts = 0;
_tspk_last_flush = millis();
_tspk_connected = false;
_tspk_connecting = false;
_tspk_state = AsyncClientState::Disconnected;
_tspk_client_state = tspk_state_t::NONE;
}, nullptr);
@ -201,27 +224,26 @@ void _tspkInitClient(const String& _url) {
_tspk_client->onConnect([](void * arg, AsyncClient * client) {
_tspk_connected = true;
_tspk_connecting = false;
AsyncThingspeak* _tspk_client = reinterpret_cast<AsyncThingspeak*>(client);
_tspk_state = AsyncClientState::Disconnected;
DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%u\n"), _tspk_client->address.host.c_str(), _tspk_client->address.port);
AsyncThingspeak* tspk_client = reinterpret_cast<AsyncThingspeak*>(client);
DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%u\n"), tspk_client->address.host.c_str(), tspk_client->address.port);
#if THINGSPEAK_USE_SSL
uint8_t fp[20] = {0};
sslFingerPrintArray(THINGSPEAK_FINGERPRINT, fp);
SSL * ssl = _tspk_client->getSSL();
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"), _tspk_client->address.path.c_str(), _tspk_data.c_str());
char headers[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + _tspk_client->address.path.length() + _tspk_client->address.host.length() + 1];
DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), tspk_client->address.path.c_str(), _tspk_data.c_str());
char headers[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + tspk_client->address.path.length() + tspk_client->address.host.length() + 1];
snprintf_P(headers, sizeof(headers),
THINGSPEAK_REQUEST_TEMPLATE,
_tspk_client->address.path.c_str(),
_tspk_client->address.host.c_str(),
tspk_client->address.path.c_str(),
tspk_client->address.host.c_str(),
_tspk_data.length()
);
@ -232,22 +254,16 @@ void _tspkInitClient(const String& _url) {
}
void _tspkPost() {
void _tspkPost(const String& address) {
if (_tspk_connected || _tspk_connecting) return;
if (_tspk_state != AsyncClientState::Disconnected) return;
_tspk_client_ts = millis();
#if THINGSPEAK_USE_SSL
bool connected = _tspk_client->connect(_tspk_host.c_str(), _tspk_port, THINGSPEAK_USE_SSL);
#else
_tspk_client->address = URL(getSetting("tspkAddress", THINGSPEAK_ADDRESS));
bool connected = _tspk_client->connect(_tspk_client->address.host.c_str(), _tspk_client->address.port);
#endif
_tspk_connecting = connected;
_tspk_state = _tspk_client->connect(address)
? AsyncClientState::Connecting
: AsyncClientState::Disconnected;
if (!connected) {
if (_tspk_state == AsyncClientState::Disconnected) {
DEBUG_MSG_P(PSTR("[THINGSPEAK] Connection failed\n"));
_tspk_client->close(true);
}
@ -256,55 +272,95 @@ void _tspkPost() {
#else // THINGSPEAK_USE_ASYNC
void _tspkPost() {
#if THINGSPEAK_USE_SSL && (SECURE_CLIENT == SECURE_CLIENT_BEARSSL)
SecureClientConfig _tspk_sc_config {
"THINGSPEAK",
[]() -> int {
return getSetting("tspkScCheck", THINGSPEAK_SECURE_CLIENT_CHECK);
},
[]() -> PGM_P {
return _tspk_client_trusted_root_ca;
},
[]() -> String {
return getSetting("tspkFP", THINGSPEAK_FINGERPRINT);
},
[]() -> uint16_t {
return getSetting("tspkScMFLN", THINGSPEAK_SECURE_CLIENT_MFLN);
},
true
};
#if THINGSPEAK_USE_SSL
WiFiClientSecure _tspk_client;
#else
WiFiClient _tspk_client;
#endif
#endif // THINGSPEAK_USE_SSL && SECURE_CLIENT_BEARSSL
if (_tspk_client.connect(_tspk_host.c_str(), _tspk_port)) {
void _tspkPost(WiFiClient* client, const URL& url) {
DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%u\n"), _tspk_host.c_str(), _tspk_port);
if (!client->connect(url.host.c_str(), url.port)) {
DEBUG_MSG_P(PSTR("[THINGSPEAK] Connection failed\n"));
return;
}
if (!_tspk_client.verify(THINGSPEAK_FINGERPRINT, _tspk_host.c_str())) {
DEBUG_MSG_P(PSTR("[THINGSPEAK] Warning: certificate doesn't match\n"));
}
DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%u\n"), url.host.c_str(), url.port);
DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), url.path.c_str(), _tspk_data.c_str());
DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), _tspk_client.path.c_str(), _tspk_data.c_str());
char headers[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + _tspk_client.path.length() + _tspk_client.host.lengh() + 1];
snprintf_P(headers, sizeof(headers),
THINGSPEAK_REQUEST_TEMPLATE,
_tspk_client.path.c_str(),
_tspk_client.host.c_str(),
_tspk_data.length()
);
char headers[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + url.path.length() + url.host.length() + 1];
snprintf_P(headers, sizeof(headers),
THINGSPEAK_REQUEST_TEMPLATE,
url.path.c_str(),
url.host.c_str(),
_tspk_data.length()
);
_tspk_client.print(headers);
_tspk_client.print(_tspk_data);
client->print(headers);
client->print(_tspk_data);
nice_delay(100);
nice_delay(100);
String response = _tspk_client.readString();
int pos = response.indexOf("\r\n\r\n");
unsigned int code = (pos > 0) ? response.substring(pos + 4).toInt() : 0;
DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %u\n"), code);
_tspk_client.stop();
const auto response = client->readString();
int pos = response.indexOf("\r\n\r\n");
_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);
} else {
_tspkClearQueue();
}
unsigned int code = (pos > 0) ? response.substring(pos + 4).toInt() : 0;
DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %u\n"), code);
return;
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);
} else {
_tspkClearQueue();
}
DEBUG_MSG_P(PSTR("[THINGSPEAK] Connection failed\n"));
}
void _tspkPost(const String& address) {
const URL url(address);
#if SECURE_CLIENT == SECURE_CLIENT_BEARSSL
if (url.protocol == "https") {
const int check = _ota_sc_config.on_check();
if (!ntpSynced() && (check == SECURE_CLIENT_CHECK_CA)) {
DEBUG_MSG_P(PSTR("[THINGSPEAK] Time not synced! Cannot use CA validation\n"));
return;
}
auto client = std::make_unique<SecureClient>(_tspk_sc_config);
if (!client->beforeConnected()) {
return;
}
_tspkPost(&client->get(), url);
return;
}
#endif
if (url.protocol == "http") {
auto client = std::make_unique<WiFiClient>();
_tspkPost(client.get(), url);
return;
}
}
@ -333,11 +389,14 @@ void _tspkFlush() {
if (!_tspk_flush) return;
if (millis() - _tspk_last_flush < THINGSPEAK_MIN_INTERVAL) return;
if (_tspk_connected || _tspk_connecting) return;
#if THINGSPEAK_USE_ASYNC
if (_tspk_state != AsyncClientState::Disconnected) return;
#endif
_tspk_last_flush = millis();
_tspk_flush = false;
_tspk_data.reserve(THINGSPEAK_DATA_BUFFER_SIZE);
_tspk_data.reserve(tspkDataBufferSize);
// Walk the fields, numbered 1...THINGSPEAK_FIELDS
for (unsigned char id=0; id<THINGSPEAK_FIELDS; id++) {
@ -354,7 +413,7 @@ void _tspkFlush() {
_tspk_data.concat("&api_key=");
_tspk_data.concat(getSetting<String>("tspkKey", THINGSPEAK_APIKEY));
--_tspk_tries;
_tspkPost();
_tspkPost(getSetting("tspkAddress", THINGSPEAK_ADDRESS));
}
}


+ 2
- 0
code/test/build/extra/secure_client.h View File

@ -1,3 +1,5 @@
#define SECURE_CLIENT SECURE_CLIENT_BEARSSL
#define MQTT_LIBRARY MQTT_LIBRARY_ARDUINOMQTT
#define OTA_CLIENT OTA_CLIENT_HTTPUPDATE
#define THINGSPEAK_USE_ASYNC 0
#define THINGSPEAK_USE_SSL 1

Loading…
Cancel
Save