From 77fe8c1b0239d9926b9282595bc19f77a1d39806 Mon Sep 17 00:00:00 2001 From: Tony Date: Sat, 16 May 2026 09:34:14 +0800 Subject: [PATCH] feat(gateway): enhance DALI and KNX integration with tunnel confirmation handling and improved message routing Signed-off-by: Tony --- apps/gateway/partitions-4M-single.csv | 11 +- .../gateway_bridge/src/gateway_bridge.cpp | 34 ++- .../gateway_knx/include/gateway_knx.hpp | 5 +- components/gateway_knx/src/gateway_knx.cpp | 241 +++++++++++++++--- knx | 2 +- 5 files changed, 246 insertions(+), 47 deletions(-) diff --git a/apps/gateway/partitions-4M-single.csv b/apps/gateway/partitions-4M-single.csv index 2c5a9dd..e2c57ea 100644 --- a/apps/gateway/partitions-4M-single.csv +++ b/apps/gateway/partitions-4M-single.csv @@ -1,6 +1,7 @@ # Name, Type, SubType, Offset, Size, Flags -nvs, data, nvs, 0x9000, 0x6000, -otadata, data, ota, 0xf000, 0x2000, -phy_init, data, phy, 0x11000, 0x1000, -factory, app, factory, 0x20000, 0x280000, -storage, data, spiffs, 0x300000, 0x100000, +nvs, data, nvs, 0x9000, 0x14000, +otadata, data, ota, 0x1D000, 0x2000, +phy_init, data, phy, 0x1F000, 0x1000, +factory, app, factory, 0x20000, 0x300000, +knxprops, data, 0x40, 0x320000, 0x60000, +storage, data, spiffs, 0x380000, 0x80000, diff --git a/components/gateway_bridge/src/gateway_bridge.cpp b/components/gateway_bridge/src/gateway_bridge.cpp index dda67d1..8932bc3 100644 --- a/components/gateway_bridge/src/gateway_bridge.cpp +++ b/components/gateway_bridge/src/gateway_bridge.cpp @@ -321,6 +321,9 @@ std::optional DecodeKnxDaliTarget(uint8_t raw_addr) { return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kGroup, static_cast((raw_addr - kDaliGroupRawMin) >> 1)}; } + if (raw_addr == 0xFE || raw_addr == 0xFF) { + return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kBroadcast, 127}; + } return std::nullopt; } @@ -4085,7 +4088,36 @@ void GatewayBridgeService::handleDaliRawFrame(const DaliRawFrame& frame) { if (!owner->knx_started || owner->knx_router == nullptr) { return; } - owner->knx_router->publishDaliStatus(update->target, update->actual_level); + + auto publish_target = [&](GatewayKnxDaliTargetKind kind, int address) { + owner->knx_router->publishDaliStatus(GatewayKnxDaliTarget{kind, address}, + update->actual_level); + }; + + publish_target(update->target.kind, update->target.address); + + if (update->target.kind == GatewayKnxDaliTargetKind::kGroup && + update->target.address >= 0 && update->target.address < 16) { + const uint16_t group_bit = static_cast(1U << update->target.address); + for (int short_address = 0; short_address <= kMaxDaliShortAddress; ++short_address) { + const auto state = owner->cache.daliAddressState(frame.gateway_id, + static_cast(short_address)); + if (!state.group_mask_known || (state.group_mask & group_bit) == 0) { + continue; + } + publish_target(GatewayKnxDaliTargetKind::kShortAddress, short_address); + } + return; + } + + if (update->target.kind == GatewayKnxDaliTargetKind::kBroadcast) { + for (int group = 0; group < 16; ++group) { + publish_target(GatewayKnxDaliTargetKind::kGroup, group); + } + for (int short_address = 0; short_address <= kMaxDaliShortAddress; ++short_address) { + publish_target(GatewayKnxDaliTargetKind::kShortAddress, short_address); + } + } } void GatewayBridgeService::collectUsedRuntimeResources( diff --git a/components/gateway_knx/include/gateway_knx.hpp b/components/gateway_knx/include/gateway_knx.hpp index b509a18..0ab0fa9 100644 --- a/components/gateway_knx/include/gateway_knx.hpp +++ b/components/gateway_knx/include/gateway_knx.hpp @@ -252,11 +252,14 @@ class GatewayKnxTpIpRouter { uint8_t connection_type{0}; uint8_t received_sequence{255}; uint8_t send_sequence{0}; + uint8_t last_tunnel_confirmation_sequence{0}; uint16_t individual_address{0}; int tcp_sock{-1}; TickType_t last_activity_tick{0}; ::sockaddr_in control_remote{}; ::sockaddr_in data_remote{}; + std::vector last_received_cemi; + std::vector last_tunnel_confirmation_packet; }; static void TaskEntry(void* arg); @@ -297,7 +300,7 @@ class GatewayKnxTpIpRouter { void sendSecureSessionStatus(uint8_t status, const ::sockaddr_in& remote); void sendTunnelIndication(const uint8_t* data, size_t len); void sendTunnelIndicationToClient(TunnelClient& client, const uint8_t* data, size_t len); - void sendCemiFrameToClient(TunnelClient& client, uint16_t service, + bool sendCemiFrameToClient(TunnelClient& client, uint16_t service, const uint8_t* data, size_t len); void sendConnectionStateResponse(uint8_t channel_id, uint8_t status, const ::sockaddr_in& remote); diff --git a/components/gateway_knx/src/gateway_knx.cpp b/components/gateway_knx/src/gateway_knx.cpp index 81d9360..d2966b4 100644 --- a/components/gateway_knx/src/gateway_knx.cpp +++ b/components/gateway_knx/src/gateway_knx.cpp @@ -718,6 +718,44 @@ std::string HexBytes(const uint8_t* data, size_t len) { return out; } +std::optional CemiMessageCode(const uint8_t* data, size_t len) { + if (data == nullptr || len == 0) { + return std::nullopt; + } + return static_cast(data[0]); +} + +uint16_t KnxIpServiceForCemi(const uint8_t* data, size_t len, uint16_t fallback_service) { + const auto message_code = CemiMessageCode(data, len); + if (!message_code.has_value()) { + return fallback_service; + } + switch (message_code.value()) { + case L_data_req: + case L_data_con: + case L_data_ind: + return kServiceTunnellingRequest; + default: + return kServiceDeviceConfigurationRequest; + } +} + +bool BuildTunnelConfirmationFrame(const uint8_t* data, size_t len, + std::vector* confirmation) { + if (data == nullptr || confirmation == nullptr || len < 2) { + return false; + } + std::vector frame_data(data, data + len); + CemiFrame frame(frame_data.data(), static_cast(frame_data.size())); + if (!frame.valid() || frame.messageCode() != L_data_req) { + return false; + } + frame.messageCode(L_data_con); + frame.confirm(ConfirmNoError); + *confirmation = std::move(frame_data); + return true; +} + DaliBridgeRequest RequestForTarget(uint16_t group_address, const GatewayKnxDaliTarget& target, BridgeOperation operation) { @@ -2327,6 +2365,9 @@ bool GatewayKnxTpIpRouter::publishDaliStatus(const GatewayKnxDaliTarget& target, kGwReg1GrpKoBlockSize * static_cast(target.address); switch_object = base + kGwReg1KoSwitchState; dimm_object = base + kGwReg1KoDimmState; + } else if (target.kind == GatewayKnxDaliTargetKind::kBroadcast) { + switch_object = kGwReg1AppKoBroadcastSwitch; + dimm_object = kGwReg1AppKoBroadcastDimm; } else { return false; } @@ -2982,6 +3023,15 @@ void GatewayKnxTpIpRouter::handleUdpDatagram(const uint8_t* data, size_t len, static_cast(service), static_cast(body[1]), static_cast(body[2]), static_cast(body[3]), EndpointString(remote).c_str()); + TunnelClient* client = findTunnelClient(body[1]); + if (client != nullptr) { + client->last_activity_tick = xTaskGetTickCount(); + if (service == kServiceTunnellingAck && body[3] == kKnxNoError && + !client->last_tunnel_confirmation_packet.empty() && + client->last_tunnel_confirmation_sequence == body[2]) { + client->last_tunnel_confirmation_packet.clear(); + } + } } break; case kServiceRoutingIndication: @@ -3123,6 +3173,22 @@ void GatewayKnxTpIpRouter::handleRoutingIndication(const uint8_t* packet_data, s } const uint8_t* cemi = frame.data(); const size_t cemi_len = frame.dataLength(); + bool consumed_by_local_application = false; + if (frame.messageCode() == L_data_req && frame.addressType() == IndividualAddress && + ets_device_ != nullptr) { + const uint16_t dest = frame.destinationAddress(); + const uint16_t own_address = ets_device_->individualAddress(); + const uint16_t client_address = ets_device_->tunnelClientAddress(); + const bool commissioning = !ets_device_->configured() || ets_device_->programmingMode(); + if (dest == own_address || dest == client_address || + (commissioning && dest == 0xffff)) { + consumed_by_local_application = + handleOpenKnxTunnelFrame(cemi, cemi_len, nullptr, kServiceRoutingIndication); + } + } + if (consumed_by_local_application) { + return; + } const bool consumed_by_openknx = handleOpenKnxBusFrame(cemi, cemi_len); const bool routed_to_dali = routeOpenKnxGroupWrite(cemi, cemi_len, "KNX routing indication"); const bool sent_to_tp = transmitOpenKnxTpFrame(cemi, cemi_len); @@ -3284,23 +3350,6 @@ void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* packet_data, s sendTunnellingAck(channel_id, sequence, kKnxErrorConnectionId, remote); return; } - if (sequence == client->received_sequence) { - ESP_LOGD(kTag, "duplicate KNXnet/IP tunnelling request channel=%u seq=%u", - static_cast(channel_id), static_cast(sequence)); - sendTunnellingAck(channel_id, sequence, kKnxNoError, client->data_remote); - return; - } - if (static_cast(sequence - 1) != client->received_sequence) { - ESP_LOGW(kTag, "reject KNXnet/IP tunnelling request channel=%u seq=%u expected=%u from %s", - static_cast(channel_id), static_cast(sequence), - static_cast(static_cast(client->received_sequence + 1)), - EndpointString(remote).c_str()); - sendTunnellingAck(channel_id, sequence, kKnxErrorSequenceNumber, remote); - return; - } - client->received_sequence = sequence; - client->last_activity_tick = xTaskGetTickCount(); - sendTunnellingAck(channel_id, sequence, kKnxNoError, client->data_remote); CemiFrame& frame = tunneling.frame(); if (!frame.valid()) { ESP_LOGW(kTag, "invalid OpenKNX tunnel cEMI channel=%u seq=%u from %s", @@ -3313,6 +3362,46 @@ void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* packet_data, s } const uint8_t* cemi = frame.data(); const size_t cemi_len = frame.dataLength(); + const std::vector current_cemi(cemi, cemi + cemi_len); + const bool duplicate_sequence = sequence == client->received_sequence; + const bool duplicate_payload = duplicate_sequence && client->last_received_cemi == current_cemi; + if (duplicate_payload) { + ESP_LOGD(kTag, "duplicate KNXnet/IP tunnelling request channel=%u seq=%u", + static_cast(channel_id), static_cast(sequence)); + sendTunnellingAck(channel_id, sequence, kKnxNoError, client->data_remote); + if (!client->last_tunnel_confirmation_packet.empty()) { + if (sendPacketToTunnelClient(*client, client->last_tunnel_confirmation_packet)) { + ESP_LOGI(kTag, + "resent cached KNXnet/IP tunnel confirmation channel=%u confirmSeq=%u after duplicate req seq=%u to %s", + static_cast(channel_id), + static_cast(client->last_tunnel_confirmation_sequence), + static_cast(sequence), EndpointString(client->data_remote).c_str()); + } else { + ESP_LOGW(kTag, + "failed to resend cached KNXnet/IP tunnel confirmation channel=%u confirmSeq=%u to %s", + static_cast(channel_id), + static_cast(client->last_tunnel_confirmation_sequence), + EndpointString(client->data_remote).c_str()); + } + } + return; + } + if (duplicate_sequence) { + ESP_LOGW(kTag, + "accept KNXnet/IP tunnelling request channel=%u with repeated seq=%u because cEMI payload changed", + static_cast(channel_id), static_cast(sequence)); + } else if (static_cast(sequence - 1) != client->received_sequence) { + ESP_LOGW(kTag, "reject KNXnet/IP tunnelling request channel=%u seq=%u expected=%u from %s", + static_cast(channel_id), static_cast(sequence), + static_cast(static_cast(client->received_sequence + 1)), + EndpointString(remote).c_str()); + sendTunnellingAck(channel_id, sequence, kKnxErrorSequenceNumber, remote); + return; + } + client->received_sequence = sequence; + client->last_received_cemi = current_cemi; + client->last_activity_tick = xTaskGetTickCount(); + sendTunnellingAck(channel_id, sequence, kKnxNoError, client->data_remote); ESP_LOGI(kTag, "rx KNXnet/IP tunnelling request channel=%u seq=%u cemiLen=%u from %s", static_cast(channel_id), static_cast(sequence), static_cast(cemi_len), EndpointString(remote).c_str()); @@ -3320,6 +3409,13 @@ 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) { + 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) { return; } @@ -3604,13 +3700,16 @@ bool GatewayKnxTpIpRouter::currentTransportAllowsTcpHpai() const { std::optional> GatewayKnxTpIpRouter::localHpaiForRemote( const sockaddr_in& remote, bool tcp) const { + std::array hpai{}; + hpai[0] = 0x08; + hpai[1] = tcp ? kKnxHpaiIpv4Tcp : kKnxHpaiIpv4Udp; + if (tcp) { + return hpai; + } const auto netif = SelectKnxNetifForRemote(remote); if (!netif.has_value()) { return std::nullopt; } - std::array hpai{}; - hpai[0] = 0x08; - hpai[1] = tcp ? kKnxHpaiIpv4Tcp : kKnxHpaiIpv4Udp; WriteIp(hpai.data() + 2, netif->address); WriteBe16(hpai.data() + 6, config_.udp_port); return hpai; @@ -3779,10 +3878,10 @@ void GatewayKnxTpIpRouter::sendTunnelIndicationToClient(TunnelClient& client, co sendCemiFrameToClient(client, kServiceTunnellingRequest, data, len); } -void GatewayKnxTpIpRouter::sendCemiFrameToClient(TunnelClient& client, uint16_t service, +bool GatewayKnxTpIpRouter::sendCemiFrameToClient(TunnelClient& client, uint16_t service, const uint8_t* data, size_t len) { if (!client.connected || data == nullptr || len == 0) { - return; + return false; } std::vector frame_data(data, data + len); CemiFrame frame(frame_data.data(), static_cast(frame_data.size())); @@ -3790,20 +3889,34 @@ void GatewayKnxTpIpRouter::sendCemiFrameToClient(TunnelClient& client, uint16_t ESP_LOGW(kTag, "not sending invalid OpenKNX cEMI service=0x%04x len=%u to %s", static_cast(service), static_cast(len), EndpointString(client.data_remote).c_str()); - return; + return false; } KnxIpTunnelingRequest request(frame); request.serviceTypeIdentifier(service); request.connectionHeader().length(LEN_CH); request.connectionHeader().channelId(client.channel_id); - request.connectionHeader().sequenceCounter(client.send_sequence++); + const auto message_code = CemiMessageCode(data, len); + const uint8_t send_sequence = client.send_sequence++; + request.connectionHeader().sequenceCounter(send_sequence); request.connectionHeader().status(kKnxNoError); const std::vector packet(request.data(), request.data() + request.totalLength()); - sendPacketToTunnelClient(client, packet); + if (!sendPacketToTunnelClient(client, packet)) { + ESP_LOGW(kTag, "failed to send KNXnet/IP cEMI service=0x%04x channel=%u seq=%u to %s", + static_cast(service), static_cast(client.channel_id), + static_cast(request.connectionHeader().sequenceCounter()), + EndpointString(client.data_remote).c_str()); + return false; + } + if (service == kServiceTunnellingRequest && message_code.has_value() && + message_code.value() == L_data_con) { + client.last_tunnel_confirmation_sequence = send_sequence; + client.last_tunnel_confirmation_packet = packet; + } ESP_LOGI(kTag, "sent KNXnet/IP cEMI service=0x%04x channel=%u seq=%u cemi=0x%02x len=%u to %s", static_cast(service), static_cast(client.channel_id), static_cast(request.connectionHeader().sequenceCounter()), static_cast(data[0]), static_cast(len), EndpointString(client.data_remote).c_str()); + return true; } void GatewayKnxTpIpRouter::sendConnectionStateResponse(uint8_t channel_id, uint8_t status, @@ -3844,18 +3957,26 @@ void GatewayKnxTpIpRouter::sendConnectResponse(uint8_t channel_id, uint8_t statu } KnxIpConnectResponse response(*knx_ip_parameters_, tunnel_address, config_.udp_port, channel_id, connection_type); + const bool tcp = currentTransportAllowsTcpHpai(); const uint32_t endpoint_address = ntohl(netif->address); - response.controlEndpoint().code(currentTransportAllowsTcpHpai() ? IPV4_TCP : IPV4_UDP); - response.controlEndpoint().ipAddress(endpoint_address); - response.controlEndpoint().ipPortNumber(config_.udp_port); + response.controlEndpoint().code(tcp ? IPV4_TCP : IPV4_UDP); + response.controlEndpoint().ipAddress(tcp ? 0 : endpoint_address); + response.controlEndpoint().ipPortNumber(tcp ? 0 : config_.udp_port); const std::vector packet(response.data(), response.data() + response.totalLength()); sendPacket(packet, remote); - ESP_LOGI(kTag, "sent KNXnet/IP connect response channel=%u type=0x%02x to %s endpoint=%u.%u.%u.%u:%u", + std::string endpoint_string; + if (tcp) { + endpoint_string = "0.0.0.0:0 (TCP HPAI)"; + } else { + sockaddr_in local_endpoint{}; + local_endpoint.sin_family = AF_INET; + local_endpoint.sin_port = htons(config_.udp_port); + local_endpoint.sin_addr.s_addr = netif->address; + endpoint_string = EndpointString(local_endpoint); + } + ESP_LOGI(kTag, "sent KNXnet/IP connect response channel=%u type=0x%02x to %s endpoint=%s", static_cast(channel_id), static_cast(connection_type), - EndpointString(remote).c_str(), static_cast((endpoint_address >> 24) & 0xff), - static_cast((endpoint_address >> 16) & 0xff), - static_cast((endpoint_address >> 8) & 0xff), - static_cast(endpoint_address & 0xff), static_cast(config_.udp_port)); + EndpointString(remote).c_str(), endpoint_string.c_str()); } void GatewayKnxTpIpRouter::sendRoutingIndication(const uint8_t* data, size_t len) { @@ -3908,15 +4029,57 @@ bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t if (ets_device_ == nullptr) { return false; } + std::vector tunnel_confirmation; + const bool needs_tunnel_confirmation = + response_client != nullptr && response_client->connected && + response_service == kServiceTunnellingRequest && + BuildTunnelConfirmationFrame(data, len, &tunnel_confirmation); + bool sent_tunnel_confirmation = false; const bool consumed = ets_device_->handleTunnelFrame( - data, len, [this, response_client, response_service](const uint8_t* response, - size_t response_len) { - if (response_client != nullptr && response_client->connected) { - sendCemiFrameToClient(*response_client, response_service, response, response_len); - } else { - sendTunnelIndication(response, response_len); + data, len, + [this, response_client, response_service, needs_tunnel_confirmation, + &tunnel_confirmation, &sent_tunnel_confirmation](const uint8_t* response, + size_t response_len) { + if (response == nullptr || response_len == 0) { + return; } + const bool routing_context = + response_client == nullptr && response_service == kServiceRoutingIndication; + const auto message_code = CemiMessageCode(response, response_len); + if (needs_tunnel_confirmation && !sent_tunnel_confirmation && + message_code.has_value() && message_code.value() != L_data_con) { + sent_tunnel_confirmation = sendCemiFrameToClient( + *response_client, kServiceTunnellingRequest, + tunnel_confirmation.data(), tunnel_confirmation.size()); + } + + const uint16_t service = KnxIpServiceForCemi(response, response_len, response_service); + if (service == kServiceDeviceConfigurationRequest) { + if (response_client != nullptr && response_client->connected) { + sendCemiFrameToClient(*response_client, service, response, response_len); + } else if (routing_context) { + sendRoutingIndication(response, response_len); + } + return; + } + if (message_code.has_value() && message_code.value() == L_data_con) { + if (response_client != nullptr && response_client->connected) { + sent_tunnel_confirmation = + sendCemiFrameToClient(*response_client, service, response, response_len) || + sent_tunnel_confirmation; + } + return; + } + if (routing_context) { + sendRoutingIndication(response, response_len); + return; + } + sendTunnelIndication(response, response_len); }); + if (needs_tunnel_confirmation && consumed && !sent_tunnel_confirmation) { + sendCemiFrameToClient(*response_client, kServiceTunnellingRequest, + tunnel_confirmation.data(), tunnel_confirmation.size()); + } syncOpenKnxConfigFromDevice(); return consumed; } diff --git a/knx b/knx index 82f22cf..c39e8cf 160000 --- a/knx +++ b/knx @@ -1 +1 @@ -Subproject commit 82f22cf5715de98e3f89512a641da28b90f628ff +Subproject commit c39e8cfc55e7de3519e0198cb5cce3c9b174d284