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:
@@ -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)
|
||||
|
||||
@@ -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`.
|
||||
- `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/<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);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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_;
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user