feat(gateway): add cloud integration for KNX and DALI with configurable transport options

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-27 15:20:29 +08:00
parent 5622e6ba81
commit 7a820e700c
7 changed files with 242 additions and 10 deletions
@@ -1226,6 +1226,28 @@ std::optional<GatewayBridgeStoredConfig> GatewayBridgeStoredConfigFromJson(std::
GatewayCloudConfig GatewayCloudConfigFromJson(cJSON* root) {
GatewayCloudConfig config;
#if defined(CONFIG_GATEWAY_CLOUD_TOPIC_PREFIX)
config.topicPrefix = CONFIG_GATEWAY_CLOUD_TOPIC_PREFIX;
#endif
#if defined(CONFIG_GATEWAY_CLOUD_CEMI_TRANSPORT_LTE_UART)
config.cemiTransport = "lte_uart";
config.lteUartEnabled = true;
#elif defined(CONFIG_GATEWAY_CLOUD_CEMI_TRANSPORT_MQTT_AND_LTE_UART)
config.cemiTransport = "mqtt_lte_uart";
config.lteUartEnabled = true;
#endif
#if defined(CONFIG_GATEWAY_CLOUD_LTE_UART_PORT)
config.lteUartPort = CONFIG_GATEWAY_CLOUD_LTE_UART_PORT;
#endif
#if defined(CONFIG_GATEWAY_CLOUD_LTE_UART_TX_PIN)
config.lteTxPin = CONFIG_GATEWAY_CLOUD_LTE_UART_TX_PIN;
#endif
#if defined(CONFIG_GATEWAY_CLOUD_LTE_UART_RX_PIN)
config.lteRxPin = CONFIG_GATEWAY_CLOUD_LTE_UART_RX_PIN;
#endif
#if defined(CONFIG_GATEWAY_CLOUD_LTE_UART_BAUDRATE)
config.lteBaudrate = CONFIG_GATEWAY_CLOUD_LTE_UART_BAUDRATE;
#endif
if (const char* value = JsonString(root, "brokerURI")) {
config.brokerURI = value;
}
@@ -1241,6 +1263,28 @@ GatewayCloudConfig GatewayCloudConfigFromJson(cJSON* root) {
if (const char* value = JsonString(root, "topicPrefix")) {
config.topicPrefix = value;
}
if (const char* value = JsonString(root, "cemiTransport")) {
config.cemiTransport = value;
}
config.lteUartEnabled = JsonBool(root, "lteUartEnabled", config.lteUartEnabled);
if (const auto value = JsonInt(root, "lteUartPort")) {
config.lteUartPort = value.value();
}
if (const auto value = JsonInt(root, "lteTxPin")) {
config.lteTxPin = value.value();
}
if (const auto value = JsonInt(root, "lteRxPin")) {
config.lteRxPin = value.value();
}
if (const auto value = JsonInt(root, "lteBaudrate")) {
config.lteBaudrate = value.value();
}
if (const auto value = JsonInt(root, "lteRxBufferSize")) {
config.lteRxBufferSize = value.value();
}
if (const auto value = JsonInt(root, "lteTxBufferSize")) {
config.lteTxBufferSize = value.value();
}
if (const auto qos = JsonInt(root, "qos")) {
config.qos = qos.value();
}
@@ -1257,6 +1301,14 @@ cJSON* GatewayCloudConfigToCjson(const GatewayCloudConfig& config) {
cJSON_AddStringToObject(root, "username", config.username.c_str());
cJSON_AddStringToObject(root, "password", config.password.c_str());
cJSON_AddStringToObject(root, "topicPrefix", config.topicPrefix.c_str());
cJSON_AddStringToObject(root, "cemiTransport", config.cemiTransport.c_str());
cJSON_AddBoolToObject(root, "lteUartEnabled", config.lteUartEnabled);
cJSON_AddNumberToObject(root, "lteUartPort", config.lteUartPort);
cJSON_AddNumberToObject(root, "lteTxPin", config.lteTxPin);
cJSON_AddNumberToObject(root, "lteRxPin", config.lteRxPin);
cJSON_AddNumberToObject(root, "lteBaudrate", config.lteBaudrate);
cJSON_AddNumberToObject(root, "lteRxBufferSize", config.lteRxBufferSize);
cJSON_AddNumberToObject(root, "lteTxBufferSize", config.lteTxBufferSize);
cJSON_AddNumberToObject(root, "qos", config.qos);
return root;
}
@@ -1832,6 +1884,22 @@ struct GatewayBridgeService::ChannelRuntime {
for (const auto& model : bridge_config.models) {
cloud->bridge().upsertModel(model);
}
wireCloudCemiProxyLocked();
}
void wireCloudCemiProxyLocked() {
if (knx_router != nullptr) {
knx_router->setCloudCemiPublisher([this](const uint8_t* data, size_t len) {
if (cloud_started && cloud != nullptr) {
cloud->publishCemiFrame(data, len);
}
});
}
if (cloud != nullptr) {
cloud->setCemiDownlinkHandler([this](const uint8_t* data, size_t len) {
return knx_router != nullptr && knx_router->injectCloudCemiFrame(data, len);
});
}
}
esp_err_t saveBridgeConfig(std::string_view json) {
@@ -2454,8 +2522,12 @@ struct GatewayBridgeService::ChannelRuntime {
cJSON* cloud_remote_json = cJSON_CreateObject();
if (cloud_remote_json != nullptr) {
const auto& cloud_remote = effective_knx->oam_router.cloud_remote;
const auto cloud_stats = knx_router == nullptr
? GatewayKnxTpIpRouter::CloudCemiStats{}
: knx_router->cloudCemiStats();
cJSON_AddBoolToObject(cloud_remote_json, "prepared", true);
cJSON_AddBoolToObject(cloud_remote_json, "enabled", cloud_remote.enabled);
cJSON_AddBoolToObject(cloud_remote_json, "running", cloud_stats.enabled);
cJSON_AddStringToObject(cloud_remote_json, "mode", cloud_remote.mode.c_str());
cJSON_AddBoolToObject(cloud_remote_json, "requireSecureTunnel",
cloud_remote.require_secure_tunnel);
@@ -2467,6 +2539,10 @@ struct GatewayBridgeService::ChannelRuntime {
!cloud_remote.mqtt_topic_prefix.empty());
cJSON_AddBoolToObject(cloud_remote_json, "authTokenRefConfigured",
!cloud_remote.auth_token_ref.empty());
cJSON_AddNumberToObject(cloud_remote_json, "uplinkFrames",
static_cast<double>(cloud_stats.uplink_frames));
cJSON_AddNumberToObject(cloud_remote_json, "downlinkFrames",
static_cast<double>(cloud_stats.downlink_frames));
cJSON_AddItemToObject(knx_json, "cloudKnxRemoteAccess", cloud_remote_json);
}
cJSON* serial_json = cJSON_CreateObject();
@@ -2564,9 +2640,16 @@ struct GatewayBridgeService::ChannelRuntime {
cJSON_AddBoolToObject(cloud_json, "configured", cloud_config_loaded);
cJSON_AddBoolToObject(cloud_json, "started", cloud_started);
cJSON_AddBoolToObject(cloud_json, "connected", cloud != nullptr && cloud->isConnected());
cJSON_AddBoolToObject(cloud_json, "lteUartActive",
cloud != nullptr && cloud->lteUartActive());
if (cloud_config.has_value()) {
cJSON_AddStringToObject(cloud_json, "deviceID", cloud_config->deviceID.c_str());
cJSON_AddStringToObject(cloud_json, "topicPrefix", cloud_config->topicPrefix.c_str());
cJSON_AddStringToObject(cloud_json, "cemiTransport",
cloud_config->cemiTransport.c_str());
cJSON_AddBoolToObject(cloud_json, "lteUartEnabled",
cloud_config->lteUartEnabled);
cJSON_AddNumberToObject(cloud_json, "lteUartPort", cloud_config->lteUartPort);
}
cJSON_AddItemToObject(root, "cloud", cloud_json);
}
@@ -2968,6 +3051,7 @@ struct GatewayBridgeService::ChannelRuntime {
cJSON_AddBoolToObject(root, "configured", cloud_config_loaded);
cJSON_AddBoolToObject(root, "started", cloud_started);
cJSON_AddBoolToObject(root, "connected", cloud != nullptr && cloud->isConnected());
cJSON_AddBoolToObject(root, "lteUartActive", cloud != nullptr && cloud->lteUartActive());
if (cloud_config.has_value()) {
cJSON_AddItemToObject(root, "config", GatewayCloudConfigToCjson(cloud_config.value()));
}
@@ -268,6 +268,13 @@ class GatewayKnxTpIpRouter {
const uint8_t* data,
size_t len)>;
using RoutingSequenceStoreHandler = std::function<void(uint64_t sequence)>;
using CloudCemiPublisher = std::function<void(const uint8_t* data, size_t len)>;
struct CloudCemiStats {
bool enabled{false};
uint64_t uplink_frames{0};
uint64_t downlink_frames{0};
};
GatewayKnxTpIpRouter(GatewayKnxBridge& bridge,
std::string openknx_namespace = "openknx");
@@ -279,7 +286,10 @@ class GatewayKnxTpIpRouter {
void setGroupObjectWriteHandler(GroupObjectWriteHandler handler);
void setOamIpSecureCredentials(const GatewayKnxIpSecureCredentialMaterial& credentials);
void setOamIpSecureRoutingSequenceStoreHandler(RoutingSequenceStoreHandler handler);
void setCloudCemiPublisher(CloudCemiPublisher publisher);
const GatewayKnxConfig& config() const;
bool injectCloudCemiFrame(const uint8_t* data, size_t len);
CloudCemiStats cloudCemiStats() const;
bool tpUartOnline() const;
bool programmingMode();
esp_err_t setProgrammingMode(bool enabled);
@@ -458,6 +468,7 @@ class GatewayKnxTpIpRouter {
size_t suppress_routing_echo_len = 0);
bool handleOpenKnxBusFrame(const uint8_t* data, size_t len);
bool transmitOpenKnxTpFrame(const uint8_t* data, size_t len);
void publishCloudCemiFrame(const uint8_t* data, size_t len);
void selectOpenKnxNetworkInterface(const ::sockaddr_in& remote);
bool routeOpenKnxGroupWrite(const uint8_t* data, size_t len, const char* context);
bool emitOpenKnxGroupValue(uint16_t group_object_number, const uint8_t* data, size_t len);
@@ -476,6 +487,7 @@ class GatewayKnxTpIpRouter {
GroupWriteHandler group_write_handler_;
GroupObjectWriteHandler group_object_write_handler_;
RoutingSequenceStoreHandler routing_sequence_store_handler_;
CloudCemiPublisher cloud_cemi_publisher_;
std::string openknx_namespace_;
GatewayKnxConfig config_;
std::unique_ptr<openknx::EtsDeviceRuntime> ets_device_;
@@ -505,6 +517,8 @@ class GatewayKnxTpIpRouter {
bool tp_uart_online_{false};
bool commissioning_only_{false};
std::atomic_bool openknx_configured_{false};
std::atomic<uint64_t> cloud_cemi_uplink_frames_{0};
std::atomic<uint64_t> cloud_cemi_downlink_frames_{0};
bool programming_button_last_pressed_{false};
bool programming_led_state_{false};
TickType_t programming_button_last_toggle_tick_{0};
@@ -46,8 +46,27 @@ void GatewayKnxTpIpRouter::setOamIpSecureRoutingSequenceStoreHandler(
routing_sequence_store_handler_ = std::move(handler);
}
void GatewayKnxTpIpRouter::setCloudCemiPublisher(CloudCemiPublisher publisher) {
cloud_cemi_publisher_ = std::move(publisher);
}
const GatewayKnxConfig& GatewayKnxTpIpRouter::config() const { return config_; }
bool GatewayKnxTpIpRouter::injectCloudCemiFrame(const uint8_t* data, size_t len) {
if (data == nullptr || len == 0 || !config_.oam_router.cloud_remote.enabled) {
return false;
}
cloud_cemi_downlink_frames_.fetch_add(1, std::memory_order_relaxed);
return handleOpenKnxTunnelFrame(data, len, nullptr, kServiceTunnellingRequest);
}
GatewayKnxTpIpRouter::CloudCemiStats GatewayKnxTpIpRouter::cloudCemiStats() const {
return CloudCemiStats{
config_.oam_router.cloud_remote.enabled,
cloud_cemi_uplink_frames_.load(std::memory_order_relaxed),
cloud_cemi_downlink_frames_.load(std::memory_order_relaxed)};
}
bool GatewayKnxTpIpRouter::tpUartOnline() const { return tp_uart_online_; }
bool GatewayKnxTpIpRouter::programmingMode() {
@@ -328,6 +347,7 @@ esp_err_t GatewayKnxTpIpRouter::initializeRuntime() {
return result;
});
ets_device_->setBusFrameSender([this](const uint8_t* data, size_t len) {
publishCloudCemiFrame(data, len);
sendTunnelIndication(data, len);
sendRoutingIndication(data, len);
});
@@ -51,6 +51,7 @@ bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t
if (response == nullptr || response_len == 0) {
return;
}
publishCloudCemiFrame(response, response_len);
const bool routing_context =
response_client == nullptr && response_service == kServiceRoutingIndication;
const auto message_code = CemiMessageCode(response, response_len);
@@ -128,6 +129,7 @@ bool GatewayKnxTpIpRouter::handleOamRouterTunnelFrame(const uint8_t* data, size_
if (response == nullptr || response_len == 0) {
return;
}
publishCloudCemiFrame(response, response_len);
const bool routing_context =
response_client == nullptr && response_service == kServiceRoutingIndication;
const auto message_code = CemiMessageCode(response, response_len);
@@ -191,15 +193,30 @@ bool GatewayKnxTpIpRouter::transmitOpenKnxTpFrame(const uint8_t* data, size_t le
}
bool GatewayKnxTpIpRouter::handleOpenKnxBusFrame(const uint8_t* data, size_t len) {
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ == nullptr) {
return false;
bool consumed = false;
{
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ == nullptr) {
return false;
}
consumed = ets_device_->handleBusFrame(data, len);
syncOpenKnxConfigFromDevice();
}
if (consumed) {
publishCloudCemiFrame(data, len);
}
const bool consumed = ets_device_->handleBusFrame(data, len);
syncOpenKnxConfigFromDevice();
return consumed;
}
void GatewayKnxTpIpRouter::publishCloudCemiFrame(const uint8_t* data, size_t len) {
if (data == nullptr || len == 0 || !config_.oam_router.cloud_remote.enabled ||
!cloud_cemi_publisher_) {
return;
}
cloud_cemi_uplink_frames_.fetch_add(1, std::memory_order_relaxed);
cloud_cemi_publisher_(data, len);
}
bool GatewayKnxTpIpRouter::routeOpenKnxGroupWrite(const uint8_t* data, size_t len,
const char* context) {
const auto decoded = DecodeOpenKnxGroupWrite(data, len);