From a8a82f9627e4701ca4b6dc0f7c58fa503f40c423 Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 31 Mar 2026 08:44:30 +0800 Subject: [PATCH] Add cloud bridge and provisioning support for ESP32 gateway - Introduced DaliCloudBridge for MQTT communication with backend. - Added GatewayProvisioningStore for persisting cloud connection settings using NVS. - Updated CMakeLists.txt to include new source files. - Enhanced README.md with usage examples and configuration details. --- CMakeLists.txt | 3 + README.md | 71 +++++++++ idf_component.yml | 2 + include/dali.hpp | 2 + include/gateway_cloud.hpp | 58 ++++++++ include/gateway_provisioning.hpp | 27 ++++ src/gateway_cloud.cpp | 241 +++++++++++++++++++++++++++++++ src/gateway_provisioning.cpp | 127 ++++++++++++++++ 8 files changed, 531 insertions(+) create mode 100644 include/gateway_cloud.hpp create mode 100644 include/gateway_provisioning.hpp create mode 100644 src/gateway_cloud.cpp create mode 100644 src/gateway_provisioning.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index cbf4d60..fff45fd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,7 +10,10 @@ idf_component_register( "src/sequence.cpp" "src/sequence_store.cpp" "src/color.cpp" + "src/gateway_cloud.cpp" + "src/gateway_provisioning.cpp" INCLUDE_DIRS "include" + REQUIRES mqtt cjson nvs_flash ) set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17) diff --git a/README.md b/README.md index c8b9f24..8359806 100644 --- a/README.md +++ b/README.md @@ -47,3 +47,74 @@ std::vector rgb = dali.dt8.getColourRGB(5); - Optional query support: provide a `transact` callback that returns the gateway reply; otherwise, query methods return `std::nullopt`. - `Dali` facade in `include/dali.hpp` mirrors `lib/dali/dali.dart` and wires `base`, `decode`, `dt1`, `dt8`, and `addr` together. - The `t`, `d`, and `g` parameters in Dart are not required here; timing/gateway selection is driven by your callbacks. + +## Cloud Bridge (ESP32 Gateway) + +The component now includes `DaliCloudBridge` in `include/gateway_cloud.hpp` to connect ESP32 gateways to the backend MQTT broker. + +### Topics + +- Downlink: `devices//down` +- Uplink: `devices//up` +- Status: `devices//status` +- Register: `devices//register` + +### Downlink JSON Envelope + +```json +{ + "type": "dali_cmd", + "seq": "123", + "op": "send|send_ext|query", + "addr": 5, + "cmd": 160 +} +``` + +### Uplink JSON Envelope + +```json +{ + "type": "dali_resp", + "seq": "123", + "op": "query", + "ok": true, + "data": 255 +} +``` + +### Usage + +```cpp +GatewayCloudConfig cfg; +cfg.brokerURI = "mqtt://192.168.1.100:1883"; +cfg.deviceID = "A1B2C3D4E5F6"; +cfg.username = "device"; +cfg.password = "A1B2C3D4E5F6"; + +DaliCloudBridge bridge(comm); +if (bridge.start(cfg)) { + bridge.publishStatus("online"); +} +``` + +### Provisioning via NVS + +Use `GatewayProvisioningStore` to persist cloud connection settings: + +```cpp +GatewayProvisioningStore store; +GatewayCloudConfig cfg; +cfg.brokerURI = "mqtt://192.168.1.100:1883"; +cfg.deviceID = "A1B2C3D4E5F6"; +cfg.username = "device"; +cfg.password = "A1B2C3D4E5F6"; + +store.save(cfg); + +GatewayCloudConfig loaded; +if (store.load(&loaded) == ESP_OK) { + DaliCloudBridge bridge(comm); + bridge.start(loaded); +} +``` diff --git a/idf_component.yml b/idf_component.yml index c93aff6..c9252b5 100644 --- a/idf_component.yml +++ b/idf_component.yml @@ -1,5 +1,7 @@ dependencies: idf: '>=5.1' + espressif/esp-mqtt: '*' + espressif/cjson: '*' description: ESP DALI CPP library component for ESP-IDF repository: git://github.com/tony-cloud/esp-dali-cpp.git repository_info: diff --git a/include/dali.hpp b/include/dali.hpp index 68e7105..986f660 100644 --- a/include/dali.hpp +++ b/include/dali.hpp @@ -9,6 +9,8 @@ #include "dt1.hpp" #include "dt8.hpp" #include "addr.hpp" +#include "gateway_cloud.hpp" +#include "gateway_provisioning.hpp" #include "color.hpp" #include "errors.hpp" #include "log.hpp" diff --git a/include/gateway_cloud.hpp b/include/gateway_cloud.hpp new file mode 100644 index 0000000..ce5bc77 --- /dev/null +++ b/include/gateway_cloud.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include "dali_comm.hpp" + +#include +#include + +#ifdef ESP_PLATFORM +#include "esp_event_base.h" +#include "mqtt_client.h" +#endif + +struct GatewayCloudConfig { + std::string brokerURI; + std::string deviceID; + std::string username = "device"; + std::string password; + std::string topicPrefix = "devices"; + int qos = 1; +}; + +// DaliCloudBridge bridges MQTT cloud topics and local DALI frames. +class DaliCloudBridge { + public: + explicit DaliCloudBridge(DaliComm& comm); + + bool start(const GatewayCloudConfig& config); + void stop(); + bool isConnected() const; + + bool publishStatus(const std::string& status); + bool publishRegister(const std::string& payloadJson); + + private: +#ifdef ESP_PLATFORM + static void mqttEventHandler(void* handler_args, + esp_event_base_t base, + int32_t event_id, + void* event_data); + void onMqttEvent(esp_mqtt_event_handle_t event); +#endif + + bool handleDownlink(const std::string& payload); + bool publishJSON(const std::string& topic, const std::string& payloadJson); + + std::string topicDown() const; + std::string topicUp() const; + std::string topicStatus() const; + std::string topicRegister() const; + + DaliComm& comm_; + GatewayCloudConfig config_; + std::atomic connected_{false}; + +#ifdef ESP_PLATFORM + esp_mqtt_client_handle_t client_ = nullptr; +#endif +}; diff --git a/include/gateway_provisioning.hpp b/include/gateway_provisioning.hpp new file mode 100644 index 0000000..dfdf99a --- /dev/null +++ b/include/gateway_provisioning.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "gateway_cloud.hpp" + +#include + +#ifdef ESP_PLATFORM +extern "C" { +#include "esp_err.h" +} +#else +using esp_err_t = int; +#endif + +// Stores/loads gateway cloud configuration using ESP-IDF NVS. +class GatewayProvisioningStore { + public: + explicit GatewayProvisioningStore(std::string nvsNamespace = "dali_cloud") + : nvsNamespace_(std::move(nvsNamespace)) {} + + esp_err_t save(const GatewayCloudConfig& config) const; + esp_err_t load(GatewayCloudConfig* config) const; + esp_err_t clear() const; + + private: + std::string nvsNamespace_; +}; diff --git a/src/gateway_cloud.cpp b/src/gateway_cloud.cpp new file mode 100644 index 0000000..3633307 --- /dev/null +++ b/src/gateway_cloud.cpp @@ -0,0 +1,241 @@ +#include "gateway_cloud.hpp" + +#include + +#ifdef ESP_PLATFORM +extern "C" { +#include "cJSON.h" +#include "esp_log.h" +} + +namespace { +constexpr const char* kTag = "dali_cloud_bridge"; + +std::string toString(const cJSON* item) { + if (item == nullptr) { + return ""; + } + if (cJSON_IsString(item) && item->valuestring != nullptr) { + return std::string(item->valuestring); + } + if (cJSON_IsNumber(item)) { + std::ostringstream oss; + oss << item->valuedouble; + return oss.str(); + } + return ""; +} + +int toInt(const cJSON* item, int fallback) { + if (item == nullptr || !cJSON_IsNumber(item)) { + return fallback; + } + return item->valueint; +} + +} // namespace +#endif + +DaliCloudBridge::DaliCloudBridge(DaliComm& comm) : comm_(comm) {} + +bool DaliCloudBridge::start(const GatewayCloudConfig& config) { + config_ = config; + if (config_.brokerURI.empty() || config_.deviceID.empty()) { + return false; + } + +#ifdef ESP_PLATFORM + if (client_ != nullptr) { + stop(); + } + + esp_mqtt_client_config_t mqttCfg = {}; + mqttCfg.broker.address.uri = config_.brokerURI.c_str(); + mqttCfg.credentials.username = config_.username.empty() ? nullptr : config_.username.c_str(); + mqttCfg.credentials.authentication.password = + config_.password.empty() ? nullptr : config_.password.c_str(); + + client_ = esp_mqtt_client_init(&mqttCfg); + if (client_ == nullptr) { + ESP_LOGE(kTag, "esp_mqtt_client_init failed"); + return false; + } + + esp_mqtt_client_register_event(client_, ESP_EVENT_ANY_ID, &DaliCloudBridge::mqttEventHandler, + this); + if (esp_mqtt_client_start(client_) != ESP_OK) { + ESP_LOGE(kTag, "esp_mqtt_client_start failed"); + esp_mqtt_client_destroy(client_); + client_ = nullptr; + return false; + } + return true; +#else + (void)config; + return false; +#endif +} + +void DaliCloudBridge::stop() { +#ifdef ESP_PLATFORM + connected_.store(false); + if (client_ != nullptr) { + esp_mqtt_client_stop(client_); + esp_mqtt_client_destroy(client_); + client_ = nullptr; + } +#endif +} + +bool DaliCloudBridge::isConnected() const { return connected_.load(); } + +bool DaliCloudBridge::publishStatus(const std::string& status) { + std::string payload = "{\"type\":\"status\",\"status\":\"" + status + "\"}"; + return publishJSON(topicStatus(), payload); +} + +bool DaliCloudBridge::publishRegister(const std::string& payloadJson) { + return publishJSON(topicRegister(), payloadJson); +} + +bool DaliCloudBridge::publishJSON(const std::string& topic, const std::string& payloadJson) { +#ifdef ESP_PLATFORM + if (client_ == nullptr || !connected_.load()) { + return false; + } + int msgID = esp_mqtt_client_publish(client_, topic.c_str(), payloadJson.c_str(), + static_cast(payloadJson.size()), config_.qos, 0); + return msgID >= 0; +#else + (void)topic; + (void)payloadJson; + return false; +#endif +} + +std::string DaliCloudBridge::topicDown() const { + return config_.topicPrefix + "/" + config_.deviceID + "/down"; +} + +std::string DaliCloudBridge::topicUp() const { + return config_.topicPrefix + "/" + config_.deviceID + "/up"; +} + +std::string DaliCloudBridge::topicStatus() const { + return config_.topicPrefix + "/" + config_.deviceID + "/status"; +} + +std::string DaliCloudBridge::topicRegister() const { + return config_.topicPrefix + "/" + config_.deviceID + "/register"; +} + +bool DaliCloudBridge::handleDownlink(const std::string& payload) { +#ifdef ESP_PLATFORM + cJSON* root = cJSON_Parse(payload.c_str()); + if (root == nullptr) { + return false; + } + + const cJSON* seqItem = cJSON_GetObjectItemCaseSensitive(root, "seq"); + const cJSON* opItem = cJSON_GetObjectItemCaseSensitive(root, "op"); + const cJSON* addrItem = cJSON_GetObjectItemCaseSensitive(root, "addr"); + const cJSON* cmdItem = cJSON_GetObjectItemCaseSensitive(root, "cmd"); + + const std::string seq = toString(seqItem); + const std::string op = toString(opItem).empty() ? "send" : toString(opItem); + const int addr = toInt(addrItem, -1); + const int cmd = toInt(cmdItem, -1); + + bool ok = false; + bool hasData = false; + int data = -1; + std::string error = ""; + + if (addr < 0 || addr > 255 || cmd < 0 || cmd > 255) { + error = "invalid addr/cmd"; + } else if (op == "send") { + ok = comm_.sendRaw(static_cast(addr), static_cast(cmd)); + } else if (op == "send_ext") { + ok = comm_.sendExtRaw(static_cast(addr), static_cast(cmd)); + } else if (op == "query") { + auto response = comm_.queryRaw(static_cast(addr), static_cast(cmd)); + if (response.has_value()) { + ok = true; + hasData = true; + data = static_cast(response.value()); + } else { + error = "no response"; + } + } else { + error = "unsupported op"; + } + + cJSON* resp = cJSON_CreateObject(); + cJSON_AddStringToObject(resp, "type", "dali_resp"); + cJSON_AddStringToObject(resp, "seq", seq.c_str()); + cJSON_AddStringToObject(resp, "op", op.c_str()); + cJSON_AddBoolToObject(resp, "ok", ok); + if (hasData) { + cJSON_AddNumberToObject(resp, "data", data); + } + if (!ok) { + cJSON_AddStringToObject(resp, "error", error.c_str()); + } + + char* raw = cJSON_PrintUnformatted(resp); + std::string out = raw == nullptr ? "{}" : std::string(raw); + if (raw != nullptr) { + cJSON_free(raw); + } + + cJSON_Delete(resp); + cJSON_Delete(root); + + return publishJSON(topicUp(), out); +#else + (void)payload; + return false; +#endif +} + +#ifdef ESP_PLATFORM +void DaliCloudBridge::mqttEventHandler(void* handler_args, + esp_event_base_t base, + int32_t event_id, + void* event_data) { + (void)base; + auto* self = static_cast(handler_args); + if (self == nullptr) { + return; + } + self->onMqttEvent(static_cast(event_data)); + (void)event_id; +} + +void DaliCloudBridge::onMqttEvent(esp_mqtt_event_handle_t event) { + switch (event->event_id) { + case MQTT_EVENT_CONNECTED: { + connected_.store(true); + ESP_LOGI(kTag, "MQTT connected"); + esp_mqtt_client_subscribe(client_, topicDown().c_str(), config_.qos); + publishStatus("online"); + publishRegister("{\"type\":\"register\",\"status\":\"online\"}"); + break; + } + case MQTT_EVENT_DISCONNECTED: + connected_.store(false); + ESP_LOGW(kTag, "MQTT disconnected"); + break; + case MQTT_EVENT_DATA: { + std::string topic(event->topic, event->topic_len); + std::string data(event->data, event->data_len); + if (topic == topicDown()) { + handleDownlink(data); + } + break; + } + default: + break; + } +} +#endif diff --git a/src/gateway_provisioning.cpp b/src/gateway_provisioning.cpp new file mode 100644 index 0000000..22c9acf --- /dev/null +++ b/src/gateway_provisioning.cpp @@ -0,0 +1,127 @@ +#include "gateway_provisioning.hpp" + +#ifdef ESP_PLATFORM +extern "C" { +#include "esp_log.h" +#include "nvs.h" +#include "nvs_flash.h" +} + +namespace { +constexpr const char* kTag = "gateway_provision"; +constexpr const char* kKeyBrokerURI = "broker_uri"; +constexpr const char* kKeyDeviceID = "device_id"; +constexpr const char* kKeyUsername = "username"; +constexpr const char* kKeyPassword = "password"; +constexpr const char* kKeyTopicPrefix = "topic_prefix"; +constexpr const char* kKeyQos = "qos"; + +esp_err_t writeString(nvs_handle_t handle, const char* key, const std::string& value) { + return nvs_set_str(handle, key, value.c_str()); +} + +esp_err_t readString(nvs_handle_t handle, const char* key, std::string* value) { + size_t required = 0; + esp_err_t err = nvs_get_str(handle, key, nullptr, &required); + if (err != ESP_OK) { + return err; + } + std::string buf(required, '\0'); + err = nvs_get_str(handle, key, buf.data(), &required); + if (err != ESP_OK) { + return err; + } + if (!buf.empty() && buf.back() == '\0') { + buf.pop_back(); + } + *value = buf; + return ESP_OK; +} +} // namespace + +esp_err_t GatewayProvisioningStore::save(const GatewayCloudConfig& config) const { + nvs_handle_t handle; + esp_err_t err = nvs_open(nvsNamespace_.c_str(), NVS_READWRITE, &handle); + if (err != ESP_OK) { + ESP_LOGE(kTag, "nvs_open(save) failed: %s", esp_err_to_name(err)); + return err; + } + + err = writeString(handle, kKeyBrokerURI, config.brokerURI); + if (err == ESP_OK) err = writeString(handle, kKeyDeviceID, config.deviceID); + if (err == ESP_OK) err = writeString(handle, kKeyUsername, config.username); + if (err == ESP_OK) err = writeString(handle, kKeyPassword, config.password); + if (err == ESP_OK) err = writeString(handle, kKeyTopicPrefix, config.topicPrefix); + if (err == ESP_OK) err = nvs_set_i32(handle, kKeyQos, config.qos); + if (err == ESP_OK) err = nvs_commit(handle); + + nvs_close(handle); + if (err != ESP_OK) { + ESP_LOGE(kTag, "save failed: %s", esp_err_to_name(err)); + } + return err; +} + +esp_err_t GatewayProvisioningStore::load(GatewayCloudConfig* config) const { + if (config == nullptr) { + return ESP_ERR_INVALID_ARG; + } + + nvs_handle_t handle; + esp_err_t err = nvs_open(nvsNamespace_.c_str(), NVS_READONLY, &handle); + if (err != ESP_OK) { + return err; + } + + err = readString(handle, kKeyBrokerURI, &config->brokerURI); + if (err == ESP_OK) err = readString(handle, kKeyDeviceID, &config->deviceID); + if (err == ESP_OK) err = readString(handle, kKeyUsername, &config->username); + if (err == ESP_OK) err = readString(handle, kKeyPassword, &config->password); + + esp_err_t topicErr = readString(handle, kKeyTopicPrefix, &config->topicPrefix); + if (topicErr != ESP_OK) { + config->topicPrefix = "devices"; + } + + int32_t qos = 1; + esp_err_t qosErr = nvs_get_i32(handle, kKeyQos, &qos); + if (qosErr == ESP_OK) { + config->qos = qos; + } else { + config->qos = 1; + } + + nvs_close(handle); + return err; +} + +esp_err_t GatewayProvisioningStore::clear() const { + nvs_handle_t handle; + esp_err_t err = nvs_open(nvsNamespace_.c_str(), NVS_READWRITE, &handle); + if (err != ESP_OK) { + return err; + } + + err = nvs_erase_all(handle); + if (err == ESP_OK) { + err = nvs_commit(handle); + } + nvs_close(handle); + return err; +} + +#else + +esp_err_t GatewayProvisioningStore::save(const GatewayCloudConfig& config) const { + (void)config; + return -1; +} + +esp_err_t GatewayProvisioningStore::load(GatewayCloudConfig* config) const { + (void)config; + return -1; +} + +esp_err_t GatewayProvisioningStore::clear() const { return -1; } + +#endif