Implement KNX Gateway functionality with support for DALI integration

- Added gateway_knx.cpp to handle KNX communication and DALI bridge requests.
- Implemented functions for encoding/decoding KNX telegrams and managing group writes.
- Introduced GatewayKnxBridge and GatewayKnxTpIpRouter classes for managing KNX to DALI routing and IP tunneling.
- Added configuration handling for KNX settings, including UART and multicast options.
- Implemented error handling and logging for various KNX operations.

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-08 18:19:37 +08:00
parent 029785ff1d
commit 1a8ee06ec1
10 changed files with 1589 additions and 5 deletions
+1
View File
@@ -5,6 +5,7 @@ set(GATEWAY_BRIDGE_REQUIRES
esp_driver_uart
freertos
gateway_cache
gateway_knx
gateway_modbus
log
lwip
@@ -9,6 +9,7 @@
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "gateway_knx.hpp"
#include "gateway_modbus.hpp"
namespace gateway {
@@ -22,6 +23,8 @@ struct GatewayBridgeServiceConfig {
bool modbus_startup_enabled{false};
bool bacnet_enabled{false};
bool bacnet_startup_enabled{false};
bool knx_enabled{false};
bool knx_startup_enabled{false};
bool cloud_enabled{true};
bool cloud_startup_enabled{false};
uint32_t modbus_task_stack_size{6144};
@@ -31,6 +34,9 @@ struct GatewayBridgeServiceConfig {
std::vector<int> reserved_uart_ports;
uint32_t bacnet_task_stack_size{8192};
UBaseType_t bacnet_task_priority{5};
uint32_t knx_task_stack_size{8192};
UBaseType_t knx_task_priority{5};
std::optional<GatewayKnxConfig> default_knx_config;
};
struct GatewayBridgeHttpResponse {
@@ -12,6 +12,7 @@
#include "dali_domain.hpp"
#include "gateway_cache.hpp"
#include "gateway_cloud.hpp"
#include "gateway_knx.hpp"
#include "gateway_modbus.hpp"
#include "gateway_provisioning.hpp"
@@ -58,6 +59,7 @@ constexpr const char* kModbusManagementPrefix = "@DALIGW";
struct GatewayBridgeStoredConfig {
BridgeRuntimeConfig bridge;
std::optional<GatewayModbusConfig> modbus;
std::optional<GatewayKnxConfig> knx;
std::optional<GatewayBacnetBridgeConfig> bacnet_server;
};
@@ -799,11 +801,15 @@ cJSON* ToCjson(const DaliValue& value) {
DaliValue::Object GatewayBridgeStoredConfigToValue(
const BridgeRuntimeConfig& bridge_config,
const std::optional<GatewayModbusConfig>& modbus_config,
const std::optional<GatewayKnxConfig>& knx_config,
const std::optional<GatewayBacnetBridgeConfig>& bacnet_server_config) {
DaliValue::Object out = bridge_config.toJson();
if (modbus_config.has_value()) {
out["modbus"] = GatewayModbusConfigToValue(modbus_config.value());
}
if (knx_config.has_value()) {
out["knx"] = GatewayKnxConfigToValue(knx_config.value());
}
if (bacnet_server_config.has_value()) {
DaliValue::Object bacnet;
bacnet["deviceInstance"] = static_cast<int64_t>(bacnet_server_config->deviceInstance);
@@ -817,9 +823,10 @@ DaliValue::Object GatewayBridgeStoredConfigToValue(
std::string GatewayBridgeStoredConfigToJson(
const BridgeRuntimeConfig& bridge_config,
const std::optional<GatewayModbusConfig>& modbus_config,
const std::optional<GatewayKnxConfig>& knx_config,
const std::optional<GatewayBacnetBridgeConfig>& bacnet_server_config) {
cJSON* root = ToCjson(DaliValue(GatewayBridgeStoredConfigToValue(
bridge_config, modbus_config, bacnet_server_config)));
bridge_config, modbus_config, knx_config, bacnet_server_config)));
const std::string body = PrintJson(root);
cJSON_Delete(root);
return body;
@@ -843,6 +850,7 @@ GatewayBridgeStoredConfig GatewayBridgeStoredConfigFromValue(const DaliValue::Ob
GatewayBridgeStoredConfig config;
config.bridge = BridgeRuntimeConfig::fromJson(object);
config.modbus = GatewayModbusConfigFromValue(getObjectValue(object, "modbus"));
config.knx = GatewayKnxConfigFromValue(getObjectValue(object, "knx"));
config.bacnet_server = GatewayBacnetBridgeConfigFromValue(
getObjectValue(object, "bacnetServer"));
return config;
@@ -1152,12 +1160,15 @@ struct GatewayBridgeService::ChannelRuntime {
std::unique_ptr<DaliComm> comm;
std::unique_ptr<DaliBridgeEngine> engine;
std::unique_ptr<GatewayModbusBridge> modbus;
std::unique_ptr<GatewayKnxBridge> knx;
std::unique_ptr<GatewayKnxTpIpRouter> knx_router;
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
std::unique_ptr<GatewayBacnetBridgeAdapter> bacnet;
#endif
std::unique_ptr<DaliCloudBridge> cloud;
BridgeRuntimeConfig bridge_config;
std::optional<GatewayModbusConfig> modbus_config;
std::optional<GatewayKnxConfig> knx_config;
std::optional<GatewayBacnetBridgeConfig> bacnet_server_config;
BridgeDiscoveryInventory discovery_inventory;
std::optional<GatewayCloudConfig> cloud_config;
@@ -1166,6 +1177,7 @@ struct GatewayBridgeService::ChannelRuntime {
bool cloud_config_loaded{false};
bool cloud_started{false};
bool modbus_started{false};
bool knx_started{false};
bool bacnet_started{false};
TaskHandle_t modbus_task_handle{nullptr};
std::atomic_bool modbus_stop_requested{false};
@@ -1210,6 +1222,7 @@ struct GatewayBridgeService::ChannelRuntime {
const auto stored_config = GatewayBridgeStoredConfigFromValue(bridge_object);
bridge_config = stored_config.bridge;
modbus_config = stored_config.modbus;
knx_config = stored_config.knx;
bacnet_server_config = stored_config.bacnet_server;
bridge_config_loaded = true;
}
@@ -1245,6 +1258,22 @@ struct GatewayBridgeService::ChannelRuntime {
modbus->setConfig(modbus_config.value());
}
knx = std::make_unique<GatewayKnxBridge>(*engine);
knx_router = std::make_unique<GatewayKnxTpIpRouter>(
*knx, [this](const uint8_t* data, size_t len) {
LockGuard guard(lock);
if (knx == nullptr) {
DaliBridgeResult result;
result.error = "KNX bridge is not ready";
return result;
}
return knx->handleCemiFrame(data, len);
});
if (knx_config.has_value()) {
knx->setConfig(knx_config.value());
knx_router->setConfig(knx_config.value());
}
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
if (service_config.bacnet_enabled) {
bacnet = std::make_unique<GatewayBacnetBridgeAdapter>(*engine);
@@ -1257,6 +1286,7 @@ struct GatewayBridgeService::ChannelRuntime {
#endif
applyCloudModelsLocked();
knx_started = false;
bacnet_started = false;
diagnostic_snapshot_cache.clear();
}
@@ -1433,14 +1463,15 @@ struct GatewayBridgeService::ChannelRuntime {
BridgeProvisioningStore store(bridgeNamespace());
const esp_err_t err = store.saveObject(
kBridgeConfigKey,
GatewayBridgeStoredConfigToValue(parsed->bridge, parsed->modbus,
parsed->bacnet_server));
GatewayBridgeStoredConfigToValue(parsed->bridge, parsed->modbus, parsed->knx,
parsed->bacnet_server));
if (err != ESP_OK) {
return err;
}
LockGuard guard(lock);
bridge_config = parsed->bridge;
modbus_config = parsed->modbus;
knx_config = parsed->knx;
bacnet_server_config = parsed->bacnet_server;
bridge_config_loaded = true;
applyBridgeConfigLocked();
@@ -1456,6 +1487,7 @@ struct GatewayBridgeService::ChannelRuntime {
LockGuard guard(lock);
bridge_config = BridgeRuntimeConfig{};
modbus_config.reset();
knx_config.reset();
bacnet_server_config.reset();
bridge_config_loaded = false;
applyBridgeConfigLocked();
@@ -1800,6 +1832,41 @@ struct GatewayBridgeService::ChannelRuntime {
}
#endif
esp_err_t startKnx() {
LockGuard guard(lock);
if (!service_config.knx_enabled) {
return ESP_ERR_NOT_SUPPORTED;
}
if (knx == nullptr || knx_router == nullptr) {
return ESP_ERR_INVALID_STATE;
}
const auto config = activeKnxConfigLocked();
if (!config.has_value()) {
return ESP_ERR_NOT_FOUND;
}
knx->setConfig(config.value());
knx_router->setConfig(config.value());
if (!config->ip_router_enabled) {
knx_started = false;
return ESP_ERR_NOT_SUPPORTED;
}
const esp_err_t err = knx_router->start(service_config.knx_task_stack_size,
service_config.knx_task_priority);
knx_started = err == ESP_OK;
return err;
}
esp_err_t stopKnx() {
LockGuard guard(lock);
if (knx_router != nullptr) {
const esp_err_t err = knx_router->stop();
knx_started = false;
return err;
}
knx_started = false;
return ESP_OK;
}
GatewayBridgeHttpResponse execute(std::string_view json) {
cJSON* root = cJSON_ParseWithLength(json.data(), json.size());
if (root == nullptr) {
@@ -1887,6 +1954,42 @@ struct GatewayBridgeService::ChannelRuntime {
cJSON_AddItemToObject(root, "bacnet", bacnet_json);
}
cJSON* knx_json = cJSON_CreateObject();
if (knx_json != nullptr) {
const auto effective_knx = knx_config.has_value() ? knx_config : service_config.default_knx_config;
cJSON_AddBoolToObject(knx_json, "enabled", service_config.knx_enabled);
cJSON_AddBoolToObject(knx_json, "startupEnabled", service_config.knx_startup_enabled);
cJSON_AddBoolToObject(knx_json, "started", knx_started);
cJSON_AddBoolToObject(knx_json, "routerReady", knx_router != nullptr && knx_router->started());
if (knx_router != nullptr) {
cJSON_AddStringToObject(knx_json, "lastError", knx_router->lastError().c_str());
}
if (effective_knx.has_value()) {
cJSON_AddBoolToObject(knx_json, "daliRouterEnabled",
effective_knx->dali_router_enabled);
cJSON_AddBoolToObject(knx_json, "ipRouterEnabled",
effective_knx->ip_router_enabled);
cJSON_AddBoolToObject(knx_json, "tunnelEnabled", effective_knx->tunnel_enabled);
cJSON_AddBoolToObject(knx_json, "multicastEnabled",
effective_knx->multicast_enabled);
cJSON_AddNumberToObject(knx_json, "mainGroup", effective_knx->main_group);
cJSON_AddNumberToObject(knx_json, "udpPort", effective_knx->udp_port);
cJSON_AddStringToObject(knx_json, "multicastAddress",
effective_knx->multicast_address.c_str());
cJSON_AddNumberToObject(knx_json, "individualAddress",
effective_knx->individual_address);
cJSON* serial_json = cJSON_CreateObject();
if (serial_json != nullptr) {
cJSON_AddNumberToObject(serial_json, "uartPort", effective_knx->tp_uart.uart_port);
cJSON_AddNumberToObject(serial_json, "txPin", effective_knx->tp_uart.tx_pin);
cJSON_AddNumberToObject(serial_json, "rxPin", effective_knx->tp_uart.rx_pin);
cJSON_AddNumberToObject(serial_json, "baudrate", effective_knx->tp_uart.baudrate);
cJSON_AddItemToObject(knx_json, "tpUart", serial_json);
}
}
cJSON_AddItemToObject(root, "knx", knx_json);
}
cJSON* cloud_json = cJSON_CreateObject();
if (cloud_json != nullptr) {
cJSON_AddBoolToObject(cloud_json, "enabled", service_config.cloud_enabled);
@@ -1906,6 +2009,7 @@ struct GatewayBridgeService::ChannelRuntime {
GatewayBridgeHttpResponse configJson() const {
return GatewayBridgeHttpResponse{ESP_OK,
GatewayBridgeStoredConfigToJson(bridge_config, modbus_config,
knx_config,
bacnet_server_config)};
}
@@ -2104,6 +2208,37 @@ struct GatewayBridgeService::ChannelRuntime {
return JsonOk(root);
}
GatewayBridgeHttpResponse knxBindingsJson() const {
cJSON* root = cJSON_CreateObject();
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
cJSON* bindings = cJSON_CreateArray();
if (bindings != nullptr && knx != nullptr) {
for (const auto& binding : knx->describeDaliBindings()) {
cJSON* item = cJSON_CreateObject();
if (item == nullptr) {
continue;
}
cJSON_AddStringToObject(item, "address", binding.address.c_str());
cJSON_AddNumberToObject(item, "rawAddress", binding.group_address);
cJSON_AddNumberToObject(item, "mainGroup", binding.main_group);
cJSON_AddNumberToObject(item, "middleGroup", binding.middle_group);
cJSON_AddNumberToObject(item, "subGroup", binding.sub_group);
cJSON_AddStringToObject(item, "name", binding.name.c_str());
cJSON_AddStringToObject(item, "datapointType", binding.datapoint_type.c_str());
cJSON_AddStringToObject(item, "dataType",
GatewayKnxDataTypeToString(binding.data_type));
cJSON_AddStringToObject(item, "targetKind",
GatewayKnxTargetKindToString(binding.target.kind));
if (binding.target.address >= 0) {
cJSON_AddNumberToObject(item, "targetAddress", binding.target.address);
}
cJSON_AddItemToArray(bindings, item);
}
}
cJSON_AddItemToObject(root, "bindings", bindings);
return JsonOk(root);
}
GatewayBridgeHttpResponse bacnetBindingsJson() {
cJSON* root = cJSON_CreateObject();
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
@@ -2597,7 +2732,8 @@ struct GatewayBridgeService::ChannelRuntime {
BridgeProvisioningStore store(bridgeNamespace());
const esp_err_t err = store.saveObject(
kBridgeConfigKey,
GatewayBridgeStoredConfigToValue(bridge_config, config, bacnet_server_config));
GatewayBridgeStoredConfigToValue(bridge_config, config, knx_config,
bacnet_server_config));
if (err != ESP_OK) {
return err;
}
@@ -2609,6 +2745,34 @@ struct GatewayBridgeService::ChannelRuntime {
return ESP_OK;
}
std::optional<GatewayKnxConfig> activeKnxConfigLocked() const {
if (knx_config.has_value()) {
return knx_config;
}
return service_config.default_knx_config;
}
esp_err_t saveKnxConfig(const GatewayKnxConfig& config) {
LockGuard guard(lock);
BridgeProvisioningStore store(bridgeNamespace());
const esp_err_t err = store.saveObject(
kBridgeConfigKey,
GatewayBridgeStoredConfigToValue(bridge_config, modbus_config, config,
bacnet_server_config));
if (err != ESP_OK) {
return err;
}
knx_config = config;
bridge_config_loaded = true;
if (knx != nullptr) {
knx->setConfig(config);
}
if (knx_router != nullptr) {
knx_router->setConfig(config);
}
return ESP_OK;
}
std::vector<uint8_t> processModbusPdu(const GatewayModbusConfig& config,
uint8_t unit_id,
const std::vector<uint8_t>& pdu) {
@@ -3215,6 +3379,16 @@ esp_err_t GatewayBridgeService::start() {
}
}
if (config_.knx_enabled && config_.knx_startup_enabled) {
for (const auto& runtime : runtimes_) {
const esp_err_t err = runtime->startKnx();
if (err != ESP_OK && err != ESP_ERR_NOT_FOUND && err != ESP_ERR_NOT_SUPPORTED) {
ESP_LOGW(kTag, "gateway=%u KNX/IP startup skipped: %s", runtime->channel.gateway_id,
esp_err_to_name(err));
}
}
}
if (config_.bacnet_enabled && config_.bacnet_startup_enabled) {
for (const auto& runtime : runtimes_) {
const esp_err_t err = runtime->startBacnet();
@@ -3294,6 +3468,9 @@ GatewayBridgeHttpResponse GatewayBridgeService::handleGet(
if (action == "modbus") {
return runtime->modbusBindingsJson();
}
if (action == "knx") {
return runtime->knxBindingsJson();
}
if (action == "bacnet") {
return runtime->bacnetBindingsJson();
}
@@ -3502,6 +3679,41 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost(
}
return handleGet("modbus", gateway_id.value());
}
if (action == "knx_config" || action == "save_knx") {
cJSON* knx_root = cJSON_ParseWithLength(body.data(), body.size());
if (knx_root == nullptr) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "invalid KNX config JSON");
}
const cJSON* knx_node = cJSON_GetObjectItemCaseSensitive(knx_root, "knx");
if (knx_node == nullptr) {
knx_node = knx_root;
}
const DaliValue knx_value = FromCjson(knx_node);
cJSON_Delete(knx_root);
const auto parsed = GatewayKnxConfigFromValue(&knx_value);
if (!parsed.has_value()) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "invalid KNX config");
}
const esp_err_t err = runtime->saveKnxConfig(parsed.value());
if (err != ESP_OK) {
return ErrorResponse(err, "failed to save KNX bridge config");
}
return handleGet("knx", gateway_id.value());
}
if (action == "knx_start") {
const esp_err_t err = runtime->startKnx();
if (err != ESP_OK) {
return ErrorResponse(err, "failed to start KNX/IP bridge");
}
return handleGet("knx", gateway_id.value());
}
if (action == "knx_stop") {
const esp_err_t err = runtime->stopKnx();
if (err != ESP_OK) {
return ErrorResponse(err, "failed to stop KNX/IP bridge");
}
return handleGet("knx", gateway_id.value());
}
if (action == "bacnet_start") {
const esp_err_t err = runtime->startBacnet();
if (err != ESP_OK) {