diff --git a/code/espurna/config/general.h b/code/espurna/config/general.h
index beba2bc8..bb0851ee 100644
--- a/code/espurna/config/general.h
+++ b/code/espurna/config/general.h
@@ -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
// -----------------------------------------------------------------------------
diff --git a/code/espurna/espurna.ino b/code/espurna/espurna.ino
index f50a9d12..e89e68ef 100644
--- a/code/espurna/espurna.ino
+++ b/code/espurna/espurna.ino
@@ -59,6 +59,7 @@ along with this program. If not, see .
#include "web.h"
#include "ws.h"
+#include "libs/URL.h"
#include "libs/HeapStats.h"
using void_callback_f = void (*)();
diff --git a/code/espurna/libs/AsyncClientHelpers.h b/code/espurna/libs/AsyncClientHelpers.h
new file mode 100644
index 00000000..446d59a8
--- /dev/null
+++ b/code/espurna/libs/AsyncClientHelpers.h
@@ -0,0 +1,13 @@
+// -----------------------------------------------------------------------------
+// AsyncClient helpers
+// -----------------------------------------------------------------------------
+
+#pragma once
+
+enum class AsyncClientState {
+ Disconnected,
+ Connecting,
+ Connected
+};
+
+
diff --git a/code/espurna/libs/URL.h b/code/espurna/libs/URL.h
index 43cfe124..f8b786c6 100644
--- a/code/espurna/libs/URL.h
+++ b/code/espurna/libs/URL.h
@@ -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) {
}
}
+
diff --git a/code/espurna/static/digicert_high_assurance_pem.h b/code/espurna/static/digicert_high_assurance_pem.h
new file mode 100644
index 00000000..dca31ebc
--- /dev/null
+++ b/code/espurna/static/digicert_high_assurance_pem.h
@@ -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
+
+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";
diff --git a/code/espurna/thingspeak.h b/code/espurna/thingspeak.h
new file mode 100644
index 00000000..14aed294
--- /dev/null
+++ b/code/espurna/thingspeak.h
@@ -0,0 +1,26 @@
+/*
+
+THINGSPEAK MODULE
+
+Copyright (C) 2019 by Xose Pérez
+
+*/
+
+#pragma once
+
+#if THINGSPEAK_SUPPORT
+
+#if THINGSPEAK_USE_ASYNC
+#include
+#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
diff --git a/code/espurna/thinkspeak.ino b/code/espurna/thinkspeak.ino
index 16263cb9..08900f4b 100644
--- a/code/espurna/thinkspeak.ino
+++ b/code/espurna/thinkspeak.ino
@@ -8,16 +8,24 @@ Copyright (C) 2019 by Xose Pérez
#if THINGSPEAK_SUPPORT
+#include
+
#include "broker.h"
+#include "thingspeak.h"
#include "libs/URL.h"
+#include "libs/SecureClientHelpers.h"
+#include "libs/AsyncClientHelpers.h"
-#if THINGSPEAK_USE_ASYNC
-#include
+#if SECURE_CLIENT != SECURE_CLIENT_NONE
+
+#if THINGSPEAK_SECURE_CLIENT_INCLUDE_CA
+#include "static/thingspeak_client_trusted_root_ca.h"
#else
-#include
+#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(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(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(_tspk_sc_config);
+ if (!client->beforeConnected()) {
+ return;
+ }
+
+ _tspkPost(&client->get(), url);
+ return;
+ }
+ #endif
+
+ if (url.protocol == "http") {
+ auto client = std::make_unique();
+ _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("tspkKey", THINGSPEAK_APIKEY));
--_tspk_tries;
- _tspkPost();
+ _tspkPost(getSetting("tspkAddress", THINGSPEAK_ADDRESS));
}
}
diff --git a/code/test/build/extra/secure_client.h b/code/test/build/extra/secure_client.h
index 11a34e9e..94223ae8 100644
--- a/code/test/build/extra/secure_client.h
+++ b/code/test/build/extra/secure_client.h
@@ -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