diff --git a/apps/gateway/main/CMakeLists.txt b/apps/gateway/main/CMakeLists.txt index be19177..9d05596 100644 --- a/apps/gateway/main/CMakeLists.txt +++ b/apps/gateway/main/CMakeLists.txt @@ -1,6 +1,6 @@ idf_component_register( SRCS "app_main.cpp" - REQUIRES gateway_core gateway_controller gateway_network gateway_bridge gateway_cache dali_domain gateway_runtime gateway_ble gateway_usb_setup gateway_485_control log + REQUIRES gateway_core gateway_controller gateway_network gateway_bridge gateway_cache dali_domain gateway_runtime gateway_ble gateway_usb_setup gateway_485_control gateway_knx log ) set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17) \ No newline at end of file diff --git a/apps/gateway/main/Kconfig.projbuild b/apps/gateway/main/Kconfig.projbuild index 9757916..d1b58c7 100644 --- a/apps/gateway/main/Kconfig.projbuild +++ b/apps/gateway/main/Kconfig.projbuild @@ -501,6 +501,98 @@ config GATEWAY_START_BACNET_BRIDGE_ENABLED help Starts configured BACnet/IP object bindings at boot. Disabled by default so the UDP BACnet/IP port is opened only after provisioning or explicit runtime start. +config GATEWAY_KNX_BRIDGE_SUPPORTED + bool "KNX to DALI bridge is supported" + depends on GATEWAY_BRIDGE_SUPPORTED && GATEWAY_WIFI_SUPPORTED + default n + help + Enables the gateway-owned KNX group-address router and KNXnet/IP TP/IP + router. Group addresses use the configured main group, middle groups as + DALI data types, and subgroups matching DALI short address structure. + +config GATEWAY_START_KNX_BRIDGE_ENABLED + bool "Start KNX/IP bridge at startup" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED + default n + help + Starts the KNXnet/IP tunneling/multicast listener at boot. Disabled by + default so UDP port 3671 is opened only after provisioning or explicit start. + +config GATEWAY_KNX_MAIN_GROUP + int "KNX DALI main group" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED + range 0 31 + default 0 + help + Main group used by the built-in KNX to DALI router. Middle groups select + the data type and subgroups select broadcast, short-address, or group targets. + +config GATEWAY_KNX_TUNNEL_ENABLED + bool "Enable KNXnet/IP tunneling mode" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED + default y + +config GATEWAY_KNX_MULTICAST_ENABLED + bool "Enable KNXnet/IP multicast routing mode" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED + default y + +config GATEWAY_KNX_UDP_PORT + int "KNXnet/IP UDP port" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED + range 1 65535 + default 3671 + +config GATEWAY_KNX_MULTICAST_ADDRESS + string "KNXnet/IP multicast address" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED && GATEWAY_KNX_MULTICAST_ENABLED + default "224.0.23.12" + +config GATEWAY_KNX_INDIVIDUAL_ADDRESS + int "KNX individual address raw value" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED + range 0 65535 + default 4353 + help + Raw 16-bit individual address advertised to KNXnet/IP tunnel clients. + The default 4353 is 1.1.1. + +config GATEWAY_KNX_TP_UART_PORT + int "KNX TP UART port" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED + range 0 2 + default 1 + +config GATEWAY_KNX_TP_TX_PIN + int "KNX TP UART TX pin" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED + range -1 48 + default -1 + +config GATEWAY_KNX_TP_RX_PIN + int "KNX TP UART RX pin" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED + range -1 48 + default -1 + +config GATEWAY_KNX_TP_BAUDRATE + int "KNX TP UART baudrate" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED + range 1200 921600 + default 19200 + +config GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE + int "KNX/IP bridge task stack bytes" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED + range 6144 24576 + default 8192 + +config GATEWAY_BRIDGE_KNX_TASK_PRIORITY + int "KNX/IP bridge task priority" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED + range 1 10 + default 5 + config GATEWAY_CLOUD_BRIDGE_SUPPORTED bool "MQTT cloud bridge is supported" depends on GATEWAY_BRIDGE_SUPPORTED && GATEWAY_WIFI_SUPPORTED diff --git a/apps/gateway/main/app_main.cpp b/apps/gateway/main/app_main.cpp index d0ed13e..6e50de0 100644 --- a/apps/gateway/main/app_main.cpp +++ b/apps/gateway/main/app_main.cpp @@ -144,6 +144,46 @@ #define CONFIG_GATEWAY_BRIDGE_BACNET_TASK_PRIORITY 5 #endif +#ifndef CONFIG_GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE +#define CONFIG_GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE 8192 +#endif + +#ifndef CONFIG_GATEWAY_BRIDGE_KNX_TASK_PRIORITY +#define CONFIG_GATEWAY_BRIDGE_KNX_TASK_PRIORITY 5 +#endif + +#ifndef CONFIG_GATEWAY_KNX_MAIN_GROUP +#define CONFIG_GATEWAY_KNX_MAIN_GROUP 0 +#endif + +#ifndef CONFIG_GATEWAY_KNX_UDP_PORT +#define CONFIG_GATEWAY_KNX_UDP_PORT 3671 +#endif + +#ifndef CONFIG_GATEWAY_KNX_MULTICAST_ADDRESS +#define CONFIG_GATEWAY_KNX_MULTICAST_ADDRESS "224.0.23.12" +#endif + +#ifndef CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS +#define CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS 4353 +#endif + +#ifndef CONFIG_GATEWAY_KNX_TP_UART_PORT +#define CONFIG_GATEWAY_KNX_TP_UART_PORT 1 +#endif + +#ifndef CONFIG_GATEWAY_KNX_TP_TX_PIN +#define CONFIG_GATEWAY_KNX_TP_TX_PIN -1 +#endif + +#ifndef CONFIG_GATEWAY_KNX_TP_RX_PIN +#define CONFIG_GATEWAY_KNX_TP_RX_PIN -1 +#endif + +#ifndef CONFIG_GATEWAY_KNX_TP_BAUDRATE +#define CONFIG_GATEWAY_KNX_TP_BAUDRATE 19200 +#endif + #ifndef CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS #define CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS 5000 #endif @@ -237,6 +277,30 @@ constexpr bool kBacnetBridgeStartupEnabled = true; constexpr bool kBacnetBridgeStartupEnabled = false; #endif +#ifdef CONFIG_GATEWAY_KNX_BRIDGE_SUPPORTED +constexpr bool kKnxBridgeSupported = true; +#else +constexpr bool kKnxBridgeSupported = false; +#endif + +#ifdef CONFIG_GATEWAY_START_KNX_BRIDGE_ENABLED +constexpr bool kKnxBridgeStartupEnabled = true; +#else +constexpr bool kKnxBridgeStartupEnabled = false; +#endif + +#ifdef CONFIG_GATEWAY_KNX_TUNNEL_ENABLED +constexpr bool kKnxTunnelEnabled = true; +#else +constexpr bool kKnxTunnelEnabled = false; +#endif + +#ifdef CONFIG_GATEWAY_KNX_MULTICAST_ENABLED +constexpr bool kKnxMulticastEnabled = true; +#else +constexpr bool kKnxMulticastEnabled = false; +#endif + #ifdef CONFIG_GATEWAY_CLOUD_BRIDGE_SUPPORTED constexpr bool kCloudBridgeSupported = true; #else @@ -449,6 +513,26 @@ bool ValidateChannelBindings() { } } + if (kKnxBridgeSupported) { + const int knx_uart = CONFIG_GATEWAY_KNX_TP_UART_PORT; + if (k485ControlEnabled && knx_uart == 0) { + ESP_LOGE(kTag, "KNX TP UART0 conflicts with the UART0 control bridge"); + return false; + } + if (kModbusBridgeSupported && kModbusDefaultSerialTransport && + knx_uart == CONFIG_GATEWAY_MODBUS_SERIAL_UART_PORT) { + ESP_LOGE(kTag, "KNX TP UART%d conflicts with default Modbus serial UART", knx_uart); + return false; + } + for (int i = 0; i < CONFIG_GATEWAY_CHANNEL_COUNT; ++i) { + if (channels[i].enabled && channels[i].serial_phy && channels[i].uart_port == knx_uart) { + ESP_LOGE(kTag, "KNX TP UART%d conflicts with DALI channel %d serial PHY", knx_uart, + i + 1); + return false; + } + } + } + if (!any_enabled) { ESP_LOGE(kTag, "no DALI PHY is configured; enable at least one native or serial channel"); return false; @@ -626,6 +710,9 @@ extern "C" void app_main(void) { bridge_config.bacnet_enabled = profile.enable_wifi && kBacnetBridgeSupported; bridge_config.bacnet_startup_enabled = profile.enable_wifi && kBacnetBridgeSupported && kBacnetBridgeStartupEnabled; + bridge_config.knx_enabled = profile.enable_wifi && kKnxBridgeSupported; + bridge_config.knx_startup_enabled = profile.enable_wifi && kKnxBridgeSupported && + kKnxBridgeStartupEnabled; bridge_config.cloud_enabled = profile.enable_wifi && kCloudBridgeSupported; bridge_config.cloud_startup_enabled = profile.enable_wifi && kCloudBridgeSupported && kCloudBridgeStartupEnabled; @@ -649,6 +736,9 @@ extern "C" void app_main(void) { bridge_config.reserved_uart_ports.push_back(2); #endif #endif + if (kKnxBridgeSupported) { + bridge_config.reserved_uart_ports.push_back(CONFIG_GATEWAY_KNX_TP_UART_PORT); + } if (kModbusBridgeSupported) { gateway::GatewayModbusConfig default_modbus; #if defined(CONFIG_GATEWAY_MODBUS_DEFAULT_TRANSPORT_RTU) @@ -675,6 +765,27 @@ extern "C" void app_main(void) { static_cast(CONFIG_GATEWAY_BRIDGE_BACNET_TASK_STACK_SIZE); bridge_config.bacnet_task_priority = static_cast(CONFIG_GATEWAY_BRIDGE_BACNET_TASK_PRIORITY); + if (kKnxBridgeSupported) { + gateway::GatewayKnxConfig default_knx; + default_knx.dali_router_enabled = true; + default_knx.ip_router_enabled = true; + default_knx.tunnel_enabled = kKnxTunnelEnabled; + default_knx.multicast_enabled = kKnxMulticastEnabled; + default_knx.main_group = static_cast(CONFIG_GATEWAY_KNX_MAIN_GROUP); + default_knx.udp_port = static_cast(CONFIG_GATEWAY_KNX_UDP_PORT); + default_knx.multicast_address = CONFIG_GATEWAY_KNX_MULTICAST_ADDRESS; + default_knx.individual_address = + static_cast(CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS); + default_knx.tp_uart.uart_port = CONFIG_GATEWAY_KNX_TP_UART_PORT; + default_knx.tp_uart.tx_pin = CONFIG_GATEWAY_KNX_TP_TX_PIN; + default_knx.tp_uart.rx_pin = CONFIG_GATEWAY_KNX_TP_RX_PIN; + default_knx.tp_uart.baudrate = static_cast(CONFIG_GATEWAY_KNX_TP_BAUDRATE); + bridge_config.default_knx_config = default_knx; + } + bridge_config.knx_task_stack_size = + static_cast(CONFIG_GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE); + bridge_config.knx_task_priority = + static_cast(CONFIG_GATEWAY_BRIDGE_KNX_TASK_PRIORITY); s_bridge = std::make_unique(*s_dali_domain, *s_cache, bridge_config); } diff --git a/apps/gateway/sdkconfig b/apps/gateway/sdkconfig index c804336..f91984c 100644 --- a/apps/gateway/sdkconfig +++ b/apps/gateway/sdkconfig @@ -666,6 +666,7 @@ CONFIG_GATEWAY_MODBUS_TCP_PORT=1502 CONFIG_GATEWAY_MODBUS_UNIT_ID=1 CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED=y # CONFIG_GATEWAY_START_BACNET_BRIDGE_ENABLED is not set +# CONFIG_GATEWAY_KNX_BRIDGE_SUPPORTED is not set CONFIG_GATEWAY_CLOUD_BRIDGE_SUPPORTED=y # CONFIG_GATEWAY_START_CLOUD_BRIDGE_ENABLED is not set CONFIG_GATEWAY_BRIDGE_MODBUS_TASK_STACK_SIZE=6144 diff --git a/components/gateway_bridge/CMakeLists.txt b/components/gateway_bridge/CMakeLists.txt index 3e765ac..bd6f366 100644 --- a/components/gateway_bridge/CMakeLists.txt +++ b/components/gateway_bridge/CMakeLists.txt @@ -5,6 +5,7 @@ set(GATEWAY_BRIDGE_REQUIRES esp_driver_uart freertos gateway_cache + gateway_knx gateway_modbus log lwip diff --git a/components/gateway_bridge/include/gateway_bridge.hpp b/components/gateway_bridge/include/gateway_bridge.hpp index 8e599c7..17327e9 100644 --- a/components/gateway_bridge/include/gateway_bridge.hpp +++ b/components/gateway_bridge/include/gateway_bridge.hpp @@ -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 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 default_knx_config; }; struct GatewayBridgeHttpResponse { diff --git a/components/gateway_bridge/src/gateway_bridge.cpp b/components/gateway_bridge/src/gateway_bridge.cpp index d93898a..7dffc9a 100644 --- a/components/gateway_bridge/src/gateway_bridge.cpp +++ b/components/gateway_bridge/src/gateway_bridge.cpp @@ -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 modbus; + std::optional knx; std::optional bacnet_server; }; @@ -799,11 +801,15 @@ cJSON* ToCjson(const DaliValue& value) { DaliValue::Object GatewayBridgeStoredConfigToValue( const BridgeRuntimeConfig& bridge_config, const std::optional& modbus_config, + const std::optional& knx_config, const std::optional& 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(bacnet_server_config->deviceInstance); @@ -817,9 +823,10 @@ DaliValue::Object GatewayBridgeStoredConfigToValue( std::string GatewayBridgeStoredConfigToJson( const BridgeRuntimeConfig& bridge_config, const std::optional& modbus_config, + const std::optional& knx_config, const std::optional& 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 comm; std::unique_ptr engine; std::unique_ptr modbus; + std::unique_ptr knx; + std::unique_ptr knx_router; #if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED) std::unique_ptr bacnet; #endif std::unique_ptr cloud; BridgeRuntimeConfig bridge_config; std::optional modbus_config; + std::optional knx_config; std::optional bacnet_server_config; BridgeDiscoveryInventory discovery_inventory; std::optional 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(*engine); + knx_router = std::make_unique( + *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(*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 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 processModbusPdu(const GatewayModbusConfig& config, uint8_t unit_id, const std::vector& 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) { diff --git a/components/gateway_knx/CMakeLists.txt b/components/gateway_knx/CMakeLists.txt new file mode 100644 index 0000000..4ec1972 --- /dev/null +++ b/components/gateway_knx/CMakeLists.txt @@ -0,0 +1,7 @@ +idf_component_register( + SRCS "src/gateway_knx.cpp" + INCLUDE_DIRS "include" + REQUIRES dali_cpp esp_driver_uart freertos log lwip +) + +set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17) \ No newline at end of file diff --git a/components/gateway_knx/include/gateway_knx.hpp b/components/gateway_knx/include/gateway_knx.hpp new file mode 100644 index 0000000..91f72d2 --- /dev/null +++ b/components/gateway_knx/include/gateway_knx.hpp @@ -0,0 +1,174 @@ +#pragma once + +#include "bridge.hpp" +#include "model_value.hpp" + +#include "esp_err.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "lwip/sockets.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace gateway { + +constexpr uint16_t kGatewayKnxDefaultUdpPort = 3671; +constexpr const char* kGatewayKnxDefaultMulticastAddress = "224.0.23.12"; +constexpr uint32_t kGatewayKnxDefaultTpBaudrate = 19200; + +struct GatewayKnxTpUartConfig { + int uart_port{1}; + int tx_pin{-1}; + int rx_pin{-1}; + uint32_t baudrate{kGatewayKnxDefaultTpBaudrate}; + size_t rx_buffer_size{1024}; + size_t tx_buffer_size{1024}; + uint32_t read_timeout_ms{20}; +}; + +struct GatewayKnxConfig { + bool dali_router_enabled{true}; + bool ip_router_enabled{false}; + bool tunnel_enabled{true}; + bool multicast_enabled{true}; + uint8_t main_group{0}; + uint16_t udp_port{kGatewayKnxDefaultUdpPort}; + std::string multicast_address{kGatewayKnxDefaultMulticastAddress}; + uint16_t individual_address{0x1101}; + GatewayKnxTpUartConfig tp_uart; +}; + +enum class GatewayKnxDaliDataType : uint8_t { + kUnknown = 0, + kSwitch = 1, + kBrightness = 2, + kColorTemperature = 3, + kRgb = 4, +}; + +enum class GatewayKnxDaliTargetKind : uint8_t { + kNone = 0, + kBroadcast = 1, + kShortAddress = 2, + kGroup = 3, +}; + +struct GatewayKnxDaliTarget { + GatewayKnxDaliTargetKind kind{GatewayKnxDaliTargetKind::kNone}; + int address{-1}; +}; + +struct GatewayKnxDaliBinding { + uint16_t group_address{0}; + uint8_t main_group{0}; + uint8_t middle_group{0}; + uint8_t sub_group{0}; + std::string address; + std::string name; + std::string datapoint_type; + GatewayKnxDaliDataType data_type{GatewayKnxDaliDataType::kUnknown}; + GatewayKnxDaliTarget target; +}; + +std::optional GatewayKnxConfigFromValue(const DaliValue* value); +DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config); + +const char* GatewayKnxDataTypeToString(GatewayKnxDaliDataType data_type); +const char* GatewayKnxTargetKindToString(GatewayKnxDaliTargetKind kind); +std::optional GatewayKnxDaliDataTypeForMiddleGroup( + uint8_t middle_group); +std::optional GatewayKnxDaliTargetForSubgroup(uint8_t sub_group); +uint16_t GatewayKnxGroupAddress(uint8_t main_group, uint8_t middle_group, + uint8_t sub_group); +std::string GatewayKnxGroupAddressString(uint16_t group_address); + +class GatewayKnxBridge { + public: + explicit GatewayKnxBridge(DaliBridgeEngine& engine); + + void setConfig(const GatewayKnxConfig& config); + const GatewayKnxConfig& config() const; + + std::vector describeDaliBindings() const; + DaliBridgeResult handleCemiFrame(const uint8_t* data, size_t len); + DaliBridgeResult handleGroupWrite(uint16_t group_address, const uint8_t* data, + size_t len); + + private: + DaliBridgeResult executeForDecodedWrite(uint16_t group_address, + GatewayKnxDaliDataType data_type, + GatewayKnxDaliTarget target, + const uint8_t* data, size_t len); + + DaliBridgeEngine& engine_; + GatewayKnxConfig config_; +}; + +class GatewayKnxTpIpRouter { + public: + using CemiFrameHandler = std::function; + + GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHandler handler); + ~GatewayKnxTpIpRouter(); + + void setConfig(const GatewayKnxConfig& config); + const GatewayKnxConfig& config() const; + + esp_err_t start(uint32_t task_stack_size, UBaseType_t task_priority); + esp_err_t stop(); + bool started() const; + const std::string& lastError() const; + + private: + static void TaskEntry(void* arg); + + void taskLoop(); + void finishTask(); + void closeSockets(); + bool configureSocket(); + bool configureTpUart(); + void handleUdpDatagram(const uint8_t* data, size_t len, const ::sockaddr_in& remote); + void handleRoutingIndication(const uint8_t* body, size_t len); + void handleTunnellingRequest(const uint8_t* body, size_t len, const ::sockaddr_in& remote); + void handleConnectRequest(const uint8_t* body, size_t len, const ::sockaddr_in& remote); + void handleConnectionStateRequest(const uint8_t* body, size_t len, + const ::sockaddr_in& remote); + void handleDisconnectRequest(const uint8_t* body, size_t len, const ::sockaddr_in& remote); + void sendTunnellingAck(uint8_t channel_id, uint8_t sequence, uint8_t status, + const ::sockaddr_in& remote); + void sendTunnelIndication(const uint8_t* data, size_t len); + void sendConnectionStateResponse(uint8_t channel_id, uint8_t status, + const ::sockaddr_in& remote); + void sendDisconnectResponse(uint8_t channel_id, uint8_t status, + const ::sockaddr_in& remote); + void sendConnectResponse(uint8_t channel_id, uint8_t status, + const ::sockaddr_in& remote); + void sendRoutingIndication(const uint8_t* data, size_t len); + void pollTpUart(); + void handleTpTelegram(const uint8_t* data, size_t len); + void forwardCemiToTp(const uint8_t* data, size_t len); + + GatewayKnxBridge& bridge_; + CemiFrameHandler handler_; + GatewayKnxConfig config_; + TaskHandle_t task_handle_{nullptr}; + std::atomic_bool stop_requested_{false}; + std::atomic_bool started_{false}; + int udp_sock_{-1}; + int tp_uart_port_{-1}; + uint8_t tunnel_channel_id_{1}; + uint8_t expected_tunnel_sequence_{0}; + uint8_t tunnel_send_sequence_{0}; + bool tunnel_connected_{false}; + ::sockaddr_in tunnel_remote_{}; + std::vector tp_rx_frame_; + std::string last_error_; +}; + +} // namespace gateway \ No newline at end of file diff --git a/components/gateway_knx/src/gateway_knx.cpp b/components/gateway_knx/src/gateway_knx.cpp new file mode 100644 index 0000000..11b65c5 --- /dev/null +++ b/components/gateway_knx/src/gateway_knx.cpp @@ -0,0 +1,980 @@ +#include "gateway_knx.hpp" + +#include "driver/uart.h" +#include "esp_log.h" +#include "lwip/inet.h" +#include "lwip/sockets.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace gateway { +namespace { + +constexpr const char* kTag = "gateway_knx"; +constexpr uint8_t kCemiLDataReq = 0x11; +constexpr uint8_t kCemiLDataInd = 0x29; +constexpr uint8_t kCemiLDataCon = 0x2e; +constexpr uint16_t kServiceConnectRequest = 0x0205; +constexpr uint16_t kServiceConnectResponse = 0x0206; +constexpr uint16_t kServiceConnectionStateRequest = 0x0207; +constexpr uint16_t kServiceConnectionStateResponse = 0x0208; +constexpr uint16_t kServiceDisconnectRequest = 0x0209; +constexpr uint16_t kServiceDisconnectResponse = 0x020a; +constexpr uint16_t kServiceTunnellingRequest = 0x0420; +constexpr uint16_t kServiceTunnellingAck = 0x0421; +constexpr uint16_t kServiceRoutingIndication = 0x0530; +constexpr uint8_t kKnxNetIpHeaderSize = 0x06; +constexpr uint8_t kKnxNetIpVersion10 = 0x10; +constexpr uint8_t kKnxNoError = 0x00; +constexpr uint8_t kKnxErrorConnectionId = 0x21; +constexpr uint8_t kKnxErrorConnectionType = 0x22; +constexpr uint8_t kKnxErrorNoMoreConnections = 0x24; +constexpr uint8_t kKnxErrorSequenceNumber = 0x04; +constexpr uint8_t kKnxConnectionTypeTunnel = 0x04; +constexpr uint8_t kKnxTunnelLayerLink = 0x02; + +struct DecodedGroupWrite { + uint16_t group_address{0}; + std::vector data; +}; + +uint16_t ReadBe16(const uint8_t* data) { + return static_cast((static_cast(data[0]) << 8) | data[1]); +} + +void WriteBe16(uint8_t* data, uint16_t value) { + data[0] = static_cast((value >> 8) & 0xff); + data[1] = static_cast(value & 0xff); +} + +std::optional ObjectIntAny(const DaliValue::Object& object, + std::initializer_list keys) { + for (const char* key : keys) { + if (const auto value = getObjectInt(object, key)) { + return value; + } + } + return std::nullopt; +} + +std::optional ObjectBoolAny(const DaliValue::Object& object, + std::initializer_list keys) { + for (const char* key : keys) { + if (const auto value = getObjectBool(object, key)) { + return value; + } + } + return std::nullopt; +} + +std::optional ObjectStringAny(const DaliValue::Object& object, + std::initializer_list keys) { + for (const char* key : keys) { + if (const auto value = getObjectString(object, key)) { + return value; + } + } + return std::nullopt; +} + +std::string TargetName(const GatewayKnxDaliTarget& target) { + switch (target.kind) { + case GatewayKnxDaliTargetKind::kBroadcast: + return "Broadcast"; + case GatewayKnxDaliTargetKind::kShortAddress: + return "A" + std::to_string(target.address); + case GatewayKnxDaliTargetKind::kGroup: + return "Group " + std::to_string(target.address); + case GatewayKnxDaliTargetKind::kNone: + default: + return "Unmapped"; + } +} + +std::string DataTypeName(GatewayKnxDaliDataType data_type) { + switch (data_type) { + case GatewayKnxDaliDataType::kSwitch: + return "Switch"; + case GatewayKnxDaliDataType::kBrightness: + return "Dimmer"; + case GatewayKnxDaliDataType::kColorTemperature: + return "Color Temperature"; + case GatewayKnxDaliDataType::kRgb: + return "RGB"; + case GatewayKnxDaliDataType::kUnknown: + default: + return "Unknown"; + } +} + +const char* DataTypeDpt(GatewayKnxDaliDataType data_type) { + switch (data_type) { + case GatewayKnxDaliDataType::kSwitch: + return "DPST-1-1"; + case GatewayKnxDaliDataType::kBrightness: + return "DPST-5-1"; + case GatewayKnxDaliDataType::kColorTemperature: + return "DPST-7-600"; + case GatewayKnxDaliDataType::kRgb: + return "DPST-232-600"; + case GatewayKnxDaliDataType::kUnknown: + default: + return ""; + } +} + +std::optional DecodeCemiGroupWrite(const uint8_t* data, size_t len) { + if (data == nullptr || len < 10) { + return std::nullopt; + } + const uint8_t message_code = data[0]; + if (message_code != kCemiLDataReq && message_code != kCemiLDataInd && + message_code != kCemiLDataCon) { + return std::nullopt; + } + const size_t base = 2U + data[1]; + if (len < base + 8U) { + return std::nullopt; + } + const uint8_t control2 = data[base + 1]; + if ((control2 & 0x80) == 0) { + return std::nullopt; + } + const uint16_t destination = ReadBe16(data + base + 4); + const size_t tpdu_len = static_cast(data[base + 6]) + 1U; + if (tpdu_len < 2U || len < base + 7U + tpdu_len) { + return std::nullopt; + } + const uint8_t* tpdu = data + base + 7; + const uint16_t apci = static_cast(((tpdu[0] & 0x03) << 8) | (tpdu[1] & 0xc0)); + if (apci != 0x80) { + return std::nullopt; + } + + DecodedGroupWrite out; + out.group_address = destination; + if (tpdu_len == 2U) { + out.data.push_back(tpdu[1] & 0x3f); + } else { + out.data.assign(tpdu + 2, tpdu + tpdu_len); + } + return out; +} + +DaliBridgeRequest RequestForTarget(uint16_t group_address, + const GatewayKnxDaliTarget& target, + BridgeOperation operation) { + DaliBridgeRequest request; + request.sequence = "knx-" + GatewayKnxGroupAddressString(group_address); + request.operation = operation; + switch (target.kind) { + case GatewayKnxDaliTargetKind::kBroadcast: + request.metadata["broadcast"] = true; + break; + case GatewayKnxDaliTargetKind::kShortAddress: + request.shortAddress = target.address; + break; + case GatewayKnxDaliTargetKind::kGroup: + request.metadata["group"] = target.address; + break; + case GatewayKnxDaliTargetKind::kNone: + default: + break; + } + request.metadata["sourceProtocol"] = "knx"; + request.metadata["knxGroupAddress"] = GatewayKnxGroupAddressString(group_address); + return request; +} + +DaliBridgeResult ErrorResult(uint16_t group_address, const char* message) { + DaliBridgeResult result; + result.sequence = "knx-" + GatewayKnxGroupAddressString(group_address); + result.error = message == nullptr ? "KNX error" : message; + return result; +} + +bool SendAll(int sock, const uint8_t* data, size_t len, const sockaddr_in& remote) { + return sendto(sock, data, len, 0, reinterpret_cast(&remote), + sizeof(remote)) == static_cast(len); +} + +std::vector KnxNetIpPacket(uint16_t service, const std::vector& body) { + std::vector packet(6 + body.size()); + packet[0] = kKnxNetIpHeaderSize; + packet[1] = kKnxNetIpVersion10; + WriteBe16(packet.data() + 2, service); + WriteBe16(packet.data() + 4, static_cast(packet.size())); + if (!body.empty()) { + std::memcpy(packet.data() + 6, body.data(), body.size()); + } + return packet; +} + +std::array HpaiForRemote(const sockaddr_in& remote) { + std::array hpai{}; + hpai[0] = 0x08; + hpai[1] = 0x01; + const uint32_t address = ntohl(remote.sin_addr.s_addr); + hpai[2] = static_cast((address >> 24) & 0xff); + hpai[3] = static_cast((address >> 16) & 0xff); + hpai[4] = static_cast((address >> 8) & 0xff); + hpai[5] = static_cast(address & 0xff); + WriteBe16(hpai.data() + 6, ntohs(remote.sin_port)); + return hpai; +} + +bool ParseKnxNetIpHeader(const uint8_t* data, size_t len, uint16_t* service, + uint16_t* total_len) { + if (data == nullptr || len < 6 || data[0] != kKnxNetIpHeaderSize || + data[1] != kKnxNetIpVersion10) { + return false; + } + *service = ReadBe16(data + 2); + *total_len = ReadBe16(data + 4); + return *total_len >= 6 && *total_len <= len; +} + +bool IsExtendedTpFrame(const uint8_t* data, size_t len) { + return len > 0 && (data[0] & 0xD3) == 0x10; +} + +size_t ExpectedTpFrameSize(const uint8_t* data, size_t len) { + if (data == nullptr || len < 6) { + return 0; + } + if (IsExtendedTpFrame(data, len)) { + return 9U + data[6]; + } + return 8U + (data[5] & 0x0F); +} + +bool ValidateTpChecksum(const uint8_t* data, size_t len) { + if (data == nullptr || len < 2) { + return false; + } + uint8_t crc = 0xFF; + for (size_t index = 0; index + 1 < len; ++index) { + crc ^= data[index]; + } + return data[len - 1] == crc; +} + +std::optional> CemiToTpTelegram(const uint8_t* data, size_t len) { + if (data == nullptr || len < 10 || data[1] != 0) { + return std::nullopt; + } + const uint8_t* ctrl = data + 2; + const bool standard = (ctrl[0] & 0x80) != 0; + const size_t tp_len = standard ? len - 2U : len - 1U; + if (tp_len < 8) { + return std::nullopt; + } + std::vector telegram(tp_len, 0); + if (standard) { + telegram[0] = ctrl[0]; + std::memcpy(telegram.data() + 1, ctrl + 2, 4); + telegram[5] = static_cast((ctrl[1] & 0xF0) | (ctrl[6] & 0x0F)); + if (tp_len > 7U) { + std::memcpy(telegram.data() + 6, ctrl + 7, tp_len - 7U); + } + } else { + std::memcpy(telegram.data(), ctrl, tp_len - 1U); + } + uint8_t crc = 0xFF; + for (size_t index = 0; index + 1 < telegram.size(); ++index) { + crc ^= telegram[index]; + } + telegram.back() = crc; + return telegram; +} + +std::optional> TpTelegramToCemi(const uint8_t* data, size_t len) { + if (data == nullptr || len < 8 || !ValidateTpChecksum(data, len)) { + return std::nullopt; + } + const bool extended = IsExtendedTpFrame(data, len); + const size_t cemi_len = len + (extended ? 2U : 3U) - 1U; + std::vector cemi(cemi_len, 0); + cemi[0] = kCemiLDataInd; + cemi[1] = 0x00; + cemi[2] = data[0]; + if (extended) { + std::memcpy(cemi.data() + 2, data, len - 1U); + } else { + cemi[3] = data[5] & 0xF0; + std::memcpy(cemi.data() + 4, data + 1, 4); + cemi[8] = data[5] & 0x0F; + const size_t copy_len = static_cast(cemi[8]) + 1U; + if (9U + copy_len > cemi.size() || 6U + copy_len > len) { + return std::nullopt; + } + std::memcpy(cemi.data() + 9, data + 6, copy_len); + } + return cemi; +} + +} // namespace + +std::optional GatewayKnxConfigFromValue(const DaliValue* value) { + if (value == nullptr || value->asObject() == nullptr) { + return std::nullopt; + } + const auto& object = *value->asObject(); + GatewayKnxConfig config; + config.dali_router_enabled = ObjectBoolAny(object, {"daliRouterEnabled", "dali_router_enabled"}) + .value_or(config.dali_router_enabled); + config.ip_router_enabled = ObjectBoolAny(object, {"ipRouterEnabled", "ip_router_enabled"}) + .value_or(config.ip_router_enabled); + config.tunnel_enabled = ObjectBoolAny(object, {"tunnelEnabled", "tunnel_enabled"}) + .value_or(config.tunnel_enabled); + config.multicast_enabled = ObjectBoolAny(object, {"multicastEnabled", "multicast_enabled"}) + .value_or(config.multicast_enabled); + config.main_group = static_cast( + std::clamp(ObjectIntAny(object, {"mainGroup", "main_group"}).value_or(config.main_group), + 0, 31)); + config.udp_port = static_cast(std::clamp( + ObjectIntAny(object, {"udpPort", "port", "udp_port"}).value_or(config.udp_port), 1, + 65535)); + config.multicast_address = ObjectStringAny(object, {"multicastAddress", "multicast_address"}) + .value_or(config.multicast_address); + config.individual_address = static_cast(std::clamp( + ObjectIntAny(object, {"individualAddress", "individual_address"}) + .value_or(config.individual_address), + 0, 0xffff)); + + const auto* tp_uart = getObjectValue(object, "tpUart"); + if (tp_uart == nullptr) { + tp_uart = getObjectValue(object, "tp_uart"); + } + if (tp_uart != nullptr && tp_uart->asObject() != nullptr) { + const auto& serial = *tp_uart->asObject(); + config.tp_uart.uart_port = std::clamp( + ObjectIntAny(serial, {"uartPort", "uart_port"}).value_or(config.tp_uart.uart_port), 0, + 2); + config.tp_uart.tx_pin = ObjectIntAny(serial, {"txPin", "tx_pin"}).value_or(config.tp_uart.tx_pin); + config.tp_uart.rx_pin = ObjectIntAny(serial, {"rxPin", "rx_pin"}).value_or(config.tp_uart.rx_pin); + config.tp_uart.baudrate = static_cast(std::max( + 1200, ObjectIntAny(serial, {"baudrate", "baud"}).value_or(config.tp_uart.baudrate))); + config.tp_uart.rx_buffer_size = static_cast(std::max( + 128, ObjectIntAny(serial, {"rxBufferSize", "rx_buffer_size"}) + .value_or(static_cast(config.tp_uart.rx_buffer_size)))); + config.tp_uart.tx_buffer_size = static_cast(std::max( + 128, ObjectIntAny(serial, {"txBufferSize", "tx_buffer_size"}) + .value_or(static_cast(config.tp_uart.tx_buffer_size)))); + config.tp_uart.read_timeout_ms = static_cast(std::max( + 1, ObjectIntAny(serial, {"readTimeoutMs", "read_timeout_ms"}) + .value_or(static_cast(config.tp_uart.read_timeout_ms)))); + } + return config; +} + +DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config) { + DaliValue::Object out; + out["daliRouterEnabled"] = config.dali_router_enabled; + out["ipRouterEnabled"] = config.ip_router_enabled; + out["tunnelEnabled"] = config.tunnel_enabled; + out["multicastEnabled"] = config.multicast_enabled; + out["mainGroup"] = static_cast(config.main_group); + out["udpPort"] = static_cast(config.udp_port); + out["multicastAddress"] = config.multicast_address; + out["individualAddress"] = static_cast(config.individual_address); + DaliValue::Object serial; + serial["uartPort"] = config.tp_uart.uart_port; + serial["txPin"] = config.tp_uart.tx_pin; + serial["rxPin"] = config.tp_uart.rx_pin; + serial["baudrate"] = static_cast(config.tp_uart.baudrate); + serial["rxBufferSize"] = static_cast(config.tp_uart.rx_buffer_size); + serial["txBufferSize"] = static_cast(config.tp_uart.tx_buffer_size); + serial["readTimeoutMs"] = static_cast(config.tp_uart.read_timeout_ms); + out["tpUart"] = std::move(serial); + return DaliValue(std::move(out)); +} + +const char* GatewayKnxDataTypeToString(GatewayKnxDaliDataType data_type) { + switch (data_type) { + case GatewayKnxDaliDataType::kSwitch: + return "switch"; + case GatewayKnxDaliDataType::kBrightness: + return "brightness"; + case GatewayKnxDaliDataType::kColorTemperature: + return "color_temperature"; + case GatewayKnxDaliDataType::kRgb: + return "rgb"; + case GatewayKnxDaliDataType::kUnknown: + default: + return "unknown"; + } +} + +const char* GatewayKnxTargetKindToString(GatewayKnxDaliTargetKind kind) { + switch (kind) { + case GatewayKnxDaliTargetKind::kBroadcast: + return "broadcast"; + case GatewayKnxDaliTargetKind::kShortAddress: + return "short_address"; + case GatewayKnxDaliTargetKind::kGroup: + return "group"; + case GatewayKnxDaliTargetKind::kNone: + default: + return "none"; + } +} + +std::optional GatewayKnxDaliDataTypeForMiddleGroup( + uint8_t middle_group) { + switch (middle_group) { + case 1: + return GatewayKnxDaliDataType::kSwitch; + case 2: + return GatewayKnxDaliDataType::kBrightness; + case 3: + return GatewayKnxDaliDataType::kColorTemperature; + case 4: + return GatewayKnxDaliDataType::kRgb; + default: + return std::nullopt; + } +} + +std::optional GatewayKnxDaliTargetForSubgroup(uint8_t sub_group) { + if (sub_group == 0) { + return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kBroadcast, 127}; + } + if (sub_group >= 1 && sub_group <= 64) { + return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kShortAddress, + static_cast(sub_group - 1)}; + } + if (sub_group >= 65 && sub_group <= 80) { + return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kGroup, + static_cast(sub_group - 65)}; + } + return std::nullopt; +} + +uint16_t GatewayKnxGroupAddress(uint8_t main_group, uint8_t middle_group, + uint8_t sub_group) { + return static_cast(((main_group & 0x1f) << 11) | + ((middle_group & 0x07) << 8) | sub_group); +} + +std::string GatewayKnxGroupAddressString(uint16_t group_address) { + const int main = (group_address >> 11) & 0x1f; + const int middle = (group_address >> 8) & 0x07; + const int sub = group_address & 0xff; + return std::to_string(main) + "/" + std::to_string(middle) + "/" + + std::to_string(sub); +} + +GatewayKnxBridge::GatewayKnxBridge(DaliBridgeEngine& engine) : engine_(engine) {} + +void GatewayKnxBridge::setConfig(const GatewayKnxConfig& config) { config_ = config; } + +const GatewayKnxConfig& GatewayKnxBridge::config() const { return config_; } + +std::vector GatewayKnxBridge::describeDaliBindings() const { + std::vector bindings; + bindings.reserve(4 * 81); + for (uint8_t middle = 1; middle <= 4; ++middle) { + const auto data_type = GatewayKnxDaliDataTypeForMiddleGroup(middle); + if (!data_type.has_value()) { + continue; + } + for (uint8_t sub = 0; sub <= 80; ++sub) { + const auto target = GatewayKnxDaliTargetForSubgroup(sub); + if (!target.has_value()) { + continue; + } + GatewayKnxDaliBinding binding; + binding.main_group = config_.main_group; + binding.middle_group = middle; + binding.sub_group = sub; + binding.group_address = GatewayKnxGroupAddress(config_.main_group, middle, sub); + binding.address = GatewayKnxGroupAddressString(binding.group_address); + binding.data_type = data_type.value(); + binding.target = target.value(); + binding.datapoint_type = DataTypeDpt(data_type.value()); + binding.name = TargetName(target.value()) + " - " + DataTypeName(data_type.value()); + bindings.push_back(std::move(binding)); + } + } + return bindings; +} + +DaliBridgeResult GatewayKnxBridge::handleCemiFrame(const uint8_t* data, size_t len) { + const auto decoded = DecodeCemiGroupWrite(data, len); + if (!decoded.has_value()) { + return ErrorResult(0, "unsupported or non group-write cEMI frame"); + } + return handleGroupWrite(decoded->group_address, decoded->data.data(), decoded->data.size()); +} + +DaliBridgeResult GatewayKnxBridge::handleGroupWrite(uint16_t group_address, const uint8_t* data, + size_t len) { + if (!config_.dali_router_enabled) { + return ErrorResult(group_address, "KNX to DALI router disabled"); + } + const uint8_t main = static_cast((group_address >> 11) & 0x1f); + const uint8_t middle = static_cast((group_address >> 8) & 0x07); + const uint8_t sub = static_cast(group_address & 0xff); + if (main != config_.main_group) { + return ErrorResult(group_address, "KNX main group does not match gateway config"); + } + const auto data_type = GatewayKnxDaliDataTypeForMiddleGroup(middle); + const auto target = GatewayKnxDaliTargetForSubgroup(sub); + if (!data_type.has_value() || !target.has_value()) { + return ErrorResult(group_address, "unmapped KNX group address"); + } + return executeForDecodedWrite(group_address, data_type.value(), target.value(), data, len); +} + +DaliBridgeResult GatewayKnxBridge::executeForDecodedWrite(uint16_t group_address, + GatewayKnxDaliDataType data_type, + GatewayKnxDaliTarget target, + const uint8_t* data, size_t len) { + if (target.kind == GatewayKnxDaliTargetKind::kNone) { + return ErrorResult(group_address, "missing DALI target"); + } + switch (data_type) { + case GatewayKnxDaliDataType::kSwitch: { + if (data == nullptr || len < 1) { + return ErrorResult(group_address, "missing DPT1 switch payload"); + } + DaliBridgeRequest request = RequestForTarget( + group_address, target, (data[0] & 0x01) != 0 ? BridgeOperation::on : BridgeOperation::off); + return engine_.execute(request); + } + case GatewayKnxDaliDataType::kBrightness: { + if (data == nullptr || len < 1) { + return ErrorResult(group_address, "missing DPT5 brightness payload"); + } + DaliBridgeRequest request = RequestForTarget(group_address, target, + BridgeOperation::setBrightnessPercent); + request.value = (static_cast(data[0]) * 100.0) / 255.0; + return engine_.execute(request); + } + case GatewayKnxDaliDataType::kColorTemperature: { + if (data == nullptr || len < 2) { + return ErrorResult(group_address, "missing DPT7 color temperature payload"); + } + DaliBridgeRequest request = RequestForTarget(group_address, target, + BridgeOperation::setColorTemperature); + request.value = static_cast(ReadBe16(data)); + return engine_.execute(request); + } + case GatewayKnxDaliDataType::kRgb: { + if (data == nullptr || len < 3) { + return ErrorResult(group_address, "missing DPT232 RGB payload"); + } + DaliBridgeRequest request = RequestForTarget(group_address, target, + BridgeOperation::setColourRGB); + DaliValue::Object rgb; + rgb["r"] = static_cast(data[0]); + rgb["g"] = static_cast(data[1]); + rgb["b"] = static_cast(data[2]); + request.value = std::move(rgb); + return engine_.execute(request); + } + case GatewayKnxDaliDataType::kUnknown: + default: + return ErrorResult(group_address, "unsupported KNX data type"); + } +} + +GatewayKnxTpIpRouter::GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHandler handler) + : bridge_(bridge), handler_(std::move(handler)) {} + +GatewayKnxTpIpRouter::~GatewayKnxTpIpRouter() { stop(); } + +void GatewayKnxTpIpRouter::setConfig(const GatewayKnxConfig& config) { config_ = config; } + +const GatewayKnxConfig& GatewayKnxTpIpRouter::config() const { return config_; } + +esp_err_t GatewayKnxTpIpRouter::start(uint32_t task_stack_size, UBaseType_t task_priority) { + if (started_ || task_handle_ != nullptr) { + return ESP_OK; + } + if (!config_.ip_router_enabled) { + return ESP_ERR_NOT_SUPPORTED; + } + stop_requested_ = false; + last_error_.clear(); + const BaseType_t created = xTaskCreate(&GatewayKnxTpIpRouter::TaskEntry, "gw_knx_ip", + task_stack_size, this, task_priority, &task_handle_); + if (created != pdPASS) { + task_handle_ = nullptr; + return ESP_ERR_NO_MEM; + } + started_ = true; + return ESP_OK; +} + +esp_err_t GatewayKnxTpIpRouter::stop() { + stop_requested_ = true; + closeSockets(); + return ESP_OK; +} + +bool GatewayKnxTpIpRouter::started() const { return started_; } + +const std::string& GatewayKnxTpIpRouter::lastError() const { return last_error_; } + +void GatewayKnxTpIpRouter::TaskEntry(void* arg) { + static_cast(arg)->taskLoop(); +} + +void GatewayKnxTpIpRouter::taskLoop() { + if (!configureSocket()) { + finishTask(); + return; + } + configureTpUart(); + + std::array buffer{}; + while (!stop_requested_) { + sockaddr_in remote{}; + socklen_t remote_len = sizeof(remote); + const int received = recvfrom(udp_sock_, buffer.data(), buffer.size(), 0, + reinterpret_cast(&remote), &remote_len); + if (received <= 0) { + pollTpUart(); + if (!stop_requested_) { + vTaskDelay(pdMS_TO_TICKS(10)); + } + continue; + } + handleUdpDatagram(buffer.data(), static_cast(received), remote); + pollTpUart(); + } + finishTask(); +} + +void GatewayKnxTpIpRouter::finishTask() { + closeSockets(); + started_ = false; + task_handle_ = nullptr; + vTaskDelete(nullptr); +} + +void GatewayKnxTpIpRouter::closeSockets() { + if (udp_sock_ >= 0) { + shutdown(udp_sock_, SHUT_RDWR); + close(udp_sock_); + udp_sock_ = -1; + } + if (tp_uart_port_ >= 0) { + uart_driver_delete(static_cast(tp_uart_port_)); + tp_uart_port_ = -1; + } +} + +bool GatewayKnxTpIpRouter::configureSocket() { + udp_sock_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + if (udp_sock_ < 0) { + last_error_ = "failed to create KNXnet/IP UDP socket"; + return false; + } + int reuse = 1; + setsockopt(udp_sock_, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); + + sockaddr_in bind_addr{}; + bind_addr.sin_family = AF_INET; + bind_addr.sin_addr.s_addr = htonl(INADDR_ANY); + bind_addr.sin_port = htons(config_.udp_port); + if (bind(udp_sock_, reinterpret_cast(&bind_addr), sizeof(bind_addr)) < 0) { + last_error_ = "failed to bind KNXnet/IP UDP socket"; + closeSockets(); + return false; + } + + timeval timeout{}; + timeout.tv_sec = 0; + timeout.tv_usec = 20000; + setsockopt(udp_sock_, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + + if (config_.multicast_enabled) { + uint8_t multicast_loop = 0; + setsockopt(udp_sock_, IPPROTO_IP, IP_MULTICAST_LOOP, &multicast_loop, + sizeof(multicast_loop)); + ip_mreq mreq{}; + mreq.imr_multiaddr.s_addr = inet_addr(config_.multicast_address.c_str()); + mreq.imr_interface.s_addr = htonl(INADDR_ANY); + if (setsockopt(udp_sock_, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) { + ESP_LOGW(kTag, "failed to join KNX multicast group %s", config_.multicast_address.c_str()); + } + } + return true; +} + +bool GatewayKnxTpIpRouter::configureTpUart() { + const auto& serial = config_.tp_uart; + if (serial.uart_port < 0 || serial.uart_port > 2) { + return false; + } + uart_config_t uart_config{}; + uart_config.baud_rate = static_cast(serial.baudrate); + uart_config.data_bits = UART_DATA_8_BITS; + uart_config.parity = UART_PARITY_EVEN; + 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(serial.uart_port); + if (uart_param_config(uart_port, &uart_config) != ESP_OK) { + return false; + } + if (uart_set_pin(uart_port, serial.tx_pin, serial.rx_pin, UART_PIN_NO_CHANGE, + UART_PIN_NO_CHANGE) != ESP_OK) { + return false; + } + if (uart_driver_install(uart_port, serial.rx_buffer_size, serial.tx_buffer_size, 0, nullptr, + 0) != ESP_OK) { + return false; + } + tp_uart_port_ = serial.uart_port; + return true; +} + +void GatewayKnxTpIpRouter::handleUdpDatagram(const uint8_t* data, size_t len, + const sockaddr_in& remote) { + uint16_t service = 0; + uint16_t total_len = 0; + if (!ParseKnxNetIpHeader(data, len, &service, &total_len)) { + return; + } + const uint8_t* body = data + 6; + const size_t body_len = total_len - 6; + switch (service) { + case kServiceRoutingIndication: + if (config_.multicast_enabled) { + handleRoutingIndication(body, body_len); + } + break; + case kServiceTunnellingRequest: + if (config_.tunnel_enabled) { + handleTunnellingRequest(body, body_len, remote); + } + break; + case kServiceConnectRequest: + if (config_.tunnel_enabled) { + handleConnectRequest(body, body_len, remote); + } + break; + case kServiceConnectionStateRequest: + handleConnectionStateRequest(body, body_len, remote); + break; + case kServiceDisconnectRequest: + handleDisconnectRequest(body, body_len, remote); + break; + default: + break; + } +} + +void GatewayKnxTpIpRouter::handleRoutingIndication(const uint8_t* body, size_t len) { + if (body == nullptr || len == 0) { + return; + } + const DaliBridgeResult result = handler_(body, len); + if (!result.ok && !result.error.empty()) { + ESP_LOGD(kTag, "KNX routing indication ignored: %s", result.error.c_str()); + } + forwardCemiToTp(body, len); +} + +void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* body, size_t len, + const sockaddr_in& remote) { + if (body == nullptr || len < 5 || body[0] != 0x04) { + return; + } + const uint8_t channel_id = body[1]; + const uint8_t sequence = body[2]; + if (!tunnel_connected_ || channel_id != tunnel_channel_id_) { + sendTunnellingAck(channel_id, sequence, kKnxErrorConnectionId, remote); + return; + } + if (sequence != expected_tunnel_sequence_) { + sendTunnellingAck(channel_id, sequence, kKnxErrorSequenceNumber, remote); + return; + } + expected_tunnel_sequence_ = static_cast((expected_tunnel_sequence_ + 1) & 0xff); + sendTunnellingAck(channel_id, sequence, kKnxNoError, remote); + const uint8_t* cemi = body + 4; + const size_t cemi_len = len - 4; + const DaliBridgeResult result = handler_(cemi, cemi_len); + if (!result.ok && !result.error.empty()) { + ESP_LOGD(kTag, "KNX tunnel frame not routed to DALI: %s", result.error.c_str()); + } + forwardCemiToTp(cemi, cemi_len); +} + +void GatewayKnxTpIpRouter::handleConnectRequest(const uint8_t* body, size_t len, + const sockaddr_in& remote) { + if (body == nullptr || len < 20) { + return; + } + const size_t cri_offset = 16; + if (body[cri_offset] < 4 || body[cri_offset + 1] != kKnxConnectionTypeTunnel || + body[cri_offset + 2] != kKnxTunnelLayerLink) { + sendConnectResponse(0, kKnxErrorConnectionType, remote); + return; + } + if (tunnel_connected_) { + sendConnectResponse(0, kKnxErrorNoMoreConnections, remote); + return; + } + tunnel_connected_ = true; + expected_tunnel_sequence_ = 0; + tunnel_send_sequence_ = 0; + tunnel_remote_ = remote; + sendConnectResponse(tunnel_channel_id_, kKnxNoError, remote); +} + +void GatewayKnxTpIpRouter::handleConnectionStateRequest(const uint8_t* body, size_t len, + const sockaddr_in& remote) { + if (body == nullptr || len < 2) { + return; + } + const uint8_t channel_id = body[0]; + sendConnectionStateResponse( + channel_id, tunnel_connected_ && channel_id == tunnel_channel_id_ ? kKnxNoError + : kKnxErrorConnectionId, + remote); +} + +void GatewayKnxTpIpRouter::handleDisconnectRequest(const uint8_t* body, size_t len, + const sockaddr_in& remote) { + if (body == nullptr || len < 2) { + return; + } + const uint8_t channel_id = body[0]; + const uint8_t status = tunnel_connected_ && channel_id == tunnel_channel_id_ + ? kKnxNoError + : kKnxErrorConnectionId; + if (status == kKnxNoError) { + tunnel_connected_ = false; + expected_tunnel_sequence_ = 0; + tunnel_send_sequence_ = 0; + } + sendDisconnectResponse(channel_id, status, remote); +} + +void GatewayKnxTpIpRouter::sendTunnellingAck(uint8_t channel_id, uint8_t sequence, + uint8_t status, const sockaddr_in& remote) { + const std::vector body{0x04, channel_id, sequence, status}; + const auto packet = KnxNetIpPacket(kServiceTunnellingAck, body); + SendAll(udp_sock_, packet.data(), packet.size(), remote); +} + +void GatewayKnxTpIpRouter::sendTunnelIndication(const uint8_t* data, size_t len) { + if (!tunnel_connected_ || udp_sock_ < 0 || data == nullptr || len == 0) { + return; + } + std::vector body; + body.reserve(4 + len); + body.push_back(0x04); + body.push_back(tunnel_channel_id_); + body.push_back(tunnel_send_sequence_++); + body.push_back(0x00); + body.insert(body.end(), data, data + len); + const auto packet = KnxNetIpPacket(kServiceTunnellingRequest, body); + SendAll(udp_sock_, packet.data(), packet.size(), tunnel_remote_); +} + +void GatewayKnxTpIpRouter::sendConnectionStateResponse(uint8_t channel_id, uint8_t status, + const sockaddr_in& remote) { + const std::vector body{channel_id, status}; + const auto packet = KnxNetIpPacket(kServiceConnectionStateResponse, body); + SendAll(udp_sock_, packet.data(), packet.size(), remote); +} + +void GatewayKnxTpIpRouter::sendDisconnectResponse(uint8_t channel_id, uint8_t status, + const sockaddr_in& remote) { + const std::vector body{channel_id, status}; + const auto packet = KnxNetIpPacket(kServiceDisconnectResponse, body); + SendAll(udp_sock_, packet.data(), packet.size(), remote); +} + +void GatewayKnxTpIpRouter::sendConnectResponse(uint8_t channel_id, uint8_t status, + const sockaddr_in& remote) { + std::vector body; + body.reserve(16); + body.push_back(channel_id); + body.push_back(status); + const auto data_endpoint = HpaiForRemote(remote); + body.insert(body.end(), data_endpoint.begin(), data_endpoint.end()); + body.push_back(0x04); + body.push_back(kKnxConnectionTypeTunnel); + body.push_back(static_cast((config_.individual_address >> 8) & 0xff)); + body.push_back(static_cast(config_.individual_address & 0xff)); + const auto packet = KnxNetIpPacket(kServiceConnectResponse, body); + SendAll(udp_sock_, packet.data(), packet.size(), remote); +} + +void GatewayKnxTpIpRouter::sendRoutingIndication(const uint8_t* data, size_t len) { + if (!config_.multicast_enabled || udp_sock_ < 0 || data == nullptr || len == 0) { + return; + } + sockaddr_in remote{}; + remote.sin_family = AF_INET; + remote.sin_port = htons(config_.udp_port); + remote.sin_addr.s_addr = inet_addr(config_.multicast_address.c_str()); + const std::vector body(data, data + len); + const auto packet = KnxNetIpPacket(kServiceRoutingIndication, body); + SendAll(udp_sock_, packet.data(), packet.size(), remote); +} + +void GatewayKnxTpIpRouter::pollTpUart() { + if (tp_uart_port_ < 0) { + return; + } + std::array buffer{}; + const int read = uart_read_bytes(static_cast(tp_uart_port_), buffer.data(), + buffer.size(), 0); + if (read <= 0) { + return; + } + for (int index = 0; index < read; ++index) { + tp_rx_frame_.push_back(buffer[index]); + const size_t expected = ExpectedTpFrameSize(tp_rx_frame_.data(), tp_rx_frame_.size()); + if (expected == 0) { + continue; + } + if (tp_rx_frame_.size() == expected) { + handleTpTelegram(tp_rx_frame_.data(), tp_rx_frame_.size()); + tp_rx_frame_.clear(); + } else if (tp_rx_frame_.size() > expected || tp_rx_frame_.size() > 263U) { + tp_rx_frame_.clear(); + } + } +} + +void GatewayKnxTpIpRouter::handleTpTelegram(const uint8_t* data, size_t len) { + const auto cemi = TpTelegramToCemi(data, len); + if (!cemi.has_value()) { + return; + } + const DaliBridgeResult result = handler_(cemi->data(), cemi->size()); + if (!result.ok && !result.error.empty()) { + ESP_LOGD(kTag, "KNX TP frame not routed to DALI: %s", result.error.c_str()); + } + sendTunnelIndication(cemi->data(), cemi->size()); + sendRoutingIndication(cemi->data(), cemi->size()); +} + +void GatewayKnxTpIpRouter::forwardCemiToTp(const uint8_t* data, size_t len) { + if (tp_uart_port_ < 0 || data == nullptr || len == 0) { + return; + } + const auto telegram = CemiToTpTelegram(data, len); + if (!telegram.has_value()) { + return; + } + uart_write_bytes(static_cast(tp_uart_port_), telegram->data(), telegram->size()); +} + +} // namespace gateway \ No newline at end of file