diff --git a/main/cloud.cpp b/main/cloud.cpp index f94e6b8..365f132 100644 --- a/main/cloud.cpp +++ b/main/cloud.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -23,11 +24,6 @@ using namespace std::chrono_literals; -namespace { -constexpr const char * const TAG = "BOBBYCLOUD"; -} // namespace - - espcpputils::websocket_client cloudClient; bool cloudStarted{}; espchrono::millis_clock::time_point lastCreateTry; @@ -36,9 +32,372 @@ std::string cloudBuffer; std::optional lastCloudCollect; std::optional lastCloudSend; +std::optional lastHeartbeat; bool hasAnnouncedItself{}; +namespace { +constexpr const char * const TAG = "BOBBYCLOUD"; +constexpr const auto json_document_size = 256; +StaticJsonDocument doc; + +template +struct is_duration : std::false_type {}; + +template +struct is_duration> : std::true_type {}; + +template +inline constexpr bool is_duration_v = is_duration<_Tp>::value; + +template +typename std::enable_if< + !std::is_same_v && + !std::is_integral_v && + !is_duration_v && + !std::is_same_v && + !std::is_same_v && + !std::is_same_v && + !std::is_same_v> && + !std::is_same_v && + !std::is_same_v && + !std::is_same_v && + !std::is_same_v && + !std::is_same_v && + !std::is_same_v && + !std::is_same_v && + !std::is_same_v + , void>::type +toArduinoJson(std::string_view key, T value, StaticJsonDocument &doc) +{ + doc[key.data()] = nullptr; +} + +template +typename std::enable_if< + std::is_same_v + , void>::type +toArduinoJson(std::string_view key, T value, StaticJsonDocument &doc) +{ + doc[key.data()] = value; +} + +template +typename std::enable_if< + !std::is_same_v && + std::is_integral_v + , void>::type +toArduinoJson(std::string_view key, T value, StaticJsonDocument &doc) +{ + doc[key.data()] = value; +} + +template +typename std::enable_if< + is_duration_v + , void>::type +toArduinoJson(std::string_view key, T value, StaticJsonDocument &doc) +{ + doc[key.data()] = value.count(); +} + +template +typename std::enable_if< + std::is_same_v + , void>::type +toArduinoJson(std::string_view key, T value, StaticJsonDocument &doc) +{ + doc[key.data()] = value.data(); +} + +template +typename std::enable_if< + std::is_same_v + , void>::type +toArduinoJson(std::string_view key, T value, StaticJsonDocument &doc) +{ + doc[key.data()] = wifi_stack::toString(value); +} + +template +typename std::enable_if< + std::is_same_v + , void>::type +toArduinoJson(std::string_view key, T value, StaticJsonDocument &doc) +{ + doc[key.data()] = wifi_stack::toString(value); +} + +template +typename std::enable_if< + std::is_same_v> + , void>::type +toArduinoJson(std::string_view key, T value, StaticJsonDocument &doc) +{ + doc[key.data()] = value ? wifi_stack::toString(*value) : std::string{}; +} + +template +typename std::enable_if< + std::is_same_v + , void>::type +toArduinoJson(std::string_view key, T value, StaticJsonDocument &doc) +{ + doc[key.data()] = wifi_stack::toString(value); +} + +template +typename std::enable_if< + std::is_same_v + , void>::type +toArduinoJson(std::string_view key, T value, StaticJsonDocument &doc) +{ + doc[key.data()] = std::to_underlying(value); +} + +template +typename std::enable_if< + std::is_same_v + , void>::type +toArduinoJson(std::string_view key, T value, StaticJsonDocument &doc) +{ + doc[key.data()] = std::to_underlying(value); +} + +template +typename std::enable_if< + std::is_same_v + , void>::type +toArduinoJson(std::string_view key, T value, StaticJsonDocument &doc) +{ + doc[key.data()] = std::to_underlying(value); +} + +template +typename std::enable_if< + std::is_same_v + , void>::type +toArduinoJson(std::string_view key, T value, StaticJsonDocument &doc) +{ + doc[key.data()] = std::to_underlying(value); +} + +template +typename std::enable_if< + std::is_same_v + , void>::type +toArduinoJson(std::string_view key, T value, StaticJsonDocument &doc) +{ + doc[key.data()] = std::to_underlying(value); +} + +template +typename std::enable_if< + std::is_same_v + , void>::type +toArduinoJson(std::string_view key, T value, StaticJsonDocument &doc) +{ + doc[key.data()] = std::to_underlying(value); +} + +template +typename std::enable_if< + std::is_same_v + , void>::type +toArduinoJson(std::string_view key, T value, StaticJsonDocument &doc) +{ + doc[key.data()] = std::to_underlying(value); +} + + +template +typename std::enable_if< + !std::is_same_v && + !std::is_integral_v && + !std::is_same_v && + !std::is_same_v && + !std::is_same_v && + !std::is_same_v> && + !std::is_same_v && + !std::is_same_v && + !std::is_same_v && + !std::is_same_v && + !std::is_same_v && + !std::is_same_v && + !std::is_same_v && + !std::is_same_v + , tl::expected>::type +set_config(ConfigWrapper &config, std::string_view newValue) +{ + return tl::make_unexpected("Unsupported config type"); +} + +template +typename std::enable_if< + std::is_same_v + , tl::expected>::type +set_config(ConfigWrapper &config, std::string_view newValue) +{ + if (cpputils::is_in(newValue, "true", "false")) + return configs.write_config(config, newValue == "true"); + else + return tl::make_unexpected(fmt::format("only true and false allowed, not {}", newValue)); +} + +template +typename std::enable_if< + !std::is_same_v && + std::is_integral_v + , tl::expected>::type +set_config(ConfigWrapper &config, std::string_view newValue) +{ + if (auto parsed = cpputils::fromString(newValue)) + return configs.write_config(config, *parsed); + else + return tl::make_unexpected(fmt::format("could not parse {}", newValue)); +} + +template +typename std::enable_if< + std::is_same_v + , tl::expected>::type +set_config(ConfigWrapper &config, std::string_view newValue) +{ + return configs.write_config(config, std::string{newValue}); +} + +template +typename std::enable_if< + std::is_same_v + , tl::expected>::type +set_config(ConfigWrapper &config, std::string_view newValue) +{ + if (const auto parsed = wifi_stack::fromString(newValue); parsed) + return configs.write_config(config, *parsed); + else + return tl::make_unexpected(parsed.error()); +} + +template +typename std::enable_if< + std::is_same_v + , tl::expected>::type +set_config(ConfigWrapper &config, std::string_view newValue) +{ + if (const auto parsed = wifi_stack::fromString(newValue); parsed) + return configs.write_config(config, *parsed); + else + return tl::make_unexpected(parsed.error()); +} + +template +typename std::enable_if< + std::is_same_v> + , tl::expected>::type +set_config(ConfigWrapper &config, std::string_view newValue) +{ + if (newValue.empty()) + return configs.write_config(config, std::nullopt); + else if (const auto parsed = wifi_stack::fromString(newValue); parsed) + return configs.write_config(config, *parsed); + else + return tl::make_unexpected(parsed.error()); +} + +template +typename std::enable_if< + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v + , tl::expected>::type +set_config(ConfigWrapper &config, std::string_view newValue) +{ + if (auto parsed = cpputils::fromString>(newValue)) + return configs.write_config(config, T(*parsed)); + else + return tl::make_unexpected(fmt::format("could not parse {}", newValue)); +} + +void send_config(uint32_t skipCount) +{ + doc.clear(); + doc["type"] = "config"; + + if (!cloudClient.is_connected()) + return; + + bool stop{false}; + + configs.callForEveryConfig([&](auto &config) { + if (skipCount > 0) + { + --skipCount; + return; + } + + if (stop) + return; + + const std::string_view nvsName{config.nvsName()}; + toArduinoJson(nvsName, config.value(), doc); + if (doc.overflowed()) + { + // send data, clear doc and try again + std::string body; + doc.remove(nvsName); + serializeJson(doc, body); + ESP_LOGI(TAG, "body_size: %d, heap free: %d, stack free: %d", body.size(), esp_get_free_heap_size(), uxTaskGetStackHighWaterMark(nullptr)); + const auto timeout = std::chrono::ceil(espchrono::milliseconds32{configs.cloudSettings.cloudTransmitTimeout.value()}).count(); + const auto result_size = cloudClient.send_text(body, timeout); + if (result_size != body.size()) + { + ESP_LOGE(TAG, "send_text failed: %d", result_size); + } + doc["type"] = "config"; + stop = true; + } + }); +} + +void send_single_config(const std::string &nvsName) +{ + if (!cloudClient.is_connected()) + return; + doc.clear(); + doc["type"] = "singleConfig"; + bool success{false}; + configs.callForEveryConfig([&](auto &config) { + if (config.nvsName() == nvsName) + { + toArduinoJson(nvsName, config.value(), doc); + success = true; + } + }); + std::string body; + if (!success) + doc["error"] = "Config not found"; + serializeJson(doc, body); + doc.clear(); + const auto timeout = std::chrono::ceil( + espchrono::milliseconds32{configs.cloudSettings.cloudTransmitTimeout.value()}).count(); + cloudClient.send_text(body, timeout); +} + +void cloudHeartbeat() +{ + if (!cloudClient.is_connected()) + return; + const auto timeout = std::chrono::ceil( + espchrono::milliseconds32{configs.cloudSettings.cloudTransmitTimeout.value()}).count(); + cloudClient.send_text(R"({"type":"heartbeat"})", timeout); +} +} // namespace + void initCloud() { if (configs.cloudSettings.cloudEnabled.value() && @@ -75,6 +434,12 @@ void updateCloud() lastCloudSend = now; } + + if (!lastHeartbeat || now - *lastHeartbeat >= 1500ms) + { + cloudHeartbeat(); + lastHeartbeat = now; + } } void cloudCollect() @@ -212,7 +577,7 @@ void cloudSend() const auto timeout = std::chrono::ceil(espchrono::milliseconds32{configs.cloudSettings.cloudTransmitTimeout.value()}).count(); - if (!hasAnnouncedItself) + if (!hasAnnouncedItself && configs.cloudSettings.cloudEnabled.value()) { std::string helloWorld = getLoginMessage(); ESP_LOGW(TAG, "=====> %s", helloWorld.c_str()); @@ -307,6 +672,120 @@ void cloudSendDisplay(std::string_view data) } } +void cloudEventHandler(void *event_handler_arg, esp_event_base_t event_base, int32_t event_id, void *event_data) +{ + CPP_UNUSED(event_handler_arg); + + const esp_websocket_event_data_t *data = reinterpret_cast(event_data); + + switch (esp_websocket_event_id_t(event_id)) + { + case WEBSOCKET_EVENT_CONNECTED: + ESP_LOGI(TAG, "WEBSOCKET_EVENT_CONNECTED"); + hasAnnouncedItself = false; + break; + case WEBSOCKET_EVENT_DISCONNECTED: + ESP_LOGI(TAG, "WEBSOCKET_EVENT_DISCONNECTED"); + break; + case WEBSOCKET_EVENT_DATA: + { + if (data->op_code != 1) // text + return; + + doc.clear(); + + ESP_LOGI(TAG, "Received: %.*s", data->data_len, data->data_ptr); + + if (const auto err = deserializeJson(doc, data->data_ptr, data->data_len); err) + { + ESP_LOGE(TAG, "deserializeJson() failed with %s", err.c_str()); + return; + } + + const std::string type = doc["type"]; + if (type == "popup") + { + std::string text = doc["msg"]; + std::string id = doc["id"]; + doc.clear(); + ESP_LOGI(TAG, "popup: %s, id: %s", text.c_str(), id.c_str()); + BobbyErrorHandler{}.errorOccured(std::move(text)); + + if (id.empty()) + return; + + auto timeout = std::chrono::ceil(espchrono::milliseconds32{configs.cloudSettings.cloudTransmitTimeout.value()}).count(); + const auto message = fmt::format(R"({{"type":"response","id":"{}"}})", id); + ESP_LOGI(TAG, "sending response: %s", message.c_str()); + cloudClient.send_text(message, timeout); + return; + } + else if (type == "getConfig") + { + JsonVariant _id = doc["id"]; + if (_id.isNull()) + { + ESP_LOGE(TAG, "getConfig: no id"); + return; + } + const auto id = _id.as(); + doc.clear(); + send_config(id); + return; + } + else if (type == "getSingleConfig") + { + JsonVariant nvskey = doc["nvskey"]; + if (nvskey.isNull()) + { + ESP_LOGE(TAG, "getSingleConfig: nvskey is null"); + return; + } + std::string name = nvskey.as(); + doc.clear(); + send_single_config(name); + return; + } + else if (type == "setConfig") + { + std::string name = doc["nvskey"]; + std::string value = doc["value"]; + doc.clear(); + bool success{false}; + configs.callForEveryConfig([&](auto &config){ + const std::string_view nvsName{config.nvsName()}; + + if (nvsName == name) + { + if (const auto result = set_config(config, value); !result) + { + ESP_LOGE(TAG, "set_config() failed with %s", result.error().c_str()); + return; + } + success = true; + } + }); + if (!success) + { + ESP_LOGE(TAG, "set_config() failed with %s", "unknown config"); + return; + } + else + { + send_single_config(name); + } + return; + } + break; + } + case WEBSOCKET_EVENT_ERROR: + ESP_LOGE(TAG, "%s event_id=%s %.*s", event_base, "WEBSOCKET_EVENT_ERROR", data->data_len, data->data_ptr); + break; + default: + ESP_LOGI(TAG, "%s unknown event_id %i", event_base, event_id); + } +} + void createCloud() { hasAnnouncedItself = false; @@ -321,47 +800,20 @@ void createCloud() lastCreateTry = espchrono::millis_clock::now(); const esp_websocket_client_config_t config = { - .uri = configs.cloudUrl.value().c_str(), + .uri = configs.cloudUrl.value().c_str() }; cloudClient = espcpputils::websocket_client{&config}; - cloudClient.register_events(WEBSOCKET_EVENT_CONNECTED, [](void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data){ + cloudClient.register_events(WEBSOCKET_EVENT_CONNECTED, [](void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { hasAnnouncedItself = false; }, nullptr); - cloudClient.register_events(WEBSOCKET_EVENT_DATA, [](void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data){ - using namespace ArduinoJson; - auto data = static_cast(event_data); - - if (data->op_code != 1) // text - return; - - StaticJsonDocument<768> doc; - if (const auto err = deserializeJson(doc, data->data_ptr, data->data_len); err) - { - ESP_LOGE(TAG, "deserializeJson() failed with %s", err.c_str()); - return; - } - - const std::string type = doc["type"]; - if (type == "popup") - { - std::string text = doc["msg"]; - std::string id = doc["id"]; - ESP_LOGI(TAG, "popup: %s, id: %s", text.c_str(), id.c_str()); - BobbyErrorHandler{}.errorOccured(std::move(text)); - - if (id.empty()) - return; - - auto timeout = std::chrono::ceil(espchrono::milliseconds32{configs.cloudSettings.cloudTransmitTimeout.value()}).count(); - const auto message = fmt::format(R"({{"type":"response","id":"{}"}})", id); - ESP_LOGI(TAG, "sending response: %s", message.c_str()); - cloudClient.send_text(message, timeout); - return; - } - }, nullptr); + if (const auto result = cloudClient.register_events(WEBSOCKET_EVENT_ANY, &cloudEventHandler, nullptr); result != ESP_OK) + { + ESP_LOGE(TAG, "register_events() failed with %s", esp_err_to_name(result)); + return; + } if (!cloudClient) {