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.
This commit is contained in:
Tony
2026-03-31 08:44:30 +08:00
parent 7e8ac7f566
commit a8a82f9627
8 changed files with 531 additions and 0 deletions
+3
View File
@@ -10,7 +10,10 @@ idf_component_register(
"src/sequence.cpp" "src/sequence.cpp"
"src/sequence_store.cpp" "src/sequence_store.cpp"
"src/color.cpp" "src/color.cpp"
"src/gateway_cloud.cpp"
"src/gateway_provisioning.cpp"
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
REQUIRES mqtt cjson nvs_flash
) )
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17) set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
+71
View File
@@ -47,3 +47,74 @@ std::vector<int> rgb = dali.dt8.getColourRGB(5);
- Optional query support: provide a `transact` callback that returns the gateway reply; otherwise, query methods return `std::nullopt`. - 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. - `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. - 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/<deviceID>/down`
- Uplink: `devices/<deviceID>/up`
- Status: `devices/<deviceID>/status`
- Register: `devices/<deviceID>/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);
}
```
+2
View File
@@ -1,5 +1,7 @@
dependencies: dependencies:
idf: '>=5.1' idf: '>=5.1'
espressif/esp-mqtt: '*'
espressif/cjson: '*'
description: ESP DALI CPP library component for ESP-IDF description: ESP DALI CPP library component for ESP-IDF
repository: git://github.com/tony-cloud/esp-dali-cpp.git repository: git://github.com/tony-cloud/esp-dali-cpp.git
repository_info: repository_info:
+2
View File
@@ -9,6 +9,8 @@
#include "dt1.hpp" #include "dt1.hpp"
#include "dt8.hpp" #include "dt8.hpp"
#include "addr.hpp" #include "addr.hpp"
#include "gateway_cloud.hpp"
#include "gateway_provisioning.hpp"
#include "color.hpp" #include "color.hpp"
#include "errors.hpp" #include "errors.hpp"
#include "log.hpp" #include "log.hpp"
+58
View File
@@ -0,0 +1,58 @@
#pragma once
#include "dali_comm.hpp"
#include <atomic>
#include <string>
#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<bool> connected_{false};
#ifdef ESP_PLATFORM
esp_mqtt_client_handle_t client_ = nullptr;
#endif
};
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include "gateway_cloud.hpp"
#include <string>
#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_;
};
+241
View File
@@ -0,0 +1,241 @@
#include "gateway_cloud.hpp"
#include <sstream>
#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<int>(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<uint8_t>(addr), static_cast<uint8_t>(cmd));
} else if (op == "send_ext") {
ok = comm_.sendExtRaw(static_cast<uint8_t>(addr), static_cast<uint8_t>(cmd));
} else if (op == "query") {
auto response = comm_.queryRaw(static_cast<uint8_t>(addr), static_cast<uint8_t>(cmd));
if (response.has_value()) {
ok = true;
hasData = true;
data = static_cast<int>(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<DaliCloudBridge*>(handler_args);
if (self == nullptr) {
return;
}
self->onMqttEvent(static_cast<esp_mqtt_event_handle_t>(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
+127
View File
@@ -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