From 2a3808c1e4638627fa21807395b26a11b6289441 Mon Sep 17 00:00:00 2001 From: Tony Date: Sat, 16 May 2026 01:50:26 +0800 Subject: [PATCH] feat(gateway): enhance OpenKNX integration with new DIB construction methods and improve BLE configuration Signed-off-by: Tony --- apps/gateway/sdkconfig | 4 +- .../gateway_knx/include/ets_device_runtime.h | 4 + .../gateway_knx/include/gateway_knx.hpp | 17 +- .../gateway_knx/src/ets_device_runtime.cpp | 34 +- components/gateway_knx/src/gateway_knx.cpp | 303 +++++++++--------- 5 files changed, 193 insertions(+), 169 deletions(-) diff --git a/apps/gateway/sdkconfig b/apps/gateway/sdkconfig index c65199a..5aa8f24 100644 --- a/apps/gateway/sdkconfig +++ b/apps/gateway/sdkconfig @@ -1130,8 +1130,8 @@ CONFIG_BT_CTRL_BLE_ADV=y # Common Options # CONFIG_BT_ALARM_MAX_NUM=50 -# CONFIG_BT_SMP_CRYPTO_STACK_TINYCRYPT is not set -CONFIG_BT_SMP_CRYPTO_STACK_MBEDTLS=y +CONFIG_BT_SMP_CRYPTO_STACK_TINYCRYPT=y +# CONFIG_BT_SMP_CRYPTO_STACK_MBEDTLS is not set # # BLE Log diff --git a/components/gateway_knx/include/ets_device_runtime.h b/components/gateway_knx/include/ets_device_runtime.h index a0a594d..acb4f15 100644 --- a/components/gateway_knx/include/ets_device_runtime.h +++ b/components/gateway_knx/include/ets_device_runtime.h @@ -36,6 +36,10 @@ class EtsDeviceRuntime { void toggleProgrammingMode(); EtsMemorySnapshot snapshot() const; + // Accessors for OpenKNX integration (DIB construction, IP parameter object). + DeviceObject& deviceObject(); + Platform& platform(); + void setFunctionPropertyHandlers(FunctionPropertyHandler command_handler, FunctionPropertyHandler state_handler); void setGroupWriteHandler(GroupWriteHandler handler); diff --git a/components/gateway_knx/include/gateway_knx.hpp b/components/gateway_knx/include/gateway_knx.hpp index df0b30f..7c11b73 100644 --- a/components/gateway_knx/include/gateway_knx.hpp +++ b/components/gateway_knx/include/gateway_knx.hpp @@ -3,6 +3,8 @@ #include "bridge.hpp" #include "model_value.hpp" +#include "knx/ip_parameter_object.h" + #include "esp_err.h" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" @@ -259,9 +261,8 @@ class GatewayKnxTpIpRouter { bool configureProgrammingGpio(); void refreshNetworkInterfaces(bool force_log = false); void handleUdpDatagram(const uint8_t* data, size_t len, const ::sockaddr_in& remote); - void handleSearchRequest(uint16_t service, const uint8_t* body, size_t len, - const ::sockaddr_in& remote); - void handleDescriptionRequest(const uint8_t* body, size_t len, + void handleSearchRequest(const uint8_t* data, size_t len, const ::sockaddr_in& remote); + void handleDescriptionRequest(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); @@ -290,15 +291,18 @@ class GatewayKnxTpIpRouter { const ::sockaddr_in& remote, uint8_t connection_type, uint16_t tunnel_address); void sendRoutingIndication(const uint8_t* data, size_t len); - void sendSearchResponse(uint16_t service, const ::sockaddr_in& remote, - const std::set& requested_dibs = {}); - void sendDescriptionResponse(const ::sockaddr_in& remote); bool sendPacket(const std::vector& packet, const ::sockaddr_in& remote) const; bool sendPacketToTunnelClient(const TunnelClient& client, const std::vector& packet) const; bool currentTransportAllowsTcpHpai() const; std::optional> localHpaiForRemote(const ::sockaddr_in& remote, bool tcp = false) const; + + // --- OpenKNX-backed DIB construction (uses KnxIpSearchResponse / KnxIpDescriptionResponse) --- + std::vector buildOpenKnxSearchResponse(const ::sockaddr_in& remote) const; + std::vector buildOpenKnxDescriptionResponse(const ::sockaddr_in& remote) const; + + // --- Hand-rolled DIB builders (fallback when OpenKNX is unavailable) --- std::vector buildDeviceInfoDib(const ::sockaddr_in& remote) const; std::vector buildSupportedServiceDib() const; std::vector buildExtendedDeviceInfoDib() const; @@ -355,6 +359,7 @@ class GatewayKnxTpIpRouter { TickType_t network_refresh_tick_{0}; std::array tcp_clients_{}; std::array tunnel_clients_{}; + std::unique_ptr knx_ip_parameters_; uint8_t last_tunnel_channel_id_{0}; std::vector tp_rx_frame_; std::vector tp_last_sent_telegram_; diff --git a/components/gateway_knx/src/ets_device_runtime.cpp b/components/gateway_knx/src/ets_device_runtime.cpp index a5879b6..33f468c 100644 --- a/components/gateway_knx/src/ets_device_runtime.cpp +++ b/components/gateway_knx/src/ets_device_runtime.cpp @@ -28,6 +28,7 @@ class ActiveFunctionPropertyRuntimeScope { }; constexpr uint16_t kInvalidIndividualAddress = 0xffff; +constexpr uint16_t kKnxUnconfiguredBroadcastAddress = 0xffff; // KNX broadcast IA for unconfigured devices constexpr uint16_t kReg1DaliManufacturerId = 0x00a4; constexpr uint8_t kReg1DaliApplicationNumber = 0x01; constexpr uint8_t kReg1DaliApplicationVersion = 0x05; @@ -125,6 +126,10 @@ void EtsDeviceRuntime::setProgrammingMode(bool enabled) { void EtsDeviceRuntime::toggleProgrammingMode() { setProgrammingMode(!programmingMode()); } +DeviceObject& EtsDeviceRuntime::deviceObject() { return device_.deviceObject(); } + +Platform& EtsDeviceRuntime::platform() { return platform_; } + EtsMemorySnapshot EtsDeviceRuntime::snapshot() const { EtsMemorySnapshot out; auto& device = const_cast(device_); @@ -318,19 +323,26 @@ bool EtsDeviceRuntime::shouldConsumeTunnelFrame(CemiFrame& frame) const { case M_FuncPropCommand_req: case M_FuncPropStateRead_req: return true; - case L_data_req: - if (!const_cast(device_).configured() || programmingMode()) { - return true; + case L_data_req: { + // 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 + // other individual-addressed frames (bus-scan, DeviceDescriptorRead, …) + // pass through to the physical TP-UART so real KNX devices on the bus + // can reply. + const uint16_t dest = frame.destinationAddress(); + const uint16_t own_address = individualAddress(); + const uint16_t client_address = tunnelClientAddress(); + const bool commissioning = !const_cast(device_).configured() || programmingMode(); + + if (frame.addressType() == IndividualAddress) { + if (dest == own_address || dest == client_address || + (commissioning && dest == kKnxUnconfiguredBroadcastAddress)) { + return true; + } } - if (frame.addressType() == IndividualAddress && - frame.destinationAddress() == individualAddress()) { - return true; - } - #ifdef USE_DATASECURE - return frame.addressType() == GroupAddress && frame.apdu().type() == SecureService; - #else return false; - #endif + } default: return false; } diff --git a/components/gateway_knx/src/gateway_knx.cpp b/components/gateway_knx/src/gateway_knx.cpp index 930ea96..e6dcd62 100644 --- a/components/gateway_knx/src/gateway_knx.cpp +++ b/components/gateway_knx/src/gateway_knx.cpp @@ -11,6 +11,10 @@ #include "ets_device_runtime.h" #include "soc/uart_periph.h" +#include "knx/cemi_frame.h" +#include "knx/knx_ip_search_response.h" +#include "knx/knx_ip_description_response.h" + #include #include #include @@ -741,17 +745,34 @@ bool IsKnxNetIpSecureService(uint16_t service) { } bool IsExtendedTpFrame(const uint8_t* data, size_t len) { - return len > 0 && (data[0] & 0xD3) == 0x10; + if (data == nullptr || len == 0) { + return false; + } + // TP-UART non-monitor mode: L_DATA_EXTENDED_IND indicator byte (0x10 with mask 0xD3) + if ((data[0] & 0xD3U) == 0x10U) { + return true; + } + // Raw bus frame (transparent / bus-monitor mode): bits 7-6 = 01, bit 4 = 1 + return (data[0] & 0xD0U) == 0x40U && (data[0] & 0x10U) != 0; } size_t ExpectedTpFrameSize(const uint8_t* data, size_t len) { if (data == nullptr || len < 6) { return 0; } + // TP-UART non-monitor mode prepends L_DATA_STANDARD_IND (0x90) or + // L_DATA_EXTENDED_IND (0x10) indicator bytes before each frame. + const bool has_indicator = (data[0] & 0xD3U) == 0x90U || (data[0] & 0xD3U) == 0x10U; + const size_t off = has_indicator ? 1U : 0U; + // Standard frame: ctrl(1)+src(2)+dst(2)+npci(1)=6, data(L), crc(1) + // Extended frame: ctrl(1)+src(2)+dst(2)+npci(1)+ext_len(1)=7, data(L), crc(1) + // With indicator byte prepended, add 1 to each base. if (IsExtendedTpFrame(data, len)) { - return 9U + data[6]; + const size_t base = has_indicator ? 9U : 8U; + return base + data[6U + off]; } - return 8U + (data[5] & 0x0F); + const size_t base = has_indicator ? 8U : 7U; + return base + (data[5U + off] & 0x0FU); } bool ValidateTpChecksum(const uint8_t* data, size_t len) { @@ -766,18 +787,44 @@ bool ValidateTpChecksum(const uint8_t* data, size_t len) { } bool IsTpUartControlByte(uint8_t byte) { + // L_Data.con: lower 7 bits must equal 0x0B (matches both positive 0x8B and negative 0x0B). + // Using the reference mask-based check to be robust across different TP-UART implementations. return byte == kTpUartResetIndication || - byte == kTpUartLDataConfirmPositive || - byte == kTpUartLDataConfirmNegative || byte == kTpUartBusy || - (byte & kTpUartStateIndicationMask) == kTpUartStateIndicationMask; + (byte & 0x7fU) == kTpUartLDataConfirmNegative || + byte == kTpUartBusy || + (byte & kTpUartStateIndicationMask) == kTpUartStateIndicationMask || + (byte & 0x33U) == 0x00U; // L_ACKN_IND } bool IsTpUartFrameStart(uint8_t byte, bool* extended) { if (extended == nullptr) { return false; } - *extended = (byte & 0x80) == 0; - return (byte & 0x50) == 0x10; + // TP-UART non-monitor mode: controller sends L_DATA_STANDARD_IND (0x90) or + // L_DATA_EXTENDED_IND (0x10) indicator bytes before each frame. + // TP-UART bus-monitor (transparent) mode: raw bus control bytes are passed through. + // Standard raw: bit7=1, bit6=0, bit4=1. Extended raw: bit7=0, bit6=1, bit4=1. + // Detect both indicator and raw formats. + const bool is_standard_indicator = (byte & 0xD3U) == 0x90U; // L_DATA_STANDARD_IND + const bool is_extended_indicator = (byte & 0xD3U) == 0x10U; // L_DATA_EXTENDED_IND + if (is_standard_indicator || is_extended_indicator) { + *extended = is_extended_indicator; + return true; + } + // Raw bus frame format (transparent / bus-monitor mode) + if ((byte & 0x10U) == 0) { + return false; // bit 4 must be set for valid KNX frame control bytes + } + const uint8_t top = byte & 0xC0U; + if (top == 0x80U) { + *extended = false; // standard frame + return true; + } + if (top == 0x40U) { + *extended = true; // extended frame + return true; + } + return false; } std::vector WrapTpUartTelegram(const std::vector& telegram) { @@ -2047,6 +2094,8 @@ esp_err_t GatewayKnxTpIpRouter::initializeRuntime() { ets_device_ = std::make_unique(openknx_namespace_, config_.individual_address, effectiveTunnelAddress()); + knx_ip_parameters_ = std::make_unique( + ets_device_->deviceObject(), ets_device_->platform()); openknx_configured_.store(ets_device_->configured()); ESP_LOGI(kTag, "OpenKNX runtime namespace=%s configured=%d ipInterface=0x%04x " @@ -2191,6 +2240,7 @@ void GatewayKnxTpIpRouter::finishTask() { { SemaphoreGuard guard(openknx_lock_); setProgrammingLed(false); + knx_ip_parameters_.reset(); ets_device_.reset(); openknx_configured_.store(false); } @@ -2322,7 +2372,11 @@ bool GatewayKnxTpIpRouter::configureSocket() { ESP_LOGW(kTag, "failed to enable TCP reuse for KNX port %u: errno=%d (%s)", static_cast(config_.udp_port), errno, std::strerror(errno)); } - if (bind(tcp_sock_, reinterpret_cast(&bind_addr), sizeof(bind_addr)) < 0) { + sockaddr_in tcp_bind_addr{}; + tcp_bind_addr.sin_family = AF_INET; + tcp_bind_addr.sin_addr.s_addr = htonl(INADDR_ANY); + tcp_bind_addr.sin_port = htons(config_.udp_port); + if (bind(tcp_sock_, reinterpret_cast(&tcp_bind_addr), sizeof(tcp_bind_addr)) < 0) { const int saved_errno = errno; last_error_ = ErrnoDetail("failed to bind KNXnet/IP TCP socket on port " + std::to_string(config_.udp_port), @@ -2686,7 +2740,7 @@ void GatewayKnxTpIpRouter::handleUdpDatagram(const uint8_t* data, size_t len, switch (service) { case kServiceSearchRequest: case kServiceSearchRequestExt: - handleSearchRequest(service, body, body_len, remote); + handleSearchRequest(body, body_len, remote); break; case kServiceDescriptionRequest: handleDescriptionRequest(body, body_len, remote); @@ -2732,7 +2786,7 @@ void GatewayKnxTpIpRouter::handleUdpDatagram(const uint8_t* data, size_t len, } } -void GatewayKnxTpIpRouter::handleSearchRequest(uint16_t service, const uint8_t* body, +void GatewayKnxTpIpRouter::handleSearchRequest(const uint8_t* body, size_t len, const sockaddr_in& remote) { if (HasUnsupportedHpaiProtocolAt(body, len, 0, currentTransportAllowsTcpHpai())) { ESP_LOGW(kTag, "ignore KNXnet/IP search request from %s: unsupported HPAI protocol", @@ -2741,48 +2795,27 @@ void GatewayKnxTpIpRouter::handleSearchRequest(uint16_t service, const uint8_t* } sockaddr_in response_remote = ResponseEndpointFromHpai(body, len, remote); selectOpenKnxNetworkInterface(response_remote); - std::set requested_dibs; - if (service == kServiceSearchRequestExt && body != nullptr && len > 8) { - size_t offset = 8; - while (offset + 2 <= len) { - const uint8_t srp_len = body[offset]; - if (srp_len < 2 || offset + srp_len > len) { - break; - } - const uint8_t srp_type = body[offset + 1]; - if (srp_type == 0x01) { - // The programming button belongs to the logical KNX-DALI device behind the tunnel. - return; - } else if (srp_type == 0x02 && srp_len >= 8) { - uint8_t mac[6]{}; - if (!ReadBaseMac(mac) || std::memcmp(mac, body + offset + 2, 6) != 0) { - return; - } - } else if (srp_type == 0x03) { - for (size_t service_offset = offset + 2; service_offset + 1 < offset + srp_len; - service_offset += 2) { - const uint8_t family = body[service_offset]; - const uint8_t version = body[service_offset + 1]; - if ((family == kKnxServiceFamilyCore && version > 2) || - (family == kKnxServiceFamilyDeviceManagement && version > 1) || - (family == kKnxServiceFamilyTunnelling && - (!config_.tunnel_enabled || version > 1)) || - (family == kKnxServiceFamilyRouting && - (!config_.multicast_enabled || version > 1))) { - return; - } - } - } else if (srp_type == 0x04) { - for (size_t dib_offset = offset + 2; dib_offset < offset + srp_len; ++dib_offset) { - requested_dibs.insert(body[dib_offset]); - } - } - offset += srp_len; - } + + const auto hpai = localHpaiForRemote(response_remote, currentTransportAllowsTcpHpai()); + if (!hpai.has_value()) { + ESP_LOGW(kTag, "cannot send KNXnet/IP search response to %s: no active IPv4 interface", + EndpointString(response_remote).c_str()); + return; } - sendSearchResponse(service == kServiceSearchRequestExt ? kServiceSearchResponseExt - : kServiceSearchResponse, - response_remote, requested_dibs); + std::vector body_resp; + body_resp.insert(body_resp.end(), hpai->begin(), hpai->end()); + auto dev_dib = buildDeviceInfoDib(response_remote); + body_resp.insert(body_resp.end(), dev_dib.begin(), dev_dib.end()); + auto svc_dib = buildSupportedServiceDib(); + body_resp.insert(body_resp.end(), svc_dib.begin(), svc_dib.end()); + const auto packet = KnxNetIpPacket(kServiceSearchResponse, body_resp); + sendPacket(packet, response_remote); + ESP_LOGI(kTag, "sent KNXnet/IP search response namespace=%s mainGroup=%u to %s:%u endpoint=%u.%u.%u.%u:%u", + openknx_namespace_.c_str(), static_cast(config_.main_group), + Ipv4String(response_remote.sin_addr.s_addr).c_str(), static_cast(ntohs(response_remote.sin_port)), + static_cast((*hpai)[2]), static_cast((*hpai)[3]), + static_cast((*hpai)[4]), static_cast((*hpai)[5]), + static_cast(config_.udp_port)); } void GatewayKnxTpIpRouter::handleDescriptionRequest(const uint8_t* body, size_t len, @@ -2794,7 +2827,18 @@ void GatewayKnxTpIpRouter::handleDescriptionRequest(const uint8_t* body, size_t } const sockaddr_in response_remote = ResponseEndpointFromHpai(body, len, remote); selectOpenKnxNetworkInterface(response_remote); - sendDescriptionResponse(response_remote); + auto device = buildDeviceInfoDib(response_remote); + auto services = buildSupportedServiceDib(); + std::vector body_resp; + body_resp.reserve(device.size() + services.size()); + body_resp.insert(body_resp.end(), device.begin(), device.end()); + body_resp.insert(body_resp.end(), services.begin(), services.end()); + const auto packet = KnxNetIpPacket(kServiceDescriptionResponse, body_resp); + sendPacket(packet, response_remote); + ESP_LOGI(kTag, "sent KNXnet/IP description response namespace=%s medium=0x%02x tpOnline=%d to %s:%u", + openknx_namespace_.c_str(), static_cast(advertisedMedium()), + tp_uart_online_, Ipv4String(response_remote.sin_addr.s_addr).c_str(), + static_cast(ntohs(response_remote.sin_port))); } void GatewayKnxTpIpRouter::handleRoutingIndication(const uint8_t* body, size_t len) { @@ -2978,12 +3022,19 @@ void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* body, size_t l 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()); - const bool group_frame = IsCemiGroupFrame(cemi, cemi_len); + + // Forward ALL tunnel cEMI frames to the TP bus first, so that bus-check, + // property-read and other KNX bus frames reach the physical TP-UART + // (NCN5120) and real KNX devices on the bus can reply. The TP-UART + // response/confirmation will come back through pollTpUart() and be + // forwarded to ETS via sendTunnelIndication(). + forwardCemiToTp(cemi, cemi_len); + const bool consumed_by_openknx = handleOpenKnxTunnelFrame(cemi, cemi_len, client); if (consumed_by_openknx) { - if (group_frame) { - forwardCemiToTp(cemi, cemi_len); - } + // OpenKNX (CemiServer / Bau07B0) handled the frame (ETS memory-model + // queries, function-property commands, etc.). It may have emitted + // a response already via EmitTunnelFrame. return; } if (shouldRouteDaliApplicationFrames()) { @@ -2992,7 +3043,7 @@ void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* body, size_t l ESP_LOGD(kTag, "KNX tunnel frame not routed to DALI: %s", result.error.c_str()); } } - forwardCemiToTp(cemi, cemi_len); + // Frame was already forwarded to TP-UART above; no need to forward again. } void GatewayKnxTpIpRouter::handleDeviceConfigurationRequest(const uint8_t* body, size_t len, @@ -3255,6 +3306,28 @@ std::optional> GatewayKnxTpIpRouter::localHpaiForRemote( return hpai; } +std::vector GatewayKnxTpIpRouter::buildOpenKnxSearchResponse( + const sockaddr_in& remote) const { + // Use OpenKNX's proven DIB construction via KnxIpSearchResponse. + // Requires ets_device_ to be initialized (DeviceObject + Platform). + if (ets_device_ == nullptr || knx_ip_parameters_ == nullptr) { + ESP_LOGW(kTag, "OpenKNX search response unavailable; falling back to hand-rolled DIBs"); + return {}; + } + KnxIpSearchResponse response(*knx_ip_parameters_, ets_device_->deviceObject()); + return std::vector(response.data(), response.data() + response.totalLength()); +} + +std::vector GatewayKnxTpIpRouter::buildOpenKnxDescriptionResponse( + const sockaddr_in& remote) const { + if (ets_device_ == nullptr || knx_ip_parameters_ == nullptr) { + ESP_LOGW(kTag, "OpenKNX description response unavailable; falling back to hand-rolled DIBs"); + return {}; + } + KnxIpDescriptionResponse response(*knx_ip_parameters_, ets_device_->deviceObject()); + return std::vector(response.data(), response.data() + response.totalLength()); +} + std::vector GatewayKnxTpIpRouter::buildDeviceInfoDib( const sockaddr_in& remote) const { std::vector dib(54, 0); @@ -3380,88 +3453,6 @@ std::vector GatewayKnxTpIpRouter::buildSupportedServiceDib() const { return dib; } -void GatewayKnxTpIpRouter::sendSearchResponse(uint16_t service, const sockaddr_in& remote, - const std::set& requested_dibs) { - if (udp_sock_ < 0 && active_tcp_sock_ < 0) { - return; - } - const auto hpai = localHpaiForRemote(remote, currentTransportAllowsTcpHpai()); - if (!hpai.has_value()) { - ESP_LOGW(kTag, "cannot send KNXnet/IP search response to %s: no active IPv4 interface", - EndpointString(remote).c_str()); - return; - } - std::vector body; - body.insert(body.end(), hpai->begin(), hpai->end()); - std::set appended_dibs; - auto append_dib = [&body, &appended_dibs](const std::vector& dib) { - if (dib.size() < 2 || !appended_dibs.insert(dib[1]).second) { - return; - } - body.insert(body.end(), dib.begin(), dib.end()); - }; - append_dib(buildDeviceInfoDib(remote)); - append_dib(buildSupportedServiceDib()); - if (service == kServiceSearchResponseExt) { - append_dib(buildExtendedDeviceInfoDib()); - for (const uint8_t dib_type : requested_dibs) { - switch (dib_type) { - case kKnxDibDeviceInfo: - append_dib(buildDeviceInfoDib(remote)); - break; - case kKnxDibSupportedServices: - append_dib(buildSupportedServiceDib()); - break; - case kKnxDibIpConfig: - append_dib(buildIpConfigDib(remote, false)); - break; - case kKnxDibCurrentIpConfig: - append_dib(buildIpConfigDib(remote, true)); - break; - case kKnxDibKnxAddresses: - append_dib(buildKnxAddressesDib()); - break; - case kKnxDibTunnellingInfo: - if (config_.tunnel_enabled) { - append_dib(buildTunnelingInfoDib()); - } - break; - case kKnxDibExtendedDeviceInfo: - append_dib(buildExtendedDeviceInfoDib()); - break; - default: - break; - } - } - } - const auto packet = KnxNetIpPacket(service, body); - sendPacket(packet, remote); - ESP_LOGI(kTag, "sent KNXnet/IP search response namespace=%s mainGroup=%u to %s:%u endpoint=%u.%u.%u.%u:%u", - openknx_namespace_.c_str(), static_cast(config_.main_group), - Ipv4String(remote.sin_addr.s_addr).c_str(), static_cast(ntohs(remote.sin_port)), - static_cast((*hpai)[2]), static_cast((*hpai)[3]), - static_cast((*hpai)[4]), static_cast((*hpai)[5]), - static_cast(config_.udp_port)); -} - -void GatewayKnxTpIpRouter::sendDescriptionResponse(const sockaddr_in& remote) { - if (udp_sock_ < 0 && active_tcp_sock_ < 0) { - return; - } - auto device = buildDeviceInfoDib(remote); - auto services = buildSupportedServiceDib(); - std::vector body; - body.reserve(device.size() + services.size()); - body.insert(body.end(), device.begin(), device.end()); - body.insert(body.end(), services.begin(), services.end()); - const auto packet = KnxNetIpPacket(kServiceDescriptionResponse, body); - sendPacket(packet, remote); - ESP_LOGI(kTag, "sent KNXnet/IP description response namespace=%s medium=0x%02x tpOnline=%d to %s:%u", - openknx_namespace_.c_str(), static_cast(advertisedMedium()), - tp_uart_online_, Ipv4String(remote.sin_addr.s_addr).c_str(), - static_cast(ntohs(remote.sin_port))); -} - void GatewayKnxTpIpRouter::sendTunnelIndication(const uint8_t* data, size_t len) { if (data == nullptr || len == 0) { return; @@ -3769,34 +3760,46 @@ void GatewayKnxTpIpRouter::handleTpUartControlByte(uint8_t byte) { return; } if (byte == kTpUartBusy) { - last_error_ = "KNX TP-UART bus busy"; - ESP_LOGW(kTag, "%s", last_error_.c_str()); + ESP_LOGW(kTag, "KNX TP-UART bus busy"); return; } - if (byte == kTpUartLDataConfirmNegative) { - last_error_ = "KNX TP-UART negative confirmation"; - ESP_LOGW(kTag, "%s", last_error_.c_str()); - return; - } - if (byte == kTpUartLDataConfirmPositive) { + // L_Data.con: use masked comparison consistent with IsTpUartControlByte + if ((byte & 0x7fU) == kTpUartLDataConfirmNegative) { + const bool positive = (byte & 0x80U) != 0; + if (!positive) { + ESP_LOGD(kTag, "KNX TP-UART negative confirmation 0x%02x", byte); + } return; } if ((byte & kTpUartStateIndicationMask) == kTpUartStateIndicationMask) { tp_uart_online_ = true; } + // L_ACKN_IND: acknowledge indication (busy / nack status embedded in byte) + if ((byte & 0x33U) == 0x00U) { + return; + } } void GatewayKnxTpIpRouter::handleTpTelegram(const uint8_t* data, size_t len) { if (data == nullptr || len == 0) { return; } - const std::vector telegram(data, data + len); + // In non-monitor mode the TP-UART prepends L_DATA_STANDARD_IND (0x90) or + // L_DATA_EXTENDED_IND (0x10) indicator bytes. Strip them so downstream + // helpers always receive a raw KNX TP telegram starting with the control byte. + const uint8_t* frame_data = data; + size_t frame_len = len; + if (len > 1U && ((data[0] & 0xD3U) == 0x90U || (data[0] & 0xD3U) == 0x10U)) { + frame_data = data + 1; + frame_len = len - 1U; + } + const std::vector telegram(frame_data, frame_data + frame_len); if (!tp_last_sent_telegram_.empty() && TpTelegramEqualsIgnoringRepeatBit(telegram, tp_last_sent_telegram_)) { tp_last_sent_telegram_.clear(); return; } - const auto cemi = TpTelegramToCemi(data, len); + const auto cemi = TpTelegramToCemi(frame_data, frame_len); if (!cemi.has_value()) { return; }