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:
@@ -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