From 3af2995b401cf58db6345f9f6c16abca838df3da Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 19 May 2026 03:14:03 +0800 Subject: [PATCH] feat(gateway): add support for full IP forwarding of KNX TP telegrams and enhance tunnel frame handling Signed-off-by: Tony --- README.md | 2 +- apps/gateway/main/Kconfig.projbuild | 11 ++++ apps/gateway/sdkconfig | 1 + apps/gateway/sdkconfig.old | 2 + .../gateway_knx/src/ets_device_runtime.cpp | 3 - components/gateway_knx/src/gateway_knx.cpp | 57 +++++++++++++++++-- knx | 2 +- 7 files changed, 68 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 362ee9d..45b2ace 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ The native rewrite now wires a shared `gateway_core` bootstrap component, a mult KNX Data Secure and KNXnet/IP Secure support are controlled by `GATEWAY_KNX_DATA_SECURE_SUPPORTED` and `GATEWAY_KNX_IP_SECURE_SUPPORTED`. The current KNXnet/IP Secure flag reserves and reports secure service capability, while runtime secure-session transport is still reported as not implemented until that path is wired. The gateway derives its KNX serial identity from the ESP base MAC, and the development factory setup key is deterministically derived from that KNX serial so the same board keeps the same FDSK across NVS erases. -The KNXnet/IP tunnel can start from the built-in default configuration before any ETS download. KNX TP-UART is enabled only when `GATEWAY_KNX_TP_UART_PORT` is `0`, `1`, or `2`; set that UART port to `-1` for IP-only operation. UART TX/RX GPIO values of `-1` mean use the ESP-IDF target default pins for that UART, not disabled. `GATEWAY_KNX_TP_UART_9BIT_MODE` enables the NCN5120/OpenKNX-style 9-bit host frame on the wire, represented on ESP-IDF as 8 data bits plus even parity. Non-UART GPIO options use `-1` as disabled, including the KNX programming button, KNX programming LED, setup AP button, Wi-Fi reset button, and status LED. +The KNXnet/IP tunnel can start from the built-in default configuration before any ETS download. KNX TP-UART is enabled only when `GATEWAY_KNX_TP_UART_PORT` is `0`, `1`, or `2`; set that UART port to `-1` for IP-only operation. UART TX/RX GPIO values of `-1` mean use the ESP-IDF target default pins for that UART, not disabled. `GATEWAY_KNX_TP_UART_9BIT_MODE` enables the NCN5120/OpenKNX-style 9-bit host frame on the wire, represented on ESP-IDF as 8 data bits plus even parity. Enable `GATEWAY_KNX_TP_FULL_IP_FORWARD` when the gateway must mirror all physical TP telegrams back out to KNXnet/IP tunnelling and multicast so ETS can monitor or download other TP devices through the gateway's IP endpoint. Non-UART GPIO options use `-1` as disabled, including the KNX programming button, KNX programming LED, setup AP button, Wi-Fi reset button, and status LED. When no KNX bridge config or ETS application data has been downloaded, the KNXnet/IP router starts in commissioning mode: OpenKNX receives tunnel programming traffic from ETS, while DALI group routing and REG1-Dali function-property actions stay inactive until ETS reports a configured application. diff --git a/apps/gateway/main/Kconfig.projbuild b/apps/gateway/main/Kconfig.projbuild index fe1723f..3a23b7f 100644 --- a/apps/gateway/main/Kconfig.projbuild +++ b/apps/gateway/main/Kconfig.projbuild @@ -852,6 +852,17 @@ config GATEWAY_KNX_TP_UART_9BIT_MODE mode commonly described as 19200 baud 9-bit UART. Disable only for hardware wired for 8N1 host UART mode. + config GATEWAY_KNX_TP_FULL_IP_FORWARD + bool "Mirror all physical KNX TP telegrams to KNXnet/IP" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED && GATEWAY_KNX_TP_UART_PORT >= 0 + default n + help + Mirrors physical KNX TP telegrams received from the TP-UART line back + out through KNXnet/IP tunnelling and multicast even when the gateway + runs the single-interface ETS device runtime. Enable this when ETS must + monitor or download other TP devices through the gateway's IP endpoint. + Leave it disabled to preserve the narrower default forwarding behavior. + config GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE int "KNX/IP bridge task stack bytes" depends on GATEWAY_KNX_BRIDGE_SUPPORTED diff --git a/apps/gateway/sdkconfig b/apps/gateway/sdkconfig index 7dcc213..2afcd3f 100644 --- a/apps/gateway/sdkconfig +++ b/apps/gateway/sdkconfig @@ -699,6 +699,7 @@ CONFIG_GATEWAY_KNX_TP_RX_PIN=-1 CONFIG_GATEWAY_KNX_TP_BAUDRATE=19200 CONFIG_GATEWAY_KNX_TP_STARTUP_TIMEOUT_MS=2000 CONFIG_GATEWAY_KNX_TP_UART_9BIT_MODE=y +CONFIG_GATEWAY_KNX_TP_FULL_IP_FORWARD=y CONFIG_GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE=12288 CONFIG_GATEWAY_BRIDGE_KNX_TASK_PRIORITY=5 CONFIG_GATEWAY_CLOUD_BRIDGE_SUPPORTED=y diff --git a/apps/gateway/sdkconfig.old b/apps/gateway/sdkconfig.old index fa5257f..6b7be51 100644 --- a/apps/gateway/sdkconfig.old +++ b/apps/gateway/sdkconfig.old @@ -697,7 +697,9 @@ CONFIG_GATEWAY_KNX_TP_UART_PORT=0 CONFIG_GATEWAY_KNX_TP_TX_PIN=-1 CONFIG_GATEWAY_KNX_TP_RX_PIN=-1 CONFIG_GATEWAY_KNX_TP_BAUDRATE=19200 +CONFIG_GATEWAY_KNX_TP_STARTUP_TIMEOUT_MS=2000 CONFIG_GATEWAY_KNX_TP_UART_9BIT_MODE=y +# CONFIG_GATEWAY_KNX_TP_FULL_IP_FORWARD is not set CONFIG_GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE=12288 CONFIG_GATEWAY_BRIDGE_KNX_TASK_PRIORITY=5 CONFIG_GATEWAY_CLOUD_BRIDGE_SUPPORTED=y diff --git a/components/gateway_knx/src/ets_device_runtime.cpp b/components/gateway_knx/src/ets_device_runtime.cpp index 41d5f4b..21abca0 100644 --- a/components/gateway_knx/src/ets_device_runtime.cpp +++ b/components/gateway_knx/src/ets_device_runtime.cpp @@ -509,9 +509,6 @@ bool EtsDeviceRuntime::shouldConsumeTunnelFrame(CemiFrame& frame) const { case M_FuncPropStateRead_req: return true; case L_data_req: { - if (tpUartOnline()) { - return true; - } // In commissioning / programming mode ETS may address the device via its // individual address, the cEMI-client tunnel address (device+1), or the // unconfigured broadcast address 0xFFFF. Consume only those; let all diff --git a/components/gateway_knx/src/gateway_knx.cpp b/components/gateway_knx/src/gateway_knx.cpp index 8ce0277..26a9bdf 100644 --- a/components/gateway_knx/src/gateway_knx.cpp +++ b/components/gateway_knx/src/gateway_knx.cpp @@ -3476,16 +3476,20 @@ void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* packet_data, s const bool consumed_by_openknx = handleOpenKnxTunnelFrame( cemi, cemi_len, client, kServiceTunnellingRequest); const bool routed_to_dali = routeOpenKnxGroupWrite(cemi, cemi_len, "KNX tunnel frame"); - if (!consumed_by_openknx && routed_to_dali) { + const bool sent_to_tp = !consumed_by_openknx && !routed_to_dali && + transmitOpenKnxTpFrame(cemi, cemi_len); + if ((!consumed_by_openknx && routed_to_dali) || sent_to_tp) { std::vector tunnel_confirmation; if (BuildTunnelConfirmationFrame(cemi, cemi_len, &tunnel_confirmation)) { sendCemiFrameToClient(*client, kServiceTunnellingRequest, tunnel_confirmation.data(), tunnel_confirmation.size()); } } - if (consumed_by_openknx || routed_to_dali) { + if (consumed_by_openknx || routed_to_dali || sent_to_tp) { return; } + + ESP_LOGD(kTag, "KNX tunnel frame ignored: no OpenKNX/DALI/TP handler matched"); } void GatewayKnxTpIpRouter::handleDeviceConfigurationRequest(const uint8_t* packet_data, size_t len, @@ -3933,11 +3937,50 @@ void GatewayKnxTpIpRouter::sendTunnelIndication(const uint8_t* data, size_t len) if (data == nullptr || len == 0) { return; } - for (auto& client : tunnel_clients_) { - if (client.connected) { - sendTunnelIndicationToClient(client, data, len); + std::vector frame_data(data, data + len); + CemiFrame frame(frame_data.data(), static_cast(frame_data.size())); + if (!frame.valid()) { + ESP_LOGW(kTag, "not sending invalid OpenKNX tunnel indication len=%u", + static_cast(len)); + return; + } + + auto is_tunnel_recipient = [](const TunnelClient& client) { + return client.connected && client.connection_type == kKnxConnectionTypeTunnel; + }; + + auto send_to_client = [this, data, len](TunnelClient& client) { + sendTunnelIndicationToClient(client, data, len); + }; + + const bool suppress_source_echo = + frame.addressType() == GroupAddress || frame.addressType() == IndividualAddress; + const uint16_t source_address = suppress_source_echo ? frame.sourceAddress() : 0; + + if (frame.addressType() == IndividualAddress) { + for (auto& client : tunnel_clients_) { + if (!is_tunnel_recipient(client)) { + continue; + } + if (client.individual_address == source_address) { + continue; + } + if (client.individual_address == frame.destinationAddress()) { + send_to_client(client); + return; + } } } + + for (auto& client : tunnel_clients_) { + if (!is_tunnel_recipient(client)) { + continue; + } + if (suppress_source_echo && client.individual_address == source_address) { + continue; + } + send_to_client(client); + } } void GatewayKnxTpIpRouter::sendTunnelIndicationToClient(TunnelClient& client, const uint8_t* data, @@ -4152,6 +4195,10 @@ bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t sendRoutingIndication(response, response_len); return; } + if (response_client != nullptr && response_client->connected) { + sendCemiFrameToClient(*response_client, service, response, response_len); + return; + } sendTunnelIndication(response, response_len); }); if (needs_tunnel_confirmation && consumed && !sent_tunnel_confirmation) { diff --git a/knx b/knx index 51bb6cd..aaeb08f 160000 --- a/knx +++ b/knx @@ -1 +1 @@ -Subproject commit 51bb6cdf6a92e93ae38190a15f0e9a288fb9d1d9 +Subproject commit aaeb08f23272beadd18bd7ecbb90fdbc36a0d3a1