5#include <ArduinoJson.h>
7#include <esp_http_client.h>
9#ifdef SENSESP_SSL_SUPPORT
10#include <mbedtls/sha256.h>
11#include <mbedtls/ssl.h>
12#include <mbedtls/x509_crt.h>
18#include "elapsedMillis.h"
19#include "esp_arduino_version.h"
34static const char* kRequestPermission =
"readwrite";
36#ifdef SENSESP_SSL_SUPPORT
38static void sha256_to_hex(
const uint8_t* sha256,
char* hex) {
39 for (
int i = 0; i < 32; i++) {
40 sprintf(hex + (i * 2),
"%02x", sha256[i]);
47static int tofu_verify_callback(
void* ctx, mbedtls_x509_crt* crt,
48 int depth, uint32_t* flags) {
56 if (client ==
nullptr) {
57 ESP_LOGW(
"SKWSClient",
"TOFU: No client context, allowing connection");
64 mbedtls_sha256_context sha256_ctx;
65 mbedtls_sha256_init(&sha256_ctx);
66 mbedtls_sha256_starts(&sha256_ctx, 0);
67 mbedtls_sha256_update(&sha256_ctx, crt->raw.p, crt->raw.len);
68 mbedtls_sha256_finish(&sha256_ctx, sha256);
69 mbedtls_sha256_free(&sha256_ctx);
72 sha256_to_hex(sha256, hex);
73 String current_fingerprint = String(hex);
75 ESP_LOGD(
"SKWSClient",
"Server certificate fingerprint: %s", hex);
77 if (!client->is_tofu_enabled()) {
79 ESP_LOGD(
"SKWSClient",
"TOFU disabled, allowing connection");
84 if (!client->has_tofu_fingerprint()) {
86 ESP_LOGI(
"SKWSClient",
"TOFU: First connection, capturing fingerprint: %s", hex);
87 client->set_tofu_fingerprint(current_fingerprint);
93 if (client->get_tofu_fingerprint() == current_fingerprint) {
94 ESP_LOGD(
"SKWSClient",
"TOFU: Fingerprint verified successfully");
100 ESP_LOGE(
"SKWSClient",
"TOFU: Fingerprint mismatch!");
101 ESP_LOGE(
"SKWSClient",
" Expected: %s", client->get_tofu_fingerprint().c_str());
102 ESP_LOGE(
"SKWSClient",
" Received: %s", hex);
104 return MBEDTLS_ERR_X509_CERT_VERIFY_FAILED;
109static esp_err_t tofu_crt_bundle_attach(
void* conf) {
110 mbedtls_ssl_config* ssl_conf =
static_cast<mbedtls_ssl_config*
>(conf);
112 mbedtls_ssl_conf_authmode(ssl_conf, MBEDTLS_SSL_VERIFY_OPTIONAL);
113 mbedtls_ssl_conf_verify(ssl_conf, tofu_verify_callback,
ws_client);
114 ESP_LOGD(
"SKWSClient",
"TOFU verification callback installed");
119static void websocket_event_handler(
void* handler_args,
120 esp_event_base_t base,
121 int32_t event_id,
void* event_data) {
122 esp_websocket_event_data_t* data = (esp_websocket_event_data_t*)event_data;
124 case WEBSOCKET_EVENT_CONNECTED:
127 case WEBSOCKET_EVENT_DISCONNECTED:
130 case WEBSOCKET_EVENT_DATA:
133 if (data->op_code <= 0x2) {
134 ws_client->on_receive_delta((uint8_t*)data->data_ptr, data->data_len);
137 case WEBSOCKET_EVENT_ERROR:
144 elapsedMillis delta_loop_elapsed = 0;
152 if (delta_loop_elapsed > 5) {
153 delta_loop_elapsed = 0;
156 vTaskDelay(pdMS_TO_TICKS(100));
162 std::shared_ptr<SKDeltaQueue> sk_delta_queue,
163 const String& server_address, uint16_t server_port,
189 ESP_LOGD(__FILENAME__,
"Starting SKWSClient");
192 MDNS.addService(
"signalk-sensesp",
"tcp", 80);
208 ESP_LOGW(__FILENAME__,
"Bad access token detected. Setting token to null.");
224 ESP_LOGW(__FILENAME__,
"Websocket client error.");
236 ESP_LOGI(__FILENAME__,
"Subscribing to Signal K listeners...");
247 bool output_available =
false;
248 JsonDocument subscription;
249 subscription[
"context"] =
"vessels.self";
254 if (listeners.size() > 0) {
255 output_available =
true;
256 JsonArray subscribe = subscription[
"subscribe"].to<JsonArray>();
258 for (
size_t i = 0; i < listeners.size(); i++) {
259 auto* listener = listeners.at(i);
260 String sk_path = listener->get_sk_path();
261 int listen_delay = listener->get_listen_delay();
263 JsonObject subscribe_path = subscribe.add<JsonObject>();
265 subscribe_path[
"path"] = sk_path;
266 subscribe_path[
"period"] = listen_delay;
267 ESP_LOGI(__FILENAME__,
"Adding %s subscription with listen_delay %d\n",
268 sk_path.c_str(), listen_delay);
273 if (output_available &&
277 serializeJson(subscription, json_message);
278 ESP_LOGI(__FILENAME__,
"Subscription JSON message:\n %s",
279 json_message.c_str());
280 int result = esp_websocket_client_send_text(
281 client_, json_message.c_str(), json_message.length(),
284 ESP_LOGE(__FILENAME__,
"Subscription send failed (result=%d)", result);
298 constexpr size_t kMaxWsMessageSize = 4096;
299 if (length > kMaxWsMessageSize) {
300 ESP_LOGW(__FILENAME__,
"WebSocket message too large (%u bytes), dropping",
304 std::unique_ptr<char[]> buf(
new char[length + 1]);
305 memcpy(buf.get(), payload, length);
308#ifdef SIGNALK_PRINT_RCV_DELTA
309 ESP_LOGD(__FILENAME__,
"Websocket payload received: %s", buf.get());
312 JsonDocument message;
313 auto error = deserializeJson(message, buf.get());
316 if (message[
"updates"].is<JsonVariant>()) {
320 if (message[
"put"].is<JsonVariant>()) {
325 if (message[
"requestId"].is<JsonVariant>() &&
326 !message[
"put"].is<JsonVariant>()) {
330 ESP_LOGE(__FILENAME__,
"deserializeJson error: %s", error.c_str());
343 JsonArray updates = message[
"updates"];
346 for (
size_t i = 0; i < updates.size(); i++) {
347 JsonObject update = updates[i];
349 JsonArray values = update[
"values"];
351 for (
size_t vi = 0; vi < values.size(); vi++) {
352 JsonDocument value_doc =
353 static_cast<JsonDocument
>(
static_cast<JsonObject
>((values[vi])));
357 constexpr size_t kMaxReceivedUpdates = 20;
360 ESP_LOGW(__FILENAME__,
361 "Dropping oldest received update (queue full)");
379 const std::vector<SKPutListener*>& put_listeners =
387 const char* path = doc[
"path"];
388 JsonObject value = doc.as<JsonObject>();
390 for (
size_t i = 0; i < listeners.size(); i++) {
397 for (
size_t i = 0; i < put_listeners.size(); i++) {
420 JsonArray puts = message[
"put"];
421 bool all_matched =
true;
422 for (
size_t i = 0; i < puts.size(); i++) {
423 JsonObject value = puts[i];
424 const char* path = value[
"path"];
425 bool matched =
false;
428 const std::vector<SKPutListener*>& listeners =
430 for (
size_t j = 0; j < listeners.size(); j++) {
434 constexpr size_t kMaxReceivedUpdates = 20;
437 ESP_LOGW(__FILENAME__,
438 "Dropping oldest received update (queue full)");
454 JsonDocument put_response;
455 put_response[
"requestId"] = message[
"requestId"];
457 put_response[
"state"] =
"COMPLETED";
458 put_response[
"statusCode"] = 200;
460 put_response[
"state"] =
"FAILED";
461 put_response[
"statusCode"] = 405;
463 String response_text;
464 serializeJson(put_response, response_text);
465 int result = esp_websocket_client_send_text(
466 client_, response_text.c_str(), response_text.length(),
469 ESP_LOGE(__FILENAME__,
"PUT response send failed (result=%d)", result);
483 int result = esp_websocket_client_send_text(
486 ESP_LOGE(__FILENAME__,
"sendTXT failed (result=%d)", result);
492 uint16_t& server_port) {
495 int num = MDNS.queryService(
"signalk-wss",
"tcp");
499 ESP_LOGI(__FILENAME__,
"Found Signal K server via mDNS (signalk-wss)");
502 num = MDNS.queryService(
"signalk-ws",
"tcp");
509 ESP_LOGI(__FILENAME__,
"Found Signal K server via mDNS (signalk-ws)");
512#if ESP_ARDUINO_VERSION_MAJOR < 3
513 server_address = MDNS.IP(0).toString();
515 server_address = MDNS.address(0).toString();
517 server_port = MDNS.port(0);
518 ESP_LOGI(__FILENAME__,
"Found server %s (port %d)", server_address.c_str(),
524static esp_err_t detect_ssl_event_handler(esp_http_client_event_t* evt) {
525 if (evt->event_id == HTTP_EVENT_ON_HEADER) {
527 if (strcasecmp(evt->header_key,
"Location") == 0) {
528 String* location =
static_cast<String*
>(evt->user_data);
529 *location = evt->header_value;
541 ESP_LOGD(__FILENAME__,
"Probing for SSL redirect at %s", url.c_str());
545 esp_http_client_config_t config = {};
546 config.url = url.c_str();
547 config.disable_auto_redirect =
true;
548 config.timeout_ms = 10000;
549 config.event_handler = detect_ssl_event_handler;
550 config.user_data = &location;
552 esp_http_client_handle_t client = esp_http_client_init(&config);
553 if (client ==
nullptr) {
554 ESP_LOGE(__FILENAME__,
"Failed to initialize HTTP client");
558 esp_err_t err = esp_http_client_perform(client);
559 int status_code = esp_http_client_get_status_code(client);
560 esp_http_client_cleanup(client);
563 ESP_LOGD(__FILENAME__,
"HTTP request failed: %s", esp_err_to_name(err));
567 if ((status_code == 301 || status_code == 302 ||
568 status_code == 307 || status_code == 308) &&
569 location.startsWith(
"https://")) {
570 ESP_LOGI(__FILENAME__,
"SSL redirect detected, enabling HTTPS/WSS");
593 if (!provisioner || !provisioner->is_connected()) {
594 ESP_LOGI(__FILENAME__,
595 "Network is not yet up. SignalK client connection will be "
596 "initiated when the link comes up.");
600 ESP_LOGI(__FILENAME__,
"Initiating websocket connection with server...");
605 ESP_LOGE(__FILENAME__,
606 "No Signal K server found in network when using mDNS service!");
608 ESP_LOGI(__FILENAME__,
609 "Signal K server has been found at address %s:%d by mDNS.",
618 ESP_LOGD(__FILENAME__,
619 "Websocket is connecting to Signal K server on address %s:%d",
628 ESP_LOGD(__FILENAME__,
629 "Websocket is not connecting to Signal K server because host and "
630 "port are not defined.");
635 if (this->
polling_href_.length() > 0 && this->polling_href_.startsWith(
"/")) {
644 ESP_LOGD(__FILENAME__,
"No prior authorization token present.");
658 const uint16_t server_port) {
659 String protocol =
ssl_enabled_ ?
"https://" :
"http://";
660 String url = protocol + server_address +
":" + server_port +
661 "/signalk/v1/stream";
662 ESP_LOGD(__FILENAME__,
"Testing token with url %s", url.c_str());
664 const String full_token = String(
"Bearer ") +
auth_token_;
665 ESP_LOGD(__FILENAME__,
"Authorization: %.8s...[redacted]", full_token.c_str());
667 esp_http_client_config_t config = {};
668 config.url = url.c_str();
669 config.timeout_ms = 10000;
670#ifdef SENSESP_SSL_SUPPORT
672 config.crt_bundle_attach = tofu_crt_bundle_attach;
673 config.skip_cert_common_name_check =
true;
677 esp_http_client_handle_t client = esp_http_client_init(&config);
678 if (client ==
nullptr) {
679 ESP_LOGE(__FILENAME__,
"Failed to initialize HTTP client");
684 esp_http_client_set_header(client,
"Authorization", full_token.c_str());
687 esp_err_t err = esp_http_client_open(client, 0);
689 ESP_LOGE(__FILENAME__,
"Failed to open HTTP connection: %s", esp_err_to_name(err));
690 esp_http_client_cleanup(client);
695 int content_length = esp_http_client_fetch_headers(client);
696 int http_code = esp_http_client_get_status_code(client);
698 ESP_LOGD(__FILENAME__,
"Testing resulted in http status %d", http_code);
702 if (content_length > 0 && content_length < 4096) {
703 char* buffer =
new char[content_length + 1];
704 int read_len = esp_http_client_read(client, buffer, content_length);
705 buffer[read_len > 0 ? read_len : 0] =
'\0';
706 payload = String(buffer);
712 while ((read_len = esp_http_client_read(client, buffer,
sizeof(buffer) - 1)) > 0) {
713 buffer[read_len] =
'\0';
714 payload += String(buffer);
715 if (payload.length() > 4096)
break;
719 esp_http_client_close(client);
720 esp_http_client_cleanup(client);
722 if (payload.length() > 0) {
723 ESP_LOGD(__FILENAME__,
"Returned payload (%d bytes): %s",
724 payload.length(), payload.c_str());
727 if (http_code == 426) {
730 ESP_LOGD(__FILENAME__,
"Attempting to connect to Signal K Websocket...");
733 this->
connect_ws(server_address, server_port);
734 }
else if (http_code == 401) {
737 ESP_LOGW(__FILENAME__,
"Token rejected (401), requesting new access");
741 }
else if (http_code > 0) {
744 ESP_LOGE(__FILENAME__,
"HTTP request failed with code %d", http_code);
750 const uint16_t server_port) {
751 ESP_LOGD(__FILENAME__,
"Sending access request (client_id=%s, ssl=%d)",
764 doc[
"permissions"] = kRequestPermission;
765 String json_req =
"";
766 serializeJson(doc, json_req);
768 ESP_LOGD(__FILENAME__,
"Access request: %s", json_req.c_str());
770 String protocol =
ssl_enabled_ ?
"https://" :
"http://";
771 String url = protocol + server_address +
":" + server_port +
772 "/signalk/v1/access/requests";
773 ESP_LOGD(__FILENAME__,
"Access request url: %s", url.c_str());
775 esp_http_client_config_t config = {};
776 config.url = url.c_str();
777 config.method = HTTP_METHOD_POST;
778 config.timeout_ms = 10000;
779#ifdef SENSESP_SSL_SUPPORT
781 config.crt_bundle_attach = tofu_crt_bundle_attach;
782 config.skip_cert_common_name_check =
true;
786 esp_http_client_handle_t client = esp_http_client_init(&config);
787 if (client ==
nullptr) {
788 ESP_LOGE(__FILENAME__,
"Failed to initialize HTTP client");
794 esp_http_client_set_header(client,
"Content-Type",
"application/json");
797 esp_err_t err = esp_http_client_open(client, json_req.length());
799 ESP_LOGE(__FILENAME__,
"Failed to open HTTP connection: %s", esp_err_to_name(err));
800 esp_http_client_cleanup(client);
805 int write_len = esp_http_client_write(client, json_req.c_str(), json_req.length());
806 if (write_len < 0 || write_len != (
int)json_req.length()) {
807 ESP_LOGE(__FILENAME__,
"Failed to write request body (wrote %d of %d bytes)",
808 write_len, json_req.length());
809 esp_http_client_close(client);
810 esp_http_client_cleanup(client);
815 int content_length = esp_http_client_fetch_headers(client);
816 int http_code = esp_http_client_get_status_code(client);
818 ESP_LOGD(__FILENAME__,
"HTTP response: code=%d, content_length=%d", http_code, content_length);
824 while ((read_len = esp_http_client_read(client, buffer,
sizeof(buffer) - 1)) > 0) {
825 buffer[read_len] =
'\0';
826 payload += String(buffer);
827 if (payload.length() > 4096)
break;
829 ESP_LOGD(__FILENAME__,
"Response payload (%d bytes): %s",
830 payload.length(), payload.c_str());
832 esp_http_client_close(client);
833 esp_http_client_cleanup(client);
836 deserializeJson(doc, payload.c_str());
837 String state = doc[
"state"].is<
const char*>() ? doc[
"state"].as<String>() :
"";
838 String href = doc[
"href"].is<
const char*>() ? doc[
"href"].as<String>() :
"";
839 String message = doc[
"message"].is<
const char*>() ? doc[
"message"].as<String>() :
"";
841 ESP_LOGD(__FILENAME__,
"Access request response: http=%d, state=%s, href=%s",
842 http_code, state.c_str(), href.c_str());
843 if (message.length() > 0) {
844 ESP_LOGI(__FILENAME__,
"Server message: %s", message.c_str());
849 if (http_code == 400 && href.length() > 0 && href.startsWith(
"/")) {
850 ESP_LOGI(__FILENAME__,
"Existing request found, will poll href: %s", href.c_str());
859 if (http_code == 202 && href.length() > 0 && href.startsWith(
"/")) {
868 if (http_code == 404) {
869 ESP_LOGI(__FILENAME__,
870 "Server security disabled (404 on access request) — connecting "
873 this->
connect_ws(server_address, server_port);
878 ESP_LOGW(__FILENAME__,
"Cannot handle response: http=%d, state=%s", http_code, state.c_str());
883 const uint16_t server_port,
885 ESP_LOGD(__FILENAME__,
"Polling SK Server for authentication token");
887 String protocol =
ssl_enabled_ ?
"https://" :
"http://";
888 String url = protocol + server_address +
":" + server_port + href;
890 esp_http_client_config_t config = {};
891 config.url = url.c_str();
892 config.timeout_ms = 10000;
893#ifdef SENSESP_SSL_SUPPORT
895 config.crt_bundle_attach = tofu_crt_bundle_attach;
896 config.skip_cert_common_name_check =
true;
900 esp_http_client_handle_t client = esp_http_client_init(&config);
901 if (client ==
nullptr) {
902 ESP_LOGE(__FILENAME__,
"Failed to initialize HTTP client");
908 esp_err_t err = esp_http_client_open(client, 0);
910 ESP_LOGE(__FILENAME__,
"Failed to open HTTP connection: %s", esp_err_to_name(err));
911 esp_http_client_cleanup(client);
916 int content_length = esp_http_client_fetch_headers(client);
917 int http_code = esp_http_client_get_status_code(client);
921 if (content_length > 0 && content_length < 4096) {
922 char* buffer =
new char[content_length + 1];
923 int read_len = esp_http_client_read(client, buffer, content_length);
924 buffer[read_len > 0 ? read_len : 0] =
'\0';
925 payload = String(buffer);
931 while ((read_len = esp_http_client_read(client, buffer,
sizeof(buffer) - 1)) > 0) {
932 buffer[read_len] =
'\0';
933 payload += String(buffer);
934 if (payload.length() > 4096)
break;
938 ESP_LOGD(__FILENAME__,
"Poll response: http=%d, payload=%s", http_code, payload.c_str());
940 esp_http_client_close(client);
941 esp_http_client_cleanup(client);
943 if (http_code == 200 || http_code == 202) {
945 auto error = deserializeJson(doc, payload.c_str());
947 ESP_LOGW(__FILENAME__,
"WARNING: Could not deserialize http payload.");
948 ESP_LOGW(__FILENAME__,
"DeserializationError: %s", error.c_str());
951 String state = doc[
"state"];
952 ESP_LOGD(__FILENAME__,
"%s", state.c_str());
953 if (state ==
"PENDING") {
957 if (state ==
"COMPLETED") {
958 JsonObject access_req = doc[
"accessRequest"];
959 String permission = access_req[
"permission"];
964 if (permission ==
"DENIED") {
965 ESP_LOGW(__FILENAME__,
"Permission denied");
970 if (permission ==
"APPROVED") {
971 ESP_LOGI(__FILENAME__,
"Permission granted");
972 String token = access_req[
"token"];
975 this->
connect_ws(server_address, server_port);
980 if (http_code == 404 || http_code == 500) {
985 ESP_LOGD(__FILENAME__,
986 "Got %d polling access request — clearing stale href.",
994 ESP_LOGW(__FILENAME__,
995 "Can't handle response %d to pending access request.\n",
1006 String path =
"/signalk/v1/stream?subscribe=none";
1007 String url = protocol +
"://" + host +
":" + String(port) + path;
1009 ESP_LOGD(__FILENAME__,
"Connecting WebSocket to %s", url.c_str());
1014 auth_header = String(
"Authorization: Bearer ") +
auth_token_ +
"\r\n";
1018 esp_websocket_client_config_t config = {};
1019 config.uri = url.c_str();
1021 config.buffer_size = 1024;
1022 if (auth_header.length() > 0) {
1023 config.headers = auth_header.c_str();
1026#ifdef SENSESP_SSL_SUPPORT
1030 config.crt_bundle_attach = tofu_crt_bundle_attach;
1031 config.skip_cert_common_name_check =
true;
1037 esp_websocket_client_stop(
client_);
1038 esp_websocket_client_destroy(
client_);
1042 client_ = esp_websocket_client_init(&config);
1044 ESP_LOGE(__FILENAME__,
"Failed to initialize WebSocket client");
1050 esp_websocket_register_events(
client_, WEBSOCKET_EVENT_ANY,
1051 websocket_event_handler,
nullptr);
1054 esp_err_t err = esp_websocket_client_start(
client_);
1055 if (err != ESP_OK) {
1056 ESP_LOGE(__FILENAME__,
"Failed to start WebSocket client: %s",
1057 esp_err_to_name(err));
1058 esp_websocket_client_destroy(
client_);
1064 ESP_LOGD(__FILENAME__,
"WebSocket client started, waiting for connection...");
1076 esp_websocket_client_stop(
client_);
1077 esp_websocket_client_destroy(
client_);
1085 std::vector<String> deltas;
1087 for (
const auto& delta : deltas) {
1088 int send_result = esp_websocket_client_send_text(
1090 if (send_result < 0) {
1091 ESP_LOGE(__FILENAME__,
1092 "WebSocket send failed (result=%d), restarting",
1119 if (config[
"sk_address"].is<String>()) {
1122 if (config[
"sk_port"].is<int>()) {
1125 if (config[
"use_mdns"].is<bool>()) {
1126 this->
use_mdns_ = config[
"use_mdns"].as<
bool>();
1128 if (config[
"token"].is<String>()) {
1131 if (config[
"client_id"].is<String>()) {
1132 this->
client_id_ = config[
"client_id"].as<String>();
1134 if (config[
"polling_href"].is<String>()) {
1135 String href = config[
"polling_href"].as<String>();
1140 if (config[
"ssl_enabled"].is<bool>()) {
1143 if (config[
"tofu_enabled"].is<bool>()) {
1146 if (config[
"tofu_fingerprint"].is<String>()) {
1162 return "Authorizing with SignalK";
1166 return "Connecting";
1168 return "Disconnected";
virtual bool load() override
Load and populate the object from a persistent storage.
virtual bool save() override
Save the object to a persistent storage.
FileSystemSaveable(const String &config_path)
An Obervable class that listens for Signal K stream deltas and notifies any observers of value change...
static bool take_semaphore(uint64_t timeout_ms=0)
static void release_semaphore()
virtual void parse_value(const JsonObject &json)
static const std::vector< SKListener * > & get_listeners()
An Obervable class that listens for Signal K PUT requests coming over the websocket connection and no...
static const std::vector< SKPutListener * > & get_listeners()
virtual void parse_value(const JsonObject &put)=0
static void handle_response(JsonDocument &response)
The websocket connection to the Signal K server.
std::list< JsonDocument > received_updates_
void poll_access_request(const String host, const uint16_t port, const String href)
SKWSClient(const String &config_path, std::shared_ptr< SKDeltaQueue > sk_delta_queue, const String &server_address, uint16_t server_port, bool use_mdns=true)
void schedule_reconnect()
void on_receive_delta(uint8_t *payload, size_t length)
Called when the websocket receives a delta.
void process_received_updates()
Loop through the received updates and process them.
void connect_ws(const String &host, const uint16_t port)
Integrator< int, int > delta_tx_count_producer_
TaskQueueProducer< SKWSConnectionState > connection_state_
SKWSConnectionState get_connection_state()
void on_receive_put(JsonDocument &message)
Called when a PUT event is received.
void release_received_updates_semaphore()
void send_access_request(const String host, const uint16_t port)
Integrator< int, int > delta_rx_count_producer_
virtual bool to_json(JsonObject &root) override final
esp_websocket_client_handle_t client_
void on_error()
Called when the websocket connection encounters an error.
void sendTXT(String &payload)
Send some processed data to the websocket.
bool get_mdns_service(String &server_address, uint16_t &server_port)
void on_disconnected()
Called when the websocket connection is disconnected.
void subscribe_listeners()
Subscribes the SK delta paths to the websocket.
void on_receive_updates(JsonDocument &message)
Called when a delta update is received.
uint16_t conf_server_port_
void test_token(const String host, const uint16_t port)
void set_connection_state(SKWSConnectionState state)
bool take_received_updates_semaphore(unsigned long int timeout_ms=0)
virtual bool from_json(const JsonObject &config) override final
String get_connection_status()
Get a String representation of the current connection state.
TaskQueueProducer< int > delta_tx_tick_producer_
Emits the number of deltas sent since last report.
void reset_reconnect_interval()
std::shared_ptr< SKDeltaQueue > sk_delta_queue_
String conf_server_address_
void on_connected()
Called when the websocket connection is established.
static std::shared_ptr< SensESPApp > get()
Get the singleton instance of the SensESPApp.
static String get_hostname()
Get the current hostname.
void emit(const SKWSConnectionState &new_value)
std::shared_ptr< reactesp::EventLoop > event_loop()
String generate_uuid4()
Generate a random UUIDv4 string.
constexpr int kWsClientTaskStackSize
constexpr TickType_t kWsSendTimeoutTicks
void ExecuteWebSocketTask(void *)
constexpr int kWsTransportTaskStackSize