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
+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