feat(gateway): add support for full IP forwarding of KNX TP telegrams and enhance tunnel frame handling

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-19 03:14:03 +08:00
parent b447da5bfc
commit 3af2995b40
7 changed files with 68 additions and 10 deletions
+1 -1
View File
@@ -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.
+11
View File
@@ -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
+1
View File
@@ -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
+2
View File
@@ -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
@@ -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
+52 -5
View File
@@ -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<uint8_t> 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<uint8_t> frame_data(data, data + len);
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
if (!frame.valid()) {
ESP_LOGW(kTag, "not sending invalid OpenKNX tunnel indication len=%u",
static_cast<unsigned>(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) {
+1 -1
Submodule knx updated: 51bb6cdf6a...aaeb08f232