Enhance DaliCloudBridge: add support for LTE UART transport and extend configuration options

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-27 15:20:16 +08:00
parent 2d100ec7ed
commit f18f7570ed
4 changed files with 420 additions and 13 deletions
+342 -12
View File
@@ -1,16 +1,26 @@
#include "gateway_cloud.hpp"
#include <algorithm>
#include <array>
#include <cctype>
#include <cstring>
#include <sstream>
#include <vector>
#ifdef ESP_PLATFORM
extern "C" {
#include "cJSON.h"
#include "driver/uart.h"
#include "esp_log.h"
}
namespace {
constexpr const char* kTag = "dali_cloud_bridge";
constexpr std::array<uint8_t, 4> kCemiEnvelopeMagic{'D', 'K', 'C', '1'};
constexpr uint8_t kCemiDirectionDown = 1;
constexpr uint8_t kCemiDirectionUp = 2;
constexpr size_t kCemiEnvelopeHeaderSize = 14;
constexpr size_t kMaxCemiEnvelopeSize = kCemiEnvelopeHeaderSize + 255 + 4096;
std::string jsonEscape(const std::string& value) {
std::string out;
@@ -143,20 +153,139 @@ bool isKnownBridgeRequestKey(const char* key) {
return false;
}
std::string lowerCopy(const std::string& value) {
std::string out(value);
std::transform(out.begin(), out.end(), out.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
return out;
}
bool cemiUsesMqtt(const GatewayCloudConfig& config) {
const std::string mode = lowerCopy(config.cemiTransport);
return mode.empty() || mode == "mqtt" || mode == "hybrid" ||
mode == "mqtt_lte_uart" || mode == "both";
}
bool cemiUsesLteUart(const GatewayCloudConfig& config) {
if (config.lteUartEnabled) {
return true;
}
const std::string mode = lowerCopy(config.cemiTransport);
return mode == "lte_uart" || mode == "serial_lte" || mode == "uart" ||
mode == "hybrid" || mode == "mqtt_lte_uart" || mode == "both";
}
void writeBe16(uint8_t* data, uint16_t value) {
data[0] = static_cast<uint8_t>((value >> 8) & 0xff);
data[1] = static_cast<uint8_t>(value & 0xff);
}
void writeBe32(uint8_t* data, uint32_t value) {
data[0] = static_cast<uint8_t>((value >> 24) & 0xff);
data[1] = static_cast<uint8_t>((value >> 16) & 0xff);
data[2] = static_cast<uint8_t>((value >> 8) & 0xff);
data[3] = static_cast<uint8_t>(value & 0xff);
}
uint16_t readBe16(const uint8_t* data) {
return static_cast<uint16_t>((static_cast<uint16_t>(data[0]) << 8) | data[1]);
}
void consumeCemiSerialBuffer(std::vector<uint8_t>* buffer,
const std::function<bool(const uint8_t*, size_t)>& handler) {
if (buffer == nullptr) {
return;
}
while (buffer->size() >= kCemiEnvelopeHeaderSize) {
auto magic = std::search(buffer->begin(), buffer->end(), kCemiEnvelopeMagic.begin(),
kCemiEnvelopeMagic.end());
if (magic == buffer->end()) {
buffer->clear();
return;
}
if (magic != buffer->begin()) {
buffer->erase(buffer->begin(), magic);
}
if (buffer->size() < kCemiEnvelopeHeaderSize) {
return;
}
const size_t session_len = (*buffer)[10];
const size_t cemi_len = readBe16(buffer->data() + 11);
const size_t frame_len = kCemiEnvelopeHeaderSize + session_len + cemi_len;
if (frame_len > kMaxCemiEnvelopeSize || cemi_len == 0) {
buffer->erase(buffer->begin());
continue;
}
if (buffer->size() < frame_len) {
return;
}
handler(buffer->data(), frame_len);
buffer->erase(buffer->begin(), buffer->begin() + static_cast<std::ptrdiff_t>(frame_len));
}
if (buffer->size() > kMaxCemiEnvelopeSize) {
buffer->erase(buffer->begin(), buffer->end() - static_cast<std::ptrdiff_t>(kCemiEnvelopeHeaderSize));
}
}
std::vector<uint8_t> buildCemiEnvelope(uint8_t direction, uint32_t sequence,
const uint8_t* data, size_t len) {
if (data == nullptr || len == 0 || len > 4096) {
return {};
}
std::vector<uint8_t> out(kCemiEnvelopeHeaderSize + len);
std::copy(kCemiEnvelopeMagic.begin(), kCemiEnvelopeMagic.end(), out.begin());
out[4] = direction;
out[5] = 0;
writeBe32(&out[6], sequence);
out[10] = 0;
writeBe16(&out[11], static_cast<uint16_t>(len));
out[13] = 0;
std::memcpy(out.data() + kCemiEnvelopeHeaderSize, data, len);
return out;
}
std::vector<uint8_t> decodeCemiEnvelope(const uint8_t* payload, size_t len) {
if (payload == nullptr || len < kCemiEnvelopeHeaderSize ||
std::memcmp(payload, kCemiEnvelopeMagic.data(), kCemiEnvelopeMagic.size()) != 0 ||
payload[4] != kCemiDirectionDown) {
return {};
}
const size_t session_len = payload[10];
const size_t cemi_len = readBe16(&payload[11]);
if (len != kCemiEnvelopeHeaderSize + session_len + cemi_len || cemi_len == 0) {
return {};
}
const uint8_t* cemi = payload + kCemiEnvelopeHeaderSize + session_len;
return std::vector<uint8_t>(cemi, cemi + cemi_len);
}
} // namespace
#endif
DaliCloudBridge::DaliCloudBridge(DaliComm& comm) : comm_(comm), bridge_(comm) {}
bool DaliCloudBridge::start(const GatewayCloudConfig& config) {
#ifdef ESP_PLATFORM
stop();
#endif
config_ = config;
if (config_.brokerURI.empty() || config_.deviceID.empty()) {
const bool mqtt_requested = cemiUsesMqtt(config_) && !config_.brokerURI.empty() &&
!config_.deviceID.empty();
const bool lte_uart_requested = cemiUsesLteUart(config_);
if (!mqtt_requested && !lte_uart_requested) {
return false;
}
#ifdef ESP_PLATFORM
if (client_ != nullptr) {
stop();
bool started = false;
if (lte_uart_requested) {
started = startLteUart() || started;
}
if (!mqtt_requested) {
connected_.store(started);
return started;
}
esp_mqtt_client_config_t mqttCfg = {};
@@ -168,7 +297,7 @@ bool DaliCloudBridge::start(const GatewayCloudConfig& config) {
client_ = esp_mqtt_client_init(&mqttCfg);
if (client_ == nullptr) {
ESP_LOGE(kTag, "esp_mqtt_client_init failed");
return false;
return started;
}
esp_mqtt_client_register_event(client_, MQTT_EVENT_ANY, &DaliCloudBridge::mqttEventHandler,
@@ -177,17 +306,20 @@ bool DaliCloudBridge::start(const GatewayCloudConfig& config) {
ESP_LOGE(kTag, "esp_mqtt_client_start failed");
esp_mqtt_client_destroy(client_);
client_ = nullptr;
return false;
return started;
}
return true;
#else
(void)config;
(void)mqtt_requested;
(void)lte_uart_requested;
return false;
#endif
}
void DaliCloudBridge::stop() {
#ifdef ESP_PLATFORM
stopLteUart();
connected_.store(false);
if (client_ != nullptr) {
esp_mqtt_client_stop(client_);
@@ -197,7 +329,15 @@ void DaliCloudBridge::stop() {
#endif
}
bool DaliCloudBridge::isConnected() const { return connected_.load(); }
bool DaliCloudBridge::isConnected() const {
return connected_.load() || lte_uart_started_.load();
}
bool DaliCloudBridge::lteUartActive() const { return lte_uart_started_.load(); }
void DaliCloudBridge::setCemiDownlinkHandler(CemiDownlinkHandler handler) {
cemi_downlink_handler_ = std::move(handler);
}
bool DaliCloudBridge::publishStatus(const std::string& status) {
std::string payload = "{\"type\":\"status\",\"status\":\"" + status + "\"}";
@@ -208,6 +348,148 @@ bool DaliCloudBridge::publishRegister(const std::string& payloadJson) {
return publishJSON(topicRegister(), payloadJson);
}
bool DaliCloudBridge::publishCemiFrame(const uint8_t* data, size_t len) {
#ifdef ESP_PLATFORM
const uint32_t sequence = cemi_sequence_.fetch_add(1, std::memory_order_relaxed);
const auto envelope = buildCemiEnvelope(kCemiDirectionUp, sequence, data, len);
if (envelope.empty()) {
return false;
}
bool delivered = false;
if (cemiUsesLteUart(config_)) {
delivered = publishLteUartBytes(envelope.data(), envelope.size()) || delivered;
}
if (cemiUsesMqtt(config_) && client_ != nullptr) {
delivered = publishBytes(topicCemiUp(), envelope.data(), envelope.size(), 0) || delivered;
}
return delivered;
#else
(void)data;
(void)len;
return false;
#endif
}
bool DaliCloudBridge::startLteUart() {
#ifdef ESP_PLATFORM
const int port = config_.lteUartPort;
if (port < 0 || port > 2 || config_.lteBaudrate <= 0) {
ESP_LOGW(kTag, "LTE UART cEMI transport disabled by invalid port=%d baud=%d",
port, config_.lteBaudrate);
return false;
}
uart_config_t uart_config = {};
uart_config.baud_rate = config_.lteBaudrate;
uart_config.data_bits = UART_DATA_8_BITS;
uart_config.parity = UART_PARITY_DISABLE;
uart_config.stop_bits = UART_STOP_BITS_1;
uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
uart_config.source_clk = UART_SCLK_DEFAULT;
const uart_port_t uart_port = static_cast<uart_port_t>(port);
esp_err_t err = uart_param_config(uart_port, &uart_config);
if (err != ESP_OK) {
ESP_LOGE(kTag, "uart_param_config failed: %s", esp_err_to_name(err));
return false;
}
err = uart_set_pin(uart_port,
config_.lteTxPin < 0 ? UART_PIN_NO_CHANGE : config_.lteTxPin,
config_.lteRxPin < 0 ? UART_PIN_NO_CHANGE : config_.lteRxPin,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
if (err != ESP_OK) {
ESP_LOGE(kTag, "uart_set_pin failed: %s", esp_err_to_name(err));
return false;
}
const int rx_buffer = std::max(config_.lteRxBufferSize, 512);
const int tx_buffer = std::max(config_.lteTxBufferSize, 512);
err = uart_driver_install(uart_port, rx_buffer, tx_buffer, 0, nullptr, 0);
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
ESP_LOGE(kTag, "uart_driver_install failed: %s", esp_err_to_name(err));
return false;
}
lte_uart_stop_requested_.store(false);
lte_uart_started_.store(true);
if (xTaskCreate(&DaliCloudBridge::lteUartTaskEntry, "cloud_lte_cemi", 4096,
this, 5, &lte_uart_task_) != pdPASS) {
lte_uart_started_.store(false);
uart_driver_delete(uart_port);
lte_uart_task_ = nullptr;
ESP_LOGE(kTag, "failed to create LTE UART cEMI task");
return false;
}
ESP_LOGI(kTag, "LTE UART cEMI transport started on UART%d", port);
return true;
#else
return false;
#endif
}
void DaliCloudBridge::stopLteUart() {
#ifdef ESP_PLATFORM
if (!lte_uart_started_.load()) {
return;
}
lte_uart_stop_requested_.store(true);
for (int i = 0; i < 20 && lte_uart_task_ != nullptr; ++i) {
vTaskDelay(pdMS_TO_TICKS(10));
}
const int port = config_.lteUartPort;
if (port >= 0 && port <= 2) {
uart_driver_delete(static_cast<uart_port_t>(port));
}
lte_uart_started_.store(false);
#endif
}
bool DaliCloudBridge::publishLteUartBytes(const uint8_t* data, size_t len) {
#ifdef ESP_PLATFORM
if (!lte_uart_started_.load() || data == nullptr || len == 0 ||
config_.lteUartPort < 0 || config_.lteUartPort > 2) {
return false;
}
const int written = uart_write_bytes(static_cast<uart_port_t>(config_.lteUartPort),
reinterpret_cast<const char*>(data), len);
return written == static_cast<int>(len);
#else
(void)data;
(void)len;
return false;
#endif
}
#ifdef ESP_PLATFORM
void DaliCloudBridge::lteUartTaskEntry(void* arg) {
auto* self = static_cast<DaliCloudBridge*>(arg);
if (self != nullptr) {
self->lteUartLoop();
}
vTaskDelete(nullptr);
}
void DaliCloudBridge::lteUartLoop() {
std::vector<uint8_t> buffer;
buffer.reserve(kMaxCemiEnvelopeSize);
std::array<uint8_t, 128> chunk{};
const uart_port_t port = static_cast<uart_port_t>(config_.lteUartPort);
while (!lte_uart_stop_requested_.load()) {
const int read = uart_read_bytes(port, chunk.data(), chunk.size(), pdMS_TO_TICKS(100));
if (read <= 0) {
continue;
}
buffer.insert(buffer.end(), chunk.begin(), chunk.begin() + read);
consumeCemiSerialBuffer(&buffer, [this](const uint8_t* payload, size_t len) {
return handleCemiDownlink(payload, len);
});
}
lte_uart_task_ = nullptr;
}
#else
void DaliCloudBridge::lteUartLoop() {}
#endif
#ifdef ESP_PLATFORM
std::string buildRegisterPayload(const GatewayCloudConfig& config) {
const std::string deviceID = jsonEscape(config.deviceID);
@@ -226,12 +508,8 @@ std::string buildRegisterPayload(const GatewayCloudConfig& config) {
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;
return publishBytes(topic, reinterpret_cast<const uint8_t*>(payloadJson.data()),
payloadJson.size(), config_.qos);
#else
(void)topic;
(void)payloadJson;
@@ -239,6 +517,25 @@ bool DaliCloudBridge::publishJSON(const std::string& topic, const std::string& p
#endif
}
bool DaliCloudBridge::publishBytes(const std::string& topic, const uint8_t* data,
size_t len, int qos) {
#ifdef ESP_PLATFORM
if (client_ == nullptr || !connected_.load() || data == nullptr) {
return false;
}
int msgID = esp_mqtt_client_publish(client_, topic.c_str(),
reinterpret_cast<const char*>(data),
static_cast<int>(len), qos, 0);
return msgID >= 0;
#else
(void)topic;
(void)data;
(void)len;
(void)qos;
return false;
#endif
}
std::string DaliCloudBridge::topicDown() const {
return config_.topicPrefix + "/" + config_.deviceID + "/down";
}
@@ -255,6 +552,18 @@ std::string DaliCloudBridge::topicRegister() const {
return config_.topicPrefix + "/" + config_.deviceID + "/register";
}
std::string DaliCloudBridge::topicCemiDown() const {
return config_.topicPrefix + "/" + config_.deviceID + "/cemi/down";
}
std::string DaliCloudBridge::topicCemiUp() const {
return config_.topicPrefix + "/" + config_.deviceID + "/cemi/up";
}
std::string DaliCloudBridge::topicCemiStatus() const {
return config_.topicPrefix + "/" + config_.deviceID + "/cemi/status";
}
bool DaliCloudBridge::handleDownlink(const std::string& payload) {
#ifdef ESP_PLATFORM
cJSON* root = cJSON_Parse(payload.c_str());
@@ -341,6 +650,24 @@ bool DaliCloudBridge::handleDownlink(const std::string& payload) {
#endif
}
bool DaliCloudBridge::handleCemiDownlink(const uint8_t* payload, size_t len) {
#ifdef ESP_PLATFORM
if (!cemi_downlink_handler_) {
return false;
}
const auto cemi = decodeCemiEnvelope(payload, len);
if (cemi.empty()) {
ESP_LOGW(kTag, "invalid cEMI cloud payload len=%u", static_cast<unsigned>(len));
return false;
}
return cemi_downlink_handler_(cemi.data(), cemi.size());
#else
(void)payload;
(void)len;
return false;
#endif
}
#ifdef ESP_PLATFORM
void DaliCloudBridge::mqttEventHandler(void* handler_args,
esp_event_base_t base,
@@ -361,6 +688,7 @@ void DaliCloudBridge::onMqttEvent(esp_mqtt_event_handle_t event) {
connected_.store(true);
ESP_LOGI(kTag, "MQTT connected");
esp_mqtt_client_subscribe(client_, topicDown().c_str(), config_.qos);
esp_mqtt_client_subscribe(client_, topicCemiDown().c_str(), 0);
publishStatus("online");
publishRegister(buildRegisterPayload(config_));
break;
@@ -374,6 +702,8 @@ void DaliCloudBridge::onMqttEvent(esp_mqtt_event_handle_t event) {
std::string data(event->data, event->data_len);
if (topic == topicDown()) {
handleDownlink(data);
} else if (topic == topicCemiDown()) {
handleCemiDownlink(reinterpret_cast<const uint8_t*>(data.data()), data.size());
}
break;
}
+45
View File
@@ -14,6 +14,14 @@ 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* kKeyCemiTransport = "cemi_transport";
constexpr const char* kKeyLteUartEnabled = "lte_uart_en";
constexpr const char* kKeyLteUartPort = "lte_uart_port";
constexpr const char* kKeyLteTxPin = "lte_tx_pin";
constexpr const char* kKeyLteRxPin = "lte_rx_pin";
constexpr const char* kKeyLteBaudrate = "lte_baudrate";
constexpr const char* kKeyLteRxBuffer = "lte_rx_buf";
constexpr const char* kKeyLteTxBuffer = "lte_tx_buf";
constexpr const char* kKeyQos = "qos";
esp_err_t writeString(nvs_handle_t handle, const char* key, const std::string& value) {
@@ -52,6 +60,15 @@ esp_err_t GatewayProvisioningStore::save(const GatewayCloudConfig& config) const
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 = writeString(handle, kKeyCemiTransport, config.cemiTransport);
if (err == ESP_OK) err = nvs_set_i32(handle, kKeyLteUartEnabled,
config.lteUartEnabled ? 1 : 0);
if (err == ESP_OK) err = nvs_set_i32(handle, kKeyLteUartPort, config.lteUartPort);
if (err == ESP_OK) err = nvs_set_i32(handle, kKeyLteTxPin, config.lteTxPin);
if (err == ESP_OK) err = nvs_set_i32(handle, kKeyLteRxPin, config.lteRxPin);
if (err == ESP_OK) err = nvs_set_i32(handle, kKeyLteBaudrate, config.lteBaudrate);
if (err == ESP_OK) err = nvs_set_i32(handle, kKeyLteRxBuffer, config.lteRxBufferSize);
if (err == ESP_OK) err = nvs_set_i32(handle, kKeyLteTxBuffer, config.lteTxBufferSize);
if (err == ESP_OK) err = nvs_set_i32(handle, kKeyQos, config.qos);
if (err == ESP_OK) err = nvs_commit(handle);
@@ -83,6 +100,34 @@ esp_err_t GatewayProvisioningStore::load(GatewayCloudConfig* config) const {
config->topicPrefix = "devices";
}
esp_err_t transportErr = readString(handle, kKeyCemiTransport, &config->cemiTransport);
if (transportErr != ESP_OK) {
config->cemiTransport = "mqtt";
}
int32_t intValue = 0;
if (nvs_get_i32(handle, kKeyLteUartEnabled, &intValue) == ESP_OK) {
config->lteUartEnabled = intValue != 0;
}
if (nvs_get_i32(handle, kKeyLteUartPort, &intValue) == ESP_OK) {
config->lteUartPort = intValue;
}
if (nvs_get_i32(handle, kKeyLteTxPin, &intValue) == ESP_OK) {
config->lteTxPin = intValue;
}
if (nvs_get_i32(handle, kKeyLteRxPin, &intValue) == ESP_OK) {
config->lteRxPin = intValue;
}
if (nvs_get_i32(handle, kKeyLteBaudrate, &intValue) == ESP_OK) {
config->lteBaudrate = intValue;
}
if (nvs_get_i32(handle, kKeyLteRxBuffer, &intValue) == ESP_OK) {
config->lteRxBufferSize = intValue;
}
if (nvs_get_i32(handle, kKeyLteTxBuffer, &intValue) == ESP_OK) {
config->lteTxBufferSize = intValue;
}
int32_t qos = 1;
esp_err_t qosErr = nvs_get_i32(handle, kKeyQos, &qos);
if (qosErr == ESP_OK) {