From e79223c87e860a468d6ad9ed1380532f588bfdce Mon Sep 17 00:00:00 2001 From: Tony Date: Sat, 16 May 2026 01:51:01 +0800 Subject: [PATCH] Refactor KNX DALI Gateway Configuration and Update Device Parameters - Introduced configuration macros for OEM manufacturer ID, application number, and application version in knxprod.h. - Updated product identity definitions to use the new configuration macros. - Modified device type enumeration to include additional device types. - Adjusted color space enumeration values for consistency. - Defined generated group object layout constants for memory offsets and block sizes. - Enhanced knx_dali_gw.cpp to utilize the new configuration macros for manufacturer ID and program version. - Updated the device initialization logic to reflect the new hardware and program version structures. - Removed obsolete knx_dali_gw subproject and updated related submodules. Signed-off-by: Tony --- .gitmodules | 4 - apps/gateway/main/Kconfig.projbuild | 41 + apps/gateway/sdkconfig | 4 + apps/gateway/sdkconfig.old | 12 +- .../gateway_bridge/include/gateway_bridge.hpp | 1 - .../gateway_bridge/src/gateway_bridge.cpp | 36 +- .../gateway_bridge/src/security_storage.cpp | 28 +- .../gateway_knx/include/ets_device_runtime.h | 13 +- .../gateway_knx/include/gateway_knx.hpp | 40 +- .../include/gateway_knx_internal.h | 43 +- .../gateway_knx/src/ets_device_runtime.cpp | 139 ++- .../gateway_knx/src/ets_memory_loader.cpp | 16 +- components/gateway_knx/src/gateway_knx.cpp | 1007 ++++++----------- components/knx_dali_gw/include/knxprod.h | 47 +- components/knx_dali_gw/src/knx_dali_gw.cpp | 43 +- knx | 2 +- knx_dali_gw | 1 - tpuart | 2 +- 18 files changed, 699 insertions(+), 780 deletions(-) delete mode 160000 knx_dali_gw diff --git a/.gitmodules b/.gitmodules index 0d0a77e..2869a08 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,10 +5,6 @@ path = knx url = https://git.tonycloud.org/knx/knx.git branch = v1 -[submodule "knx_dali_gw"] - path = knx_dali_gw - url = https://git.tonycloud.org/knx/GW-REG1-Dali.git - branch = tonycloud-dev [submodule "tpuart"] path = tpuart url = https://git.tonycloud.org/knx/tpuart.git diff --git a/apps/gateway/main/Kconfig.projbuild b/apps/gateway/main/Kconfig.projbuild index e470564..eb260f7 100644 --- a/apps/gateway/main/Kconfig.projbuild +++ b/apps/gateway/main/Kconfig.projbuild @@ -663,6 +663,47 @@ config GATEWAY_KNX_SECURITY_PLAIN_NVS during bring-up, but production builds should replace it with encrypted NVS, flash encryption, and secure boot before exposing real keys. +config GATEWAY_KNX_OEM_MANUFACTURER_ID + hex "KNX OEM manufacturer ID" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED + range 0x0000 0xffff + default 0x00A4 + help + Manufacturer ID advertised by the ETS-programmable KNX-DALI gateway + application. This value must match the manufacturer ID used by the + Kaenx Creator generated KNX product database. + +config GATEWAY_KNX_OEM_HARDWARE_ID + hex "KNX OEM hardware ID" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED + range 0x0000 0xffff + default 0xA401 + help + Hardware ID encoded into the OpenKNX Device Object hardware type as + 0000HHHHVV00. This must match the hardware identifier from the KNX + product database, for example Hardware Id or SerialNumber 0xA401 in + the generated Hardware.xml. + +config GATEWAY_KNX_OEM_APPLICATION_NUMBER + hex "KNX OEM application number" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED + range 0x0000 0xffff + default 0x0001 + help + Application number advertised by the ETS-programmable KNX-DALI gateway + application. Keep this in sync with MAIN_ApplicationNumber from the + generated knxprod.h. + +config GATEWAY_KNX_OEM_APPLICATION_VERSION + hex "KNX OEM application version" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED + range 0x00 0xff + default 0x08 + help + Application version advertised by the ETS-programmable KNX-DALI gateway + application. Keep this in sync with MAIN_ApplicationVersion from the + generated knxprod.h. + config GATEWAY_KNX_MAIN_GROUP int "KNX DALI main group" depends on GATEWAY_KNX_BRIDGE_SUPPORTED diff --git a/apps/gateway/sdkconfig b/apps/gateway/sdkconfig index 5aa8f24..36466c6 100644 --- a/apps/gateway/sdkconfig +++ b/apps/gateway/sdkconfig @@ -676,6 +676,10 @@ CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED=y # CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED is not set # CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS is not set CONFIG_GATEWAY_KNX_SECURITY_PLAIN_NVS=y +CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID=0x01e5 +CONFIG_GATEWAY_KNX_OEM_HARDWARE_ID=0xa401 +CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER=0x0001 +CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION=0x08 CONFIG_GATEWAY_KNX_MAIN_GROUP=0 CONFIG_GATEWAY_KNX_TUNNEL_ENABLED=y CONFIG_GATEWAY_KNX_MULTICAST_ENABLED=y diff --git a/apps/gateway/sdkconfig.old b/apps/gateway/sdkconfig.old index 3ff6cc8..9491e43 100644 --- a/apps/gateway/sdkconfig.old +++ b/apps/gateway/sdkconfig.old @@ -622,11 +622,8 @@ CONFIG_GATEWAY_CHANNEL1_NATIVE_BAUDRATE=1200 # CONFIG_GATEWAY_CACHE_SUPPORTED=y CONFIG_GATEWAY_CACHE_START_ENABLED=y -CONFIG_GATEWAY_CACHE_RECONCILIATION_ENABLED=y -# CONFIG_GATEWAY_CACHE_FULL_STATE_MIRROR is not set +# CONFIG_GATEWAY_CACHE_RECONCILIATION_ENABLED is not set CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS=60000 -CONFIG_GATEWAY_CACHE_OUTSIDE_BUS_FIRST=y -# CONFIG_GATEWAY_CACHE_LOCAL_GATEWAY_FIRST is not set # end of Gateway Cache # CONFIG_GATEWAY_ENABLE_DALI_BUS is not set @@ -679,6 +676,9 @@ CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED=y # CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED is not set # CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS is not set CONFIG_GATEWAY_KNX_SECURITY_PLAIN_NVS=y +CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID=0x00fa +CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER=0xa401 +CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION=0x08 CONFIG_GATEWAY_KNX_MAIN_GROUP=0 CONFIG_GATEWAY_KNX_TUNNEL_ENABLED=y CONFIG_GATEWAY_KNX_MULTICAST_ENABLED=y @@ -1133,8 +1133,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_bridge/include/gateway_bridge.hpp b/components/gateway_bridge/include/gateway_bridge.hpp index 1008b20..73cc420 100644 --- a/components/gateway_bridge/include/gateway_bridge.hpp +++ b/components/gateway_bridge/include/gateway_bridge.hpp @@ -72,7 +72,6 @@ class GatewayBridgeService { esp_err_t startKnxEndpoint(ChannelRuntime* requested_runtime, std::set* used_uarts = nullptr); esp_err_t stopKnxEndpoint(ChannelRuntime* requested_runtime); - DaliBridgeResult routeKnxCemiFrame(const uint8_t* data, size_t len); DaliBridgeResult routeKnxGroupWrite(uint16_t group_address, const uint8_t* data, size_t len); void handleDaliRawFrame(const DaliRawFrame& frame); diff --git a/components/gateway_bridge/src/gateway_bridge.cpp b/components/gateway_bridge/src/gateway_bridge.cpp index 34e6579..6156532 100644 --- a/components/gateway_bridge/src/gateway_bridge.cpp +++ b/components/gateway_bridge/src/gateway_bridge.cpp @@ -1433,11 +1433,7 @@ struct GatewayBridgeService::ChannelRuntime { } knx = std::make_unique(*engine); - knx_router = std::make_unique( - *knx, [this](const uint8_t* data, size_t len) { - return service.routeKnxCemiFrame(data, len); - }, - openKnxNamespace()); + knx_router = std::make_unique(*knx, openKnxNamespace()); knx_router->setGroupWriteHandler( [this](uint16_t group_address, const uint8_t* data, size_t len) { return service.routeKnxGroupWrite(group_address, data, len); @@ -4028,36 +4024,6 @@ esp_err_t GatewayBridgeService::stopKnxEndpoint(ChannelRuntime* requested_runtim return owner->stopKnx(); } -DaliBridgeResult GatewayBridgeService::routeKnxCemiFrame(const uint8_t* data, size_t len) { - std::vector matches; - for (const auto& runtime : runtimes_) { - LockGuard guard(runtime->lock); - if (runtime->knx != nullptr && runtime->knx->matchesCemiFrame(data, len)) { - matches.push_back(runtime.get()); - } - } - if (matches.empty()) { - DaliBridgeResult result; - result.error = "No DALI bridge mapping matched KNX cEMI group write"; - return result; - } - if (matches.size() > 1) { - DaliBridgeResult result; - result.error = "KNX cEMI group write matched multiple DALI bridge channels"; - ESP_LOGW(kTag, "%s", result.error.c_str()); - return result; - } - - ChannelRuntime* runtime = matches.front(); - LockGuard guard(runtime->lock); - if (runtime->knx == nullptr || !runtime->knx->matchesCemiFrame(data, len)) { - DaliBridgeResult result; - result.error = "DALI bridge mapping changed before KNX cEMI dispatch"; - return result; - } - return runtime->knx->handleCemiFrame(data, len); -} - DaliBridgeResult GatewayBridgeService::routeKnxGroupWrite(uint16_t group_address, const uint8_t* data, size_t len) { std::vector matches; diff --git a/components/gateway_bridge/src/security_storage.cpp b/components/gateway_bridge/src/security_storage.cpp index 4eb40f5..90e18d2 100644 --- a/components/gateway_bridge/src/security_storage.cpp +++ b/components/gateway_bridge/src/security_storage.cpp @@ -1,5 +1,7 @@ #include "security_storage.h" +#include "gateway_knx_internal.h" + #include "esp_log.h" #include "esp_mac.h" #include "esp_timer.h" @@ -9,7 +11,9 @@ #include #include +#include #include +#include #include #include #include @@ -22,11 +26,7 @@ constexpr const char* kFactoryFdskKey = "factory_fdsk"; constexpr size_t kFdskSize = 16; constexpr size_t kSerialSize = 6; constexpr size_t kFdskQrSize = 36; -constexpr uint16_t kKnxManufacturerId = 0x00A4; constexpr const char* kProductIdentity = "REG1-Dali"; -constexpr const char* kManufacturerId = "00A4"; -constexpr const char* kApplicationNumber = "01"; -constexpr const char* kApplicationVersion = "05"; constexpr const char* kDevelopmentStorage = "base_mac_derived_plain_nvs_development"; constexpr char kFdskDerivationLabel[] = "DaliMaster REG1-Dali deterministic FDSK v1"; constexpr uint8_t kCrc4Tab[16] = { @@ -38,6 +38,12 @@ constexpr char kHexAlphabet[] = "0123456789ABCDEF"; extern "C" void knx_platform_clear_cached_fdsk() __attribute__((weak)); +std::string hexValue(uint32_t value, int width) { + std::array buffer{}; + std::snprintf(buffer.data(), buffer.size(), "%0*" PRIX32, width, value); + return buffer.data(); +} + bool ensureNvsReady() { const esp_err_t err = nvs_flash_init(); if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { @@ -123,8 +129,10 @@ bool loadKnxSerialNumber(uint8_t* serial) { return false; } - serial[0] = static_cast((kKnxManufacturerId >> 8) & 0xff); - serial[1] = static_cast(kKnxManufacturerId & 0xff); + serial[0] = static_cast( + (gateway::knx_internal::kReg1DaliManufacturerId >> 8) & 0xff); + serial[1] = static_cast( + gateway::knx_internal::kReg1DaliManufacturerId & 0xff); std::copy(mac.begin() + 2, mac.end(), serial + 2); return true; } @@ -352,9 +360,11 @@ FactoryCertificatePayload BuildFactoryCertificatePayload() { } payload.available = true; payload.productIdentity = kProductIdentity; - payload.manufacturerId = kManufacturerId; - payload.applicationNumber = kApplicationNumber; - payload.applicationVersion = kApplicationVersion; + payload.manufacturerId = hexValue(gateway::knx_internal::kReg1DaliManufacturerId, 4); + payload.applicationNumber = hexValue( + gateway::knx_internal::kReg1DaliApplicationNumber, 2); + payload.applicationVersion = hexValue( + gateway::knx_internal::kReg1DaliApplicationVersion, 2); payload.serialNumber = info.serialNumber; payload.fdskLabel = info.label; payload.fdskQrCode = info.qrCode; diff --git a/components/gateway_knx/include/ets_device_runtime.h b/components/gateway_knx/include/ets_device_runtime.h index acb4f15..06e7769 100644 --- a/components/gateway_knx/include/ets_device_runtime.h +++ b/components/gateway_knx/include/ets_device_runtime.h @@ -9,11 +9,14 @@ #include #include #include +#include #include #include namespace gateway::openknx { +class TpuartUartInterface; + class EtsDeviceRuntime { public: using CemiFrameSender = std::function; @@ -25,7 +28,8 @@ class EtsDeviceRuntime { EtsDeviceRuntime(std::string nvs_namespace, uint16_t fallback_individual_address, - uint16_t tunnel_client_address = 0); + uint16_t tunnel_client_address = 0, + std::unique_ptr tp_uart_interface = nullptr); ~EtsDeviceRuntime(); uint16_t individualAddress() const; @@ -43,7 +47,12 @@ class EtsDeviceRuntime { void setFunctionPropertyHandlers(FunctionPropertyHandler command_handler, FunctionPropertyHandler state_handler); void setGroupWriteHandler(GroupWriteHandler handler); + void setBusFrameSender(CemiFrameSender sender); void setNetworkInterface(esp_netif_t* netif); + bool hasTpUart() const; + bool enableTpUart(bool enabled = true); + bool tpUartOnline() const; + bool transmitTpFrame(const uint8_t* data, size_t len); bool handleTunnelFrame(const uint8_t* data, size_t len, CemiFrameSender sender); bool handleBusFrame(const uint8_t* data, size_t len); @@ -70,9 +79,11 @@ class EtsDeviceRuntime { bool shouldConsumeBusFrame(CemiFrame& frame) const; std::string nvs_namespace_; + std::unique_ptr tp_uart_interface_; EspIdfPlatform platform_; Bau07B0 device_; CemiFrameSender sender_; + CemiFrameSender bus_frame_sender_; GroupWriteHandler group_write_handler_; FunctionPropertyHandler command_handler_; FunctionPropertyHandler state_handler_; diff --git a/components/gateway_knx/include/gateway_knx.hpp b/components/gateway_knx/include/gateway_knx.hpp index 7c11b73..85da596 100644 --- a/components/gateway_knx/include/gateway_knx.hpp +++ b/components/gateway_knx/include/gateway_knx.hpp @@ -27,6 +27,7 @@ namespace gateway { namespace openknx { class EtsDeviceRuntime; +class TpuartUartInterface; } constexpr uint16_t kGatewayKnxDefaultUdpPort = 3671; @@ -143,9 +144,7 @@ class GatewayKnxBridge { size_t etsBindingCount() const; std::vector describeDaliBindings() const; - bool matchesCemiFrame(const uint8_t* data, size_t len) const; bool matchesGroupAddress(uint16_t group_address) const; - DaliBridgeResult handleCemiFrame(const uint8_t* data, size_t len); DaliBridgeResult handleGroupWrite(uint16_t group_address, const uint8_t* data, size_t len); bool handleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id, @@ -198,12 +197,11 @@ class GatewayKnxBridge { class GatewayKnxTpIpRouter { public: - using CemiFrameHandler = std::function; using GroupWriteHandler = std::function; - GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHandler handler, + GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, std::string openknx_namespace = "openknx"); ~GatewayKnxTpIpRouter(); @@ -256,22 +254,23 @@ class GatewayKnxTpIpRouter { void handleTcpAccept(); void handleTcpClient(TcpClient& client); void closeTcpClient(TcpClient& client); + std::unique_ptr createOpenKnxTpUartInterface(); bool configureTpUart(); - bool initializeTpUart(); bool configureProgrammingGpio(); void refreshNetworkInterfaces(bool force_log = false); void handleUdpDatagram(const uint8_t* data, size_t len, const ::sockaddr_in& remote); - void handleSearchRequest(const uint8_t* data, size_t len, const ::sockaddr_in& remote); - void handleDescriptionRequest(const uint8_t* data, size_t len, + void handleSearchRequest(uint16_t service, const uint8_t* packet_data, size_t len, + const ::sockaddr_in& remote); + void handleDescriptionRequest(const uint8_t* packet_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 handleDeviceConfigurationRequest(const uint8_t* body, size_t len, + void handleRoutingIndication(const uint8_t* packet_data, size_t len); + void handleTunnellingRequest(const uint8_t* packet_data, size_t len, const ::sockaddr_in& remote); + void handleDeviceConfigurationRequest(const uint8_t* packet_data, 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, + void handleConnectRequest(const uint8_t* packet_data, size_t len, const ::sockaddr_in& remote); + void handleConnectionStateRequest(const uint8_t* packet_data, size_t len, const ::sockaddr_in& remote); - void handleDisconnectRequest(const uint8_t* body, size_t len, const ::sockaddr_in& remote); + void handleDisconnectRequest(const uint8_t* packet_data, size_t len, const ::sockaddr_in& remote); void handleSecureService(uint16_t service, const uint8_t* body, size_t len, const ::sockaddr_in& remote); void sendTunnellingAck(uint8_t channel_id, uint8_t sequence, uint8_t status, @@ -283,6 +282,8 @@ 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, + 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, @@ -319,9 +320,11 @@ class GatewayKnxTpIpRouter { uint16_t effectiveTunnelAddressForSlot(size_t slot) const; void pruneStaleTunnelClients(); bool handleOpenKnxTunnelFrame(const uint8_t* data, size_t len, - TunnelClient* response_client); + TunnelClient* response_client, uint16_t response_service); bool handleOpenKnxBusFrame(const uint8_t* data, size_t len); + bool transmitOpenKnxTpFrame(const uint8_t* data, size_t len); void selectOpenKnxNetworkInterface(const ::sockaddr_in& remote); + bool routeOpenKnxGroupWrite(const uint8_t* data, size_t len, const char* context); bool emitOpenKnxGroupValue(uint16_t group_object_number, const uint8_t* data, size_t len); bool shouldRouteDaliApplicationFrames() const; uint8_t advertisedMedium() const; @@ -329,16 +332,11 @@ class GatewayKnxTpIpRouter { uint16_t effectiveIpInterfaceIndividualAddress() const; uint16_t effectiveKnxDeviceIndividualAddress() const; uint16_t effectiveTunnelAddress() const; - void pollTpUart(); void pollProgrammingButton(); void updateProgrammingLed(); void setProgrammingLed(bool on); - void handleTpUartControlByte(uint8_t byte); - void handleTpTelegram(const uint8_t* data, size_t len); - void forwardCemiToTp(const uint8_t* data, size_t len); GatewayKnxBridge& bridge_; - CemiFrameHandler handler_; GroupWriteHandler group_write_handler_; std::string openknx_namespace_; GatewayKnxConfig config_; @@ -361,10 +359,6 @@ class GatewayKnxTpIpRouter { 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_; - TickType_t tp_uart_last_byte_tick_{0}; - bool tp_uart_extended_frame_{false}; bool tp_uart_online_{false}; bool commissioning_only_{false}; std::atomic_bool openknx_configured_{false}; diff --git a/components/gateway_knx/include/gateway_knx_internal.h b/components/gateway_knx/include/gateway_knx_internal.h index c8ce3ff..2469f19 100644 --- a/components/gateway_knx/include/gateway_knx_internal.h +++ b/components/gateway_knx/include/gateway_knx_internal.h @@ -1,10 +1,11 @@ #pragma once -// Internal header shared between gateway_knx.cpp and gateway_knx_router.cpp. +// Internal helpers and product identity shared by gateway_knx component sources. #include "driver/uart.h" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" +#include "sdkconfig.h" #include "soc/uart_periph.h" #include @@ -15,6 +16,46 @@ namespace knx_internal { constexpr const char* kTag = "gateway_knx"; +#ifndef CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID +#define CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID 0x00A4 +#endif + +#ifndef CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER +#define CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER 0x0001 +#endif + +#ifndef CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION +#define CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION 0x08 +#endif + +#ifndef CONFIG_GATEWAY_KNX_OEM_HARDWARE_ID +#define CONFIG_GATEWAY_KNX_OEM_HARDWARE_ID 0xA401 +#endif + +inline constexpr uint16_t kReg1DaliManufacturerId = + static_cast(CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID); +inline constexpr uint16_t kReg1DaliHardwareId = + static_cast(CONFIG_GATEWAY_KNX_OEM_HARDWARE_ID); +inline constexpr uint16_t kReg1DaliApplicationNumber = + static_cast(CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER); +inline constexpr uint8_t kReg1DaliApplicationVersion = + static_cast(CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION); +inline constexpr uint8_t kReg1DaliHardwareType[6] = { + 0x00, + 0x00, + static_cast((kReg1DaliHardwareId >> 8) & 0xff), + static_cast(kReg1DaliHardwareId & 0xff), + kReg1DaliApplicationVersion, + 0x00}; +inline constexpr uint8_t kReg1DaliOrderNumber[10] = { + 'R', 'E', 'G', '1', '-', 'D', 'a', 'l', 'i', 0}; +inline constexpr uint8_t kReg1DaliProgramVersion[5] = { + static_cast((kReg1DaliManufacturerId >> 8) & 0xff), + static_cast(kReg1DaliManufacturerId & 0xff), + static_cast((kReg1DaliApplicationNumber >> 8) & 0xff), + static_cast(kReg1DaliApplicationNumber & 0xff), + kReg1DaliApplicationVersion}; + // RAII semaphore guard. class SemaphoreGuard { public: diff --git a/components/gateway_knx/src/ets_device_runtime.cpp b/components/gateway_knx/src/ets_device_runtime.cpp index 33f468c..5b9b6df 100644 --- a/components/gateway_knx/src/ets_device_runtime.cpp +++ b/components/gateway_knx/src/ets_device_runtime.cpp @@ -1,11 +1,17 @@ #include "ets_device_runtime.h" +#include "gateway_knx_internal.h" + +#include "esp_log.h" #include "knx/cemi_server.h" #include "knx/secure_application_layer.h" #include "knx/property.h" +#include "tpuart_uart_interface.h" #include #include +#include +#include #include #include @@ -29,53 +35,97 @@ 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; -constexpr uint8_t kReg1DaliOrderNumber[10] = {'R', 'E', 'G', '1', '-', 'D', 'a', 'l', 'i', 0}; bool IsUsableIndividualAddress(uint16_t address) { return address != 0 && address != kInvalidIndividualAddress; } -bool IsErasedMemory(const uint8_t* data, size_t size) { - if (data == nullptr || size == 0) { - return true; +std::string HexBytesString(const uint8_t* data, size_t length) { + if (data == nullptr || length == 0) { + return {}; } - return std::all_of(data, data + size, [](uint8_t value) { return value == 0xff; }); + + std::string out; + out.reserve(length * 3); + char buffer[4] = {0}; + for (size_t index = 0; index < length; ++index) { + std::snprintf(buffer, sizeof(buffer), "%02X", data[index]); + out += buffer; + if (index + 1 < length) { + out.push_back(' '); + } + } + return out; +} + +std::string PrintableOrderNumber(const uint8_t* data, size_t length) { + if (data == nullptr || length == 0) { + return {}; + } + + std::string out; + out.reserve(length); + for (size_t index = 0; index < length; ++index) { + if (data[index] == 0) { + break; + } + out.push_back(static_cast(data[index])); + } + return out; +} + +void LogReg1DaliIdentity(const std::string& nvs_namespace, Bau07B0& device) { + uint8_t program_version[5] = {0}; + if (auto* property = device.parameters().property(PID_PROG_VERSION); property != nullptr) { + property->read(program_version); + } + + const std::string hardware_type = + HexBytesString(device.deviceObject().hardwareType(), LEN_HARDWARE_TYPE); + const std::string program_version_hex = + HexBytesString(program_version, sizeof(program_version)); + const std::string order_number = + PrintableOrderNumber(device.deviceObject().orderNumber(), + sizeof(knx_internal::kReg1DaliOrderNumber)); + + ESP_LOGI("gateway_knx", + "OpenKNX identity namespace=%s manufacturer=0x%04x mask=0x%04x deviceVersion=0x%04x hardwareType=%s progVersion=%s order=%s", + nvs_namespace.c_str(), device.deviceObject().manufacturerId(), + device.deviceObject().maskVersion(), device.deviceObject().version(), + hardware_type.c_str(), program_version_hex.c_str(), order_number.c_str()); } void ApplyReg1DaliIdentity(Bau07B0& device, EspIdfPlatform& platform) { - device.deviceObject().manufacturerId(kReg1DaliManufacturerId); + device.deviceObject().manufacturerId(knx_internal::kReg1DaliManufacturerId); device.deviceObject().bauNumber(platform.uniqueSerialNumber()); - device.deviceObject().orderNumber(kReg1DaliOrderNumber); - const uint8_t program_version[5] = {0x00, 0xa4, 0x00, kReg1DaliApplicationNumber, - kReg1DaliApplicationVersion}; - device.parameters().property(PID_PROG_VERSION)->write(program_version); + device.deviceObject().hardwareType(knx_internal::kReg1DaliHardwareType); + device.deviceObject().orderNumber(knx_internal::kReg1DaliOrderNumber); + device.parameters().property(PID_PROG_VERSION)->write( + knx_internal::kReg1DaliProgramVersion); } } // namespace EtsDeviceRuntime::EtsDeviceRuntime(std::string nvs_namespace, uint16_t fallback_individual_address, - uint16_t tunnel_client_address) + uint16_t tunnel_client_address, + std::unique_ptr tp_uart_interface) : nvs_namespace_(std::move(nvs_namespace)), - platform_(nullptr, nvs_namespace_.c_str()), + tp_uart_interface_(std::move(tp_uart_interface)), + platform_(tp_uart_interface_.get(), nvs_namespace_.c_str()), device_(platform_) { platform_.outboundCemiFrameCallback(&EtsDeviceRuntime::HandleOutboundCemiFrame, this); ApplyReg1DaliIdentity(device_, platform_); if (IsUsableIndividualAddress(fallback_individual_address)) { device_.deviceObject().individualAddress(fallback_individual_address); } - const uint8_t* memory = platform_.getNonVolatileMemoryStart(); - const size_t memory_size = platform_.getNonVolatileMemorySize(); - if (!IsErasedMemory(memory, memory_size)) { - device_.readMemory(); - } + ESP_LOGI("gateway_knx", "OpenKNX loading memory namespace=%s", nvs_namespace_.c_str()); + device_.readMemory(); if (!IsUsableIndividualAddress(device_.deviceObject().individualAddress()) && IsUsableIndividualAddress(fallback_individual_address)) { device_.deviceObject().individualAddress(fallback_individual_address); } + LogReg1DaliIdentity(nvs_namespace_, device_); if (auto* server = device_.getCemiServer()) { server->clientAddress(IsUsableIndividualAddress(tunnel_client_address) ? tunnel_client_address @@ -92,6 +142,9 @@ EtsDeviceRuntime::EtsDeviceRuntime(std::string nvs_namespace, } EtsDeviceRuntime::~EtsDeviceRuntime() { + if (tp_uart_interface_ != nullptr) { + device_.enabled(false); + } platform_.outboundCemiFrameCallback(nullptr, nullptr); #ifdef USE_DATASECURE device_.secureGroupWriteCallback(nullptr, nullptr); @@ -170,10 +223,43 @@ void EtsDeviceRuntime::setGroupWriteHandler(GroupWriteHandler handler) { group_write_handler_ = std::move(handler); } +void EtsDeviceRuntime::setBusFrameSender(CemiFrameSender sender) { + bus_frame_sender_ = std::move(sender); +} + void EtsDeviceRuntime::setNetworkInterface(esp_netif_t* netif) { platform_.networkInterface(netif); } +bool EtsDeviceRuntime::hasTpUart() const { return tp_uart_interface_ != nullptr; } + +bool EtsDeviceRuntime::enableTpUart(bool enabled) { + if (tp_uart_interface_ == nullptr) { + return false; + } + device_.enabled(enabled); + loop(); + return !enabled || device_.enabled(); +} + +bool EtsDeviceRuntime::tpUartOnline() const { + return tp_uart_interface_ != nullptr && const_cast(device_).enabled(); +} + +bool EtsDeviceRuntime::transmitTpFrame(const uint8_t* data, size_t len) { + auto* data_link_layer = device_.getDataLinkLayer(); + if (tp_uart_interface_ == nullptr || data_link_layer == nullptr || data == nullptr || len < 2 || + !data_link_layer->enabled()) { + return false; + } + std::vector frame_data(data, data + len); + CemiFrame frame(frame_data.data(), static_cast(frame_data.size())); + if (!frame.valid()) { + return false; + } + return data_link_layer->transmitFrame(frame); +} + bool EtsDeviceRuntime::handleTunnelFrame(const uint8_t* data, size_t len, CemiFrameSender sender) { auto* server = device_.getCemiServer(); @@ -248,10 +334,16 @@ bool EtsDeviceRuntime::HandleOutboundCemiFrame(CemiFrame& frame, void* context) void EtsDeviceRuntime::EmitTunnelFrame(CemiFrame& frame, void* context) { auto* self = static_cast(context); - if (self == nullptr || !self->sender_) { + if (self == nullptr) { return; } - self->sender_(frame.data(), frame.dataLength()); + if (self->sender_) { + self->sender_(frame.data(), frame.dataLength()); + return; + } + if (self->bus_frame_sender_) { + self->bus_frame_sender_(frame.data(), frame.dataLength()); + } } void EtsDeviceRuntime::HandleSecureGroupWrite(uint16_t group_address, const uint8_t* data, @@ -324,6 +416,9 @@ 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/ets_memory_loader.cpp b/components/gateway_knx/src/ets_memory_loader.cpp index d963f63..f28b678 100644 --- a/components/gateway_knx/src/ets_memory_loader.cpp +++ b/components/gateway_knx/src/ets_memory_loader.cpp @@ -1,6 +1,7 @@ #include "ets_memory_loader.h" #include "esp_idf_platform.h" +#include "gateway_knx_internal.h" #include "knx/bau07B0.h" #include "knx/property.h" @@ -28,18 +29,13 @@ bool IsErasedMemory(const uint8_t* data, size_t size) { return std::all_of(data, data + size, [](uint8_t value) { return value == 0xff; }); } -constexpr uint16_t kReg1DaliManufacturerId = 0x00a4; -constexpr uint8_t kReg1DaliApplicationNumber = 0x01; -constexpr uint8_t kReg1DaliApplicationVersion = 0x05; -constexpr uint8_t kReg1DaliOrderNumber[10] = {'R', 'E', 'G', '1', '-', 'D', 'a', 'l', 'i', 0}; - void ApplyReg1DaliIdentity(Bau07B0& device, EspIdfPlatform& platform) { - device.deviceObject().manufacturerId(kReg1DaliManufacturerId); + device.deviceObject().manufacturerId(knx_internal::kReg1DaliManufacturerId); device.deviceObject().bauNumber(platform.uniqueSerialNumber()); - device.deviceObject().orderNumber(kReg1DaliOrderNumber); - const uint8_t program_version[5] = {0x00, 0xa4, 0x00, kReg1DaliApplicationNumber, - kReg1DaliApplicationVersion}; - device.parameters().property(PID_PROG_VERSION)->write(program_version); + device.deviceObject().hardwareType(knx_internal::kReg1DaliHardwareType); + device.deviceObject().orderNumber(knx_internal::kReg1DaliOrderNumber); + device.parameters().property(PID_PROG_VERSION)->write( + knx_internal::kReg1DaliProgramVersion); } } // namespace diff --git a/components/gateway_knx/src/gateway_knx.cpp b/components/gateway_knx/src/gateway_knx.cpp index e6dcd62..4e8dced 100644 --- a/components/gateway_knx/src/gateway_knx.cpp +++ b/components/gateway_knx/src/gateway_knx.cpp @@ -9,11 +9,25 @@ #include "lwip/inet.h" #include "lwip/sockets.h" #include "ets_device_runtime.h" +#include "gateway_knx_internal.h" #include "soc/uart_periph.h" +#include "tpuart_uart_interface.h" #include "knx/cemi_frame.h" +#include "knx/knx_ip_connect_request.h" +#include "knx/knx_ip_connect_response.h" +#include "knx/knx_ip_config_request.h" +#include "knx/knx_ip_description_request.h" +#include "knx/knx_ip_routing_indication.h" +#include "knx/knx_ip_disconnect_request.h" +#include "knx/knx_ip_disconnect_response.h" +#include "knx/knx_ip_search_request.h" #include "knx/knx_ip_search_response.h" #include "knx/knx_ip_description_response.h" +#include "knx/knx_ip_state_request.h" +#include "knx/knx_ip_state_response.h" +#include "knx/knx_ip_tunneling_ack.h" +#include "knx/knx_ip_tunneling_request.h" #include #include @@ -32,9 +46,6 @@ 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 kServiceSearchRequest = 0x0201; constexpr uint16_t kServiceSearchResponse = 0x0202; constexpr uint16_t kServiceDescriptionRequest = 0x0203; @@ -86,22 +97,10 @@ constexpr uint8_t kKnxServiceFamilyCore = 0x02; constexpr uint8_t kKnxServiceFamilyDeviceManagement = 0x03; constexpr uint8_t kKnxServiceFamilyTunnelling = 0x04; constexpr uint8_t kKnxServiceFamilyRouting = 0x05; -constexpr uint16_t kKnxManufacturerId = 0x00a4; constexpr uint16_t kKnxIpOnlyDeviceDescriptor = 0x57b0; constexpr uint16_t kKnxTpIpInterfaceDeviceDescriptor = 0x091a; constexpr uint8_t kKnxIpAssignmentManual = 0x01; constexpr uint8_t kKnxIpCapabilityManual = 0x01; -constexpr uint8_t kTpUartResetRequest = 0x01; -constexpr uint8_t kTpUartResetIndication = 0x03; -constexpr uint8_t kTpUartStateRequest = 0x02; -constexpr uint8_t kTpUartStateIndicationMask = 0x07; -constexpr uint8_t kTpUartSetAddressRequest = 0x28; -constexpr uint8_t kTpUartAckInfo = 0x10; -constexpr uint8_t kTpUartLDataConfirmPositive = 0x8b; -constexpr uint8_t kTpUartLDataConfirmNegative = 0x0b; -constexpr uint8_t kTpUartLDataStart = 0x80; -constexpr uint8_t kTpUartLDataEnd = 0x40; -constexpr uint8_t kTpUartBusy = 0xc0; constexpr uint16_t kGwReg1AdrKoOffset = 12; constexpr uint16_t kGwReg1AdrKoBlockSize = 18; constexpr uint16_t kGwReg1GrpKoOffset = 1164; @@ -280,18 +279,17 @@ std::optional SelectKnxNetifForRemote(const sockaddr_in& remote) { return netifs.front(); } -sockaddr_in EndpointFromHpaiAt(const uint8_t* body, size_t len, size_t offset, - const sockaddr_in& fallback) { +sockaddr_in EndpointFromOpenKnxHpai(const IpHostProtocolAddressInformation& hpai, + const sockaddr_in& fallback) { sockaddr_in out = fallback; - if (body == nullptr || offset + 8 > len || body[offset] != 0x08 || - (body[offset + 1] != kKnxHpaiIpv4Udp && body[offset + 1] != kKnxHpaiIpv4Tcp)) { + if (hpai.length() != LEN_IPHPAI || + (hpai.code() != IPV4_UDP && hpai.code() != IPV4_TCP)) { return out; } - uint32_t address = 0; - std::memcpy(&address, body + offset + 2, sizeof(address)); - const uint16_t port = ReadBe16(body + offset + 6); + const uint32_t address = hpai.ipAddress(); + const uint16_t port = hpai.ipPortNumber(); if (address != 0) { - out.sin_addr.s_addr = address; + out.sin_addr.s_addr = htonl(address); } if (port != 0) { out.sin_port = htons(port); @@ -299,18 +297,13 @@ sockaddr_in EndpointFromHpaiAt(const uint8_t* body, size_t len, size_t offset, return out; } -sockaddr_in ResponseEndpointFromHpai(const uint8_t* body, size_t len, - const sockaddr_in& fallback) { - return EndpointFromHpaiAt(body, len, 0, fallback); -} - -bool HasUnsupportedHpaiProtocolAt(const uint8_t* body, size_t len, size_t offset, - bool allow_tcp) { - if (body == nullptr || offset + 2 > len || body[offset] != 0x08) { +bool OpenKnxHpaiUsesUnsupportedProtocol(const IpHostProtocolAddressInformation& hpai, + bool allow_tcp) { + if (hpai.length() != LEN_IPHPAI) { return false; } - const uint8_t protocol = body[offset + 1]; - return protocol != kKnxHpaiIpv4Udp && !(allow_tcp && protocol == kKnxHpaiIpv4Tcp); + const HostProtocolCode protocol = hpai.code(); + return protocol != IPV4_UDP && !(allow_tcp && protocol == IPV4_TCP); } void WriteBe16(uint8_t* data, uint16_t value) { @@ -318,32 +311,6 @@ void WriteBe16(uint8_t* data, uint16_t value) { data[1] = static_cast(value & 0xff); } -uint16_t TunnelServiceForCemi(const uint8_t* data, size_t len) { - if (data == nullptr || len == 0) { - return kServiceTunnellingRequest; - } - return (data[0] == kCemiLDataReq || data[0] == kCemiLDataCon || data[0] == kCemiLDataInd) - ? kServiceTunnellingRequest - : kServiceDeviceConfigurationRequest; -} - -std::vector CemiWithTunnelSourceAddress(const uint8_t* data, size_t len, - uint16_t source_address) { - std::vector frame(data, data + len); - if (len < 8 || frame[0] != kCemiLDataReq) { - return frame; - } - const size_t additional_info_len = frame[1]; - const size_t source_offset = 2 + additional_info_len + 2; - if (source_offset + 1 >= frame.size()) { - return frame; - } - if (ReadBe16(frame.data() + source_offset) == 0) { - WriteBe16(frame.data() + source_offset, source_address); - } - return frame; -} - std::optional ObjectIntAny(const DaliValue::Object& object, std::initializer_list keys) { for (const char* key : keys) { @@ -522,60 +489,45 @@ const char* DataTypeDpt(GatewayKnxDaliDataType data_type) { } } -std::optional DecodeCemiGroupWrite(const uint8_t* data, size_t len) { +std::optional DecodeOpenKnxGroupWrite(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) { + std::vector frame_data(data, data + len); + CemiFrame frame(frame_data.data(), static_cast(frame_data.size())); + if (!frame.valid()) { return std::nullopt; } - const size_t base = 2U + data[1]; - if (len < base + 8U) { + const MessageCode message_code = frame.messageCode(); + if (message_code != L_data_req && message_code != L_data_ind && message_code != L_data_con) { return std::nullopt; } - const uint8_t control2 = data[base + 1]; - if ((control2 & 0x80) == 0) { + if (frame.addressType() != GroupAddress) { 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) { + const TpduType tpdu_type = frame.tpdu().type(); + if (tpdu_type != DataGroup && tpdu_type != DataBroadcast) { 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) { + if (frame.apdu().type() != GroupValueWrite) { return std::nullopt; } DecodedGroupWrite out; - out.group_address = destination; - if (tpdu_len == 2U) { - out.data.push_back(tpdu[1] & 0x3f); + out.group_address = frame.destinationAddress(); + const uint8_t apdu_length = frame.apdu().length(); + const uint8_t* apdu_data = frame.apdu().data(); + if (apdu_data == nullptr || apdu_length == 0) { + return std::nullopt; + } + if (apdu_length == 1U) { + out.data.push_back(apdu_data[0] & 0x3f); } else { - out.data.assign(tpdu + 2, tpdu + tpdu_len); + out.data.assign(apdu_data + 1, apdu_data + apdu_length); } return out; } -bool IsCemiGroupFrame(const uint8_t* data, size_t len) { - if (data == nullptr || len < 10) { - return false; - } - const uint8_t message_code = data[0]; - if (message_code != kCemiLDataReq && message_code != kCemiLDataInd && - message_code != kCemiLDataCon) { - return false; - } - const size_t base = 2U + data[1]; - if (len < base + 8U) { - return false; - } - return (data[base + 1] & 0x80) != 0; -} - uint8_t Reg1PercentToArc(uint8_t value) { if (value == 0 || value == 0xff) { return value; @@ -707,16 +659,11 @@ bool SendStream(int sock, const uint8_t* data, size_t len) { return true; } -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::vector OpenKnxIpPacket(uint16_t service, const std::vector& body) { + KnxIpFrame frame(static_cast(LEN_KNXIP_HEADER + body.size())); + frame.serviceTypeIdentifier(service); + std::copy(body.begin(), body.end(), frame.data() + LEN_KNXIP_HEADER); + return std::vector(frame.data(), frame.data() + frame.totalLength()); } bool ParseKnxNetIpHeader(const uint8_t* data, size_t len, uint16_t* service, @@ -744,168 +691,6 @@ bool IsKnxNetIpSecureService(uint16_t service) { } } -bool IsExtendedTpFrame(const uint8_t* data, size_t len) { - 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)) { - const size_t base = has_indicator ? 9U : 8U; - return base + data[6U + off]; - } - const size_t base = has_indicator ? 8U : 7U; - return base + (data[5U + off] & 0x0FU); -} - -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; -} - -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 & 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; - } - // 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) { - std::vector wrapped; - wrapped.reserve(telegram.size() * 2U); - for (size_t index = 0; index < telegram.size(); ++index) { - const uint8_t control = static_cast( - (index + 1U == telegram.size() ? kTpUartLDataEnd : kTpUartLDataStart) | - (index & 0x3fU)); - wrapped.push_back(control); - wrapped.push_back(telegram[index]); - } - return wrapped; -} - -bool TpTelegramEqualsIgnoringRepeatBit(const std::vector& left, - const std::vector& right) { - if (left.size() != right.size() || left.empty()) { - return false; - } - if ((left[0] & static_cast(~0x20U)) != - (right[0] & static_cast(~0x20U))) { - return false; - } - return std::equal(left.begin() + 1, left.end(), right.begin() + 1); -} - -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) { @@ -1340,11 +1125,6 @@ std::vector GatewayKnxBridge::describeDaliBindings() cons return bindings; } -bool GatewayKnxBridge::matchesCemiFrame(const uint8_t* data, size_t len) const { - const auto decoded = DecodeCemiGroupWrite(data, len); - return decoded.has_value() && matchesGroupAddress(decoded->group_address); -} - bool GatewayKnxBridge::matchesGroupAddress(uint16_t group_address) const { if (!config_.dali_router_enabled) { return false; @@ -1370,14 +1150,6 @@ bool GatewayKnxBridge::matchesGroupAddress(uint16_t group_address) const { GatewayKnxDaliTargetForSubgroup(sub).has_value(); } -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) { @@ -1917,10 +1689,9 @@ DaliBridgeResult GatewayKnxBridge::executeForDecodedWrite(uint16_t group_address } } -GatewayKnxTpIpRouter::GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHandler handler, +GatewayKnxTpIpRouter::GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, std::string openknx_namespace) : bridge_(bridge), - handler_(std::move(handler)), openknx_namespace_(std::move(openknx_namespace)) { openknx_lock_ = xSemaphoreCreateMutex(); startup_semaphore_ = xSemaphoreCreateBinary(); @@ -2091,9 +1862,14 @@ void GatewayKnxTpIpRouter::TaskEntry(void* arg) { esp_err_t GatewayKnxTpIpRouter::initializeRuntime() { { SemaphoreGuard guard(openknx_lock_); + auto tp_uart_interface = createOpenKnxTpUartInterface(); + if (GatewayKnxConfigUsesTpUart(config_) && tp_uart_interface == nullptr && !last_error_.empty()) { + return ESP_FAIL; + } ets_device_ = std::make_unique(openknx_namespace_, config_.individual_address, - effectiveTunnelAddress()); + effectiveTunnelAddress(), + std::move(tp_uart_interface)); knx_ip_parameters_ = std::make_unique( ets_device_->deviceObject(), ets_device_->platform()); openknx_configured_.store(ets_device_->configured()); @@ -2132,6 +1908,11 @@ esp_err_t GatewayKnxTpIpRouter::initializeRuntime() { ESP_LOGD(kTag, "secure KNX group write not routed to DALI: %s", result.error.c_str()); } }); + ets_device_->setBusFrameSender([this](const uint8_t* data, size_t len) { + routeOpenKnxGroupWrite(data, len, "KNX TP frame"); + sendTunnelIndication(data, len); + sendRoutingIndication(data, len); + }); syncOpenKnxConfigFromDevice(); } if (!configureTpUart()) { @@ -2159,12 +1940,12 @@ void GatewayKnxTpIpRouter::taskLoop() { } std::array buffer{}; auto run_maintenance = [this]() { - pollTpUart(); { SemaphoreGuard guard(openknx_lock_); if (ets_device_ != nullptr) { pollProgrammingButton(); ets_device_->loop(); + tp_uart_online_ = ets_device_->tpUartOnline(); updateProgrammingLed(); } } @@ -2309,10 +2090,6 @@ void GatewayKnxTpIpRouter::closeSockets() { resetTunnelClient(client); } last_tunnel_channel_id_ = 0; - if (tp_uart_port_ >= 0) { - uart_driver_delete(static_cast(tp_uart_port_)); - tp_uart_port_ = -1; - } } bool GatewayKnxTpIpRouter::configureSocket() { @@ -2533,26 +2310,22 @@ void GatewayKnxTpIpRouter::refreshNetworkInterfaces(bool force_log) { } } -bool GatewayKnxTpIpRouter::configureTpUart() { +std::unique_ptr +GatewayKnxTpIpRouter::createOpenKnxTpUartInterface() { if (!GatewayKnxConfigUsesTpUart(config_)) { tp_uart_port_ = -1; + tp_uart_tx_pin_ = -1; + tp_uart_rx_pin_ = -1; tp_uart_online_ = false; ESP_LOGI(kTag, "KNX TP-UART disabled by UART port; KNXnet/IP uses IP-only runtime"); - return true; + return nullptr; } const auto& serial = config_.tp_uart; if (serial.uart_port < 0 || serial.uart_port > 2) { last_error_ = "invalid KNX TP-UART port " + std::to_string(serial.uart_port); ESP_LOGE(kTag, "%s", last_error_.c_str()); - return false; + return nullptr; } - uart_config_t uart_config{}; - uart_config.baud_rate = static_cast(serial.baudrate); - uart_config.data_bits = UART_DATA_8_BITS; - uart_config.parity = serial.nine_bit_mode ? UART_PARITY_EVEN : UART_PARITY_DISABLE; - 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); int tx_pin = UART_PIN_NO_CHANGE; int rx_pin = UART_PIN_NO_CHANGE; @@ -2565,45 +2338,36 @@ bool GatewayKnxTpIpRouter::configureTpUart() { (!rx_pin_ok ? std::string("RX") : std::string("")) + " pin; configure explicit txPin/rxPin values"; ESP_LOGE(kTag, "%s", last_error_.c_str()); - return false; - } - esp_err_t err = uart_param_config(uart_port, &uart_config); - if (err != ESP_OK) { - last_error_ = EspErrDetail("failed to configure KNX TP-UART parameters on UART" + - std::to_string(serial.uart_port), - err); - ESP_LOGE(kTag, "%s", last_error_.c_str()); - return false; - } - err = uart_set_pin(uart_port, tx_pin, rx_pin, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); - if (err != ESP_OK) { - last_error_ = EspErrDetail("failed to configure KNX TP-UART pins uart=" + - std::to_string(serial.uart_port) + " tx=" + - UartPinDescription(serial.tx_pin, tx_pin) + " rx=" + - UartPinDescription(serial.rx_pin, rx_pin), - err); - ESP_LOGE(kTag, "%s", last_error_.c_str()); - return false; - } - err = uart_driver_install(uart_port, serial.rx_buffer_size, serial.tx_buffer_size, 0, nullptr, - 0); - if (err != ESP_OK) { - last_error_ = EspErrDetail("failed to install KNX TP-UART driver on UART" + - std::to_string(serial.uart_port), - err); - ESP_LOGE(kTag, "%s", last_error_.c_str()); - return false; + return nullptr; } tp_uart_port_ = serial.uart_port; tp_uart_tx_pin_ = tx_pin; tp_uart_rx_pin_ = rx_pin; - if (!initializeTpUart()) { + return std::make_unique( + uart_port, serial.tx_pin, serial.rx_pin, serial.rx_buffer_size, + serial.tx_buffer_size, serial.nine_bit_mode); +} + +bool GatewayKnxTpIpRouter::configureTpUart() { + if (tp_uart_port_ < 0) { + return true; + } + if (ets_device_ == nullptr || !ets_device_->hasTpUart()) { + last_error_ = "KNX TP-UART interface is unavailable in OpenKNX runtime"; + ESP_LOGE(kTag, "%s", last_error_.c_str()); + return false; + } + tp_uart_online_ = ets_device_->enableTpUart(true); + if (!tp_uart_online_) { + last_error_ = "OpenKNX failed to initialize KNX TP-UART uart=" + + std::to_string(config_.tp_uart.uart_port) + " tx=" + + UartPinDescription(config_.tp_uart.tx_pin, tp_uart_tx_pin_) + " rx=" + + UartPinDescription(config_.tp_uart.rx_pin, tp_uart_rx_pin_); if (ets_device_ != nullptr && !ets_device_->configured()) { ESP_LOGW(kTag, "%s; continuing KNXnet/IP in commissioning-only IP mode so ETS can program the " "device", last_error_.c_str()); - uart_driver_delete(uart_port); tp_uart_port_ = -1; tp_uart_online_ = false; return true; @@ -2612,10 +2376,10 @@ bool GatewayKnxTpIpRouter::configureTpUart() { return false; } ESP_LOGI(kTag, "KNX TP-UART online uart=%d tx=%s rx=%s baud=%u nineBit=%d", - serial.uart_port, - UartPinDescription(serial.tx_pin, tp_uart_tx_pin_).c_str(), - UartPinDescription(serial.rx_pin, tp_uart_rx_pin_).c_str(), - static_cast(serial.baudrate), serial.nine_bit_mode); + config_.tp_uart.uart_port, + UartPinDescription(config_.tp_uart.tx_pin, tp_uart_tx_pin_).c_str(), + UartPinDescription(config_.tp_uart.rx_pin, tp_uart_rx_pin_).c_str(), + static_cast(config_.tp_uart.baudrate), config_.tp_uart.nine_bit_mode); return true; } @@ -2664,66 +2428,6 @@ bool GatewayKnxTpIpRouter::configureProgrammingGpio() { return true; } -bool GatewayKnxTpIpRouter::initializeTpUart() { - if (tp_uart_port_ < 0) { - return false; - } - const uart_port_t uart_port = static_cast(tp_uart_port_); - tp_rx_frame_.clear(); - tp_last_sent_telegram_.clear(); - tp_uart_last_byte_tick_ = 0; - tp_uart_extended_frame_ = false; - tp_uart_online_ = false; - uart_flush_input(uart_port); - - const uint8_t reset_request = kTpUartResetRequest; - if (uart_write_bytes(uart_port, &reset_request, 1) != 1) { - last_error_ = "failed to send KNX TP-UART reset request uart=" + - std::to_string(tp_uart_port_); - return false; - } - - const TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(1500); - bool saw_reset = false; - std::array buffer{}; - while (xTaskGetTickCount() < deadline) { - const int read = uart_read_bytes(uart_port, buffer.data(), buffer.size(), - pdMS_TO_TICKS(config_.tp_uart.read_timeout_ms)); - if (read <= 0) { - continue; - } - for (int index = 0; index < read; ++index) { - const uint8_t byte = buffer[static_cast(index)]; - if (!saw_reset) { - if (byte == kTpUartResetIndication) { - saw_reset = true; - const std::array set_address{ - kTpUartSetAddressRequest, - static_cast((effectiveIpInterfaceIndividualAddress() >> 8) & 0xff), - static_cast(effectiveIpInterfaceIndividualAddress() & 0xff), - }; - uart_write_bytes(uart_port, set_address.data(), set_address.size()); - const uint8_t state_request = kTpUartStateRequest; - uart_write_bytes(uart_port, &state_request, 1); - } - continue; - } - if ((byte & kTpUartStateIndicationMask) == kTpUartStateIndicationMask) { - tp_uart_online_ = true; - return true; - } - } - } - - last_error_ = (saw_reset ? "timed out waiting for KNX TP-UART state indication" - : "timed out waiting for KNX TP-UART reset indication") + - std::string(" uart=") + std::to_string(config_.tp_uart.uart_port) + - " tx=" + UartPinDescription(config_.tp_uart.tx_pin, tp_uart_tx_pin_) + - " rx=" + UartPinDescription(config_.tp_uart.rx_pin, tp_uart_rx_pin_) + - " timeoutMs=1500"; - return false; -} - void GatewayKnxTpIpRouter::handleUdpDatagram(const uint8_t* data, size_t len, const sockaddr_in& remote) { uint16_t service = 0; @@ -2740,13 +2444,13 @@ void GatewayKnxTpIpRouter::handleUdpDatagram(const uint8_t* data, size_t len, switch (service) { case kServiceSearchRequest: case kServiceSearchRequestExt: - handleSearchRequest(body, body_len, remote); + handleSearchRequest(service, data, total_len, remote); break; case kServiceDescriptionRequest: - handleDescriptionRequest(body, body_len, remote); + handleDescriptionRequest(data, total_len, remote); break; case kServiceDeviceConfigurationRequest: - handleDeviceConfigurationRequest(body, body_len, remote); + handleDeviceConfigurationRequest(data, total_len, remote); break; case kServiceDeviceConfigurationAck: case kServiceTunnellingAck: @@ -2759,24 +2463,24 @@ void GatewayKnxTpIpRouter::handleUdpDatagram(const uint8_t* data, size_t len, break; case kServiceRoutingIndication: if (config_.multicast_enabled) { - handleRoutingIndication(body, body_len); + handleRoutingIndication(data, total_len); } break; case kServiceTunnellingRequest: if (config_.tunnel_enabled) { - handleTunnellingRequest(body, body_len, remote); + handleTunnellingRequest(data, total_len, remote); } break; case kServiceConnectRequest: if (config_.tunnel_enabled) { - handleConnectRequest(body, body_len, remote); + handleConnectRequest(data, total_len, remote); } break; case kServiceConnectionStateRequest: - handleConnectionStateRequest(body, body_len, remote); + handleConnectionStateRequest(data, total_len, remote); break; case kServiceDisconnectRequest: - handleDisconnectRequest(body, body_len, remote); + handleDisconnectRequest(data, total_len, remote); break; default: ESP_LOGD(kTag, "ignore KNXnet/IP service=0x%04x len=%u from %s", @@ -2786,14 +2490,22 @@ void GatewayKnxTpIpRouter::handleUdpDatagram(const uint8_t* data, size_t len, } } -void GatewayKnxTpIpRouter::handleSearchRequest(const uint8_t* body, +void GatewayKnxTpIpRouter::handleSearchRequest(uint16_t service, const uint8_t* packet_data, size_t len, const sockaddr_in& remote) { - if (HasUnsupportedHpaiProtocolAt(body, len, 0, currentTransportAllowsTcpHpai())) { + if (packet_data == nullptr || len < kKnxNetIpHeaderSize + LEN_IPHPAI) { + ESP_LOGW(kTag, "invalid KNXnet/IP search request from %s len=%u", + EndpointString(remote).c_str(), static_cast(len)); + return; + } + std::vector request_packet(packet_data, packet_data + len); + KnxIpSearchRequest request(request_packet.data(), static_cast(request_packet.size())); + auto& request_hpai = request.hpai(); + if (OpenKnxHpaiUsesUnsupportedProtocol(request_hpai, currentTransportAllowsTcpHpai())) { ESP_LOGW(kTag, "ignore KNXnet/IP search request from %s: unsupported HPAI protocol", EndpointString(remote).c_str()); return; } - sockaddr_in response_remote = ResponseEndpointFromHpai(body, len, remote); + sockaddr_in response_remote = EndpointFromOpenKnxHpai(request_hpai, remote); selectOpenKnxNetworkInterface(response_remote); const auto hpai = localHpaiForRemote(response_remote, currentTransportAllowsTcpHpai()); @@ -2808,51 +2520,92 @@ void GatewayKnxTpIpRouter::handleSearchRequest(const uint8_t* body, 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), + if (service == kServiceSearchRequestExt) { + auto ext_dib = buildExtendedDeviceInfoDib(); + body_resp.insert(body_resp.end(), ext_dib.begin(), ext_dib.end()); + auto ip_dib = buildIpConfigDib(response_remote, false); + body_resp.insert(body_resp.end(), ip_dib.begin(), ip_dib.end()); + auto current_ip_dib = buildIpConfigDib(response_remote, true); + body_resp.insert(body_resp.end(), current_ip_dib.begin(), current_ip_dib.end()); + auto addresses_dib = buildKnxAddressesDib(); + body_resp.insert(body_resp.end(), addresses_dib.begin(), addresses_dib.end()); + auto tunneling_dib = buildTunnelingInfoDib(); + body_resp.insert(body_resp.end(), tunneling_dib.begin(), tunneling_dib.end()); + } + const auto response_packet = OpenKnxIpPacket( + service == kServiceSearchRequestExt ? kServiceSearchResponseExt : kServiceSearchResponse, + body_resp); + sendPacket(response_packet, response_remote); + ESP_LOGI(kTag, "sent KNXnet/IP search response service=0x%04x namespace=%s mainGroup=%u to %s:%u endpoint=%u.%u.%u.%u:%u", + static_cast(service), 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, +void GatewayKnxTpIpRouter::handleDescriptionRequest(const uint8_t* packet_data, size_t len, const sockaddr_in& remote) { - if (HasUnsupportedHpaiProtocolAt(body, len, 0, currentTransportAllowsTcpHpai())) { + if (packet_data == nullptr || len < kKnxNetIpHeaderSize + LEN_IPHPAI) { + ESP_LOGW(kTag, "invalid KNXnet/IP description request from %s len=%u", + EndpointString(remote).c_str(), static_cast(len)); + return; + } + std::vector request_packet(packet_data, packet_data + len); + KnxIpDescriptionRequest request(request_packet.data(), static_cast(request_packet.size())); + auto& hpai = request.hpaiCtrl(); + if (OpenKnxHpaiUsesUnsupportedProtocol(hpai, currentTransportAllowsTcpHpai())) { ESP_LOGW(kTag, "ignore KNXnet/IP description request from %s: unsupported HPAI protocol", EndpointString(remote).c_str()); return; } - const sockaddr_in response_remote = ResponseEndpointFromHpai(body, len, remote); + const sockaddr_in response_remote = EndpointFromOpenKnxHpai(hpai, remote); selectOpenKnxNetworkInterface(response_remote); auto device = buildDeviceInfoDib(response_remote); auto services = buildSupportedServiceDib(); std::vector body_resp; - body_resp.reserve(device.size() + services.size()); + auto extended = buildExtendedDeviceInfoDib(); + auto ip_config = buildIpConfigDib(response_remote, false); + auto current_ip_config = buildIpConfigDib(response_remote, true); + auto addresses = buildKnxAddressesDib(); + auto tunneling = buildTunnelingInfoDib(); + body_resp.reserve(device.size() + services.size() + extended.size() + ip_config.size() + + current_ip_config.size() + addresses.size() + tunneling.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); + body_resp.insert(body_resp.end(), extended.begin(), extended.end()); + body_resp.insert(body_resp.end(), ip_config.begin(), ip_config.end()); + body_resp.insert(body_resp.end(), current_ip_config.begin(), current_ip_config.end()); + body_resp.insert(body_resp.end(), addresses.begin(), addresses.end()); + body_resp.insert(body_resp.end(), tunneling.begin(), tunneling.end()); + const auto response_packet = OpenKnxIpPacket(kServiceDescriptionResponse, body_resp); + sendPacket(response_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) { - if (body == nullptr || len == 0) { +void GatewayKnxTpIpRouter::handleRoutingIndication(const uint8_t* packet_data, size_t len) { + if (packet_data == nullptr || len < kKnxNetIpHeaderSize + 2) { return; } - const bool consumed_by_openknx = handleOpenKnxBusFrame(body, len); - if (!consumed_by_openknx && shouldRouteDaliApplicationFrames()) { - const DaliBridgeResult result = handler_(body, len); - if (!result.ok && !result.error.empty()) { - ESP_LOGD(kTag, "KNX routing indication ignored: %s", result.error.c_str()); - } + std::vector packet(packet_data, packet_data + len); + KnxIpRoutingIndication routing(packet.data(), static_cast(packet.size())); + CemiFrame& frame = routing.frame(); + if (!frame.valid()) { + ESP_LOGW(kTag, "invalid OpenKNX routing cEMI len=%u", static_cast(len)); + return; + } + const uint8_t* cemi = frame.data(); + const size_t cemi_len = frame.dataLength(); + 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); + if (!consumed_by_openknx && !routed_to_dali && !sent_to_tp) { + ESP_LOGD(kTag, "KNX routing indication ignored: no OpenKNX/DALI handler matched"); } - forwardCemiToTp(body, len); } GatewayKnxTpIpRouter::TunnelClient* GatewayKnxTpIpRouter::findTunnelClient( @@ -2974,15 +2727,24 @@ void GatewayKnxTpIpRouter::pruneStaleTunnelClients() { } } -void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* body, size_t len, +void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* packet_data, size_t len, const sockaddr_in& remote) { - if (body == nullptr || len < 5 || body[0] != 0x04) { + if (packet_data == nullptr || len < kKnxNetIpHeaderSize + LEN_CH + 2) { ESP_LOGW(kTag, "invalid KNXnet/IP tunnelling request from %s len=%u", EndpointString(remote).c_str(), static_cast(len)); return; } - const uint8_t channel_id = body[1]; - const uint8_t sequence = body[2]; + std::vector packet(packet_data, packet_data + len); + KnxIpTunnelingRequest tunneling(packet.data(), static_cast(packet.size())); + auto& header = tunneling.connectionHeader(); + if (header.length() != LEN_CH) { + ESP_LOGW(kTag, "invalid KNXnet/IP tunnelling header from %s len=%u chLen=%u", + EndpointString(remote).c_str(), static_cast(len), + static_cast(header.length())); + return; + } + const uint8_t channel_id = header.channelId(); + const uint8_t sequence = header.sequenceCounter(); TunnelClient* client = findTunnelClient(channel_id); if (client == nullptr) { ESP_LOGW(kTag, "reject KNXnet/IP tunnelling request channel=%u seq=%u from %s: no connection", @@ -3016,45 +2778,48 @@ void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* body, size_t l client->received_sequence = sequence; client->last_activity_tick = xTaskGetTickCount(); sendTunnellingAck(channel_id, sequence, kKnxNoError, client->data_remote); - const auto cemi_frame = CemiWithTunnelSourceAddress(body + 4, len - 4, client->individual_address); - const uint8_t* cemi = cemi_frame.data(); - const size_t cemi_len = cemi_frame.size(); + CemiFrame& frame = tunneling.frame(); + if (!frame.valid()) { + ESP_LOGW(kTag, "invalid OpenKNX tunnel cEMI channel=%u seq=%u from %s", + static_cast(channel_id), static_cast(sequence), + EndpointString(remote).c_str()); + return; + } + if (frame.messageCode() == L_data_req && frame.sourceAddress() == 0) { + frame.sourceAddress(client->individual_address); + } + const uint8_t* cemi = frame.data(); + const size_t cemi_len = frame.dataLength(); 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()); - // 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) { - // OpenKNX (CemiServer / Bau07B0) handled the frame (ETS memory-model - // queries, function-property commands, etc.). It may have emitted - // a response already via EmitTunnelFrame. + 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) { return; } - if (shouldRouteDaliApplicationFrames()) { - 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()); - } - } - // Frame was already forwarded to TP-UART above; no need to forward again. } -void GatewayKnxTpIpRouter::handleDeviceConfigurationRequest(const uint8_t* body, size_t len, +void GatewayKnxTpIpRouter::handleDeviceConfigurationRequest(const uint8_t* packet_data, size_t len, const sockaddr_in& remote) { - if (body == nullptr || len < 5 || body[0] != 0x04) { + if (packet_data == nullptr || len < kKnxNetIpHeaderSize + LEN_CH + 2) { ESP_LOGW(kTag, "invalid KNXnet/IP device-configuration request from %s len=%u", EndpointString(remote).c_str(), static_cast(len)); return; } - const uint8_t channel_id = body[1]; - const uint8_t sequence = body[2]; + std::vector packet(packet_data, packet_data + len); + KnxIpConfigRequest config_request(packet.data(), static_cast(packet.size())); + auto& header = config_request.connectionHeader(); + if (header.length() != LEN_CH) { + ESP_LOGW(kTag, "invalid KNXnet/IP device-configuration header from %s len=%u chLen=%u", + EndpointString(remote).c_str(), static_cast(len), + static_cast(header.length())); + return; + } + const uint8_t channel_id = header.channelId(); + const uint8_t sequence = header.sequenceCounter(); TunnelClient* client = findTunnelClient(channel_id); if (client == nullptr) { ESP_LOGW(kTag, "reject KNXnet/IP device-configuration request channel=%u seq=%u from %s: no connection", @@ -3073,38 +2838,49 @@ void GatewayKnxTpIpRouter::handleDeviceConfigurationRequest(const uint8_t* body, } client->last_activity_tick = xTaskGetTickCount(); sendDeviceConfigurationAck(channel_id, sequence, kKnxNoError, client->data_remote); - const uint8_t* cemi = body + 4; - const size_t cemi_len = len - 4; + CemiFrame& frame = config_request.frame(); + if (!frame.valid()) { + ESP_LOGW(kTag, "invalid OpenKNX device-configuration cEMI channel=%u seq=%u from %s", + static_cast(channel_id), static_cast(sequence), + EndpointString(remote).c_str()); + return; + } + const uint8_t* cemi = frame.data(); + const size_t cemi_len = frame.dataLength(); ESP_LOGI(kTag, "rx KNXnet/IP device-configuration request channel=%u seq=%u cemiLen=%u from %s", static_cast(channel_id), static_cast(sequence), static_cast(cemi_len), EndpointString(remote).c_str()); - if (!handleOpenKnxTunnelFrame(cemi, cemi_len, client)) { + if (!handleOpenKnxTunnelFrame(cemi, cemi_len, client, kServiceDeviceConfigurationRequest)) { ESP_LOGW(kTag, "KNXnet/IP device-configuration cEMI was not consumed by OpenKNX cemiLen=%u", static_cast(cemi_len)); } } -void GatewayKnxTpIpRouter::handleConnectRequest(const uint8_t* body, size_t len, +void GatewayKnxTpIpRouter::handleConnectRequest(const uint8_t* packet_data, size_t len, const sockaddr_in& remote) { - if (body == nullptr || len < 18) { + if (packet_data == nullptr || len < kKnxNetIpHeaderSize + 2 * LEN_IPHPAI + 2) { ESP_LOGW(kTag, "invalid KNXnet/IP connect request from %s len=%u", EndpointString(remote).c_str(), static_cast(len)); return; } - if (HasUnsupportedHpaiProtocolAt(body, len, 0, currentTransportAllowsTcpHpai()) || - HasUnsupportedHpaiProtocolAt(body, len, 8, currentTransportAllowsTcpHpai())) { + std::vector packet(packet_data, packet_data + len); + KnxIpConnectRequest request(packet.data(), static_cast(packet.size())); + auto& control_hpai = request.hpaiCtrl(); + auto& data_hpai = request.hpaiData(); + if (OpenKnxHpaiUsesUnsupportedProtocol(control_hpai, currentTransportAllowsTcpHpai()) || + OpenKnxHpaiUsesUnsupportedProtocol(data_hpai, currentTransportAllowsTcpHpai())) { ESP_LOGW(kTag, "reject KNXnet/IP connect from %s: unsupported HPAI protocol", EndpointString(remote).c_str()); sendConnectResponse(0, kKnxErrorConnectionType, remote, kKnxConnectionTypeTunnel, 0); return; } - sockaddr_in control_remote = EndpointFromHpaiAt(body, len, 0, remote); - sockaddr_in data_remote = EndpointFromHpaiAt(body, len, 8, remote); + sockaddr_in control_remote = EndpointFromOpenKnxHpai(control_hpai, remote); + sockaddr_in data_remote = EndpointFromOpenKnxHpai(data_hpai, remote); selectOpenKnxNetworkInterface(control_remote); - const size_t cri_offset = 16; - const uint8_t cri_length = body[cri_offset]; - const uint8_t connection_type = body[cri_offset + 1]; - if (cri_length < 2 || cri_offset + cri_length > len) { + auto& cri = request.cri(); + const uint8_t cri_length = cri.length(); + const uint8_t connection_type = static_cast(cri.type()); + if (cri_length < 2 || kKnxNetIpHeaderSize + 2 * LEN_IPHPAI + cri_length > len) { ESP_LOGW(kTag, "invalid KNXnet/IP connect CRI from %s len=%u criLen=%u", EndpointString(remote).c_str(), static_cast(len), static_cast(cri_length)); @@ -3119,10 +2895,10 @@ void GatewayKnxTpIpRouter::handleConnectRequest(const uint8_t* body, size_t len, return; } if (connection_type == kKnxConnectionTypeTunnel && - (cri_length < 4 || body[cri_offset + 2] != kKnxTunnelLayerLink)) { + (cri_length < 4 || cri.layer() != kKnxTunnelLayerLink)) { ESP_LOGW(kTag, "reject KNXnet/IP tunnel connect from %s unsupported layer=0x%02x", EndpointString(remote).c_str(), - static_cast(cri_length >= 3 ? body[cri_offset + 2] : 0)); + static_cast(cri_length >= 3 ? cri.layer() : 0)); sendConnectResponse(0, kKnxErrorTunnellingLayer, control_remote, connection_type, 0); return; } @@ -3154,19 +2930,22 @@ void GatewayKnxTpIpRouter::handleConnectRequest(const uint8_t* body, size_t len, client->individual_address); } -void GatewayKnxTpIpRouter::handleConnectionStateRequest(const uint8_t* body, size_t len, +void GatewayKnxTpIpRouter::handleConnectionStateRequest(const uint8_t* packet_data, size_t len, const sockaddr_in& remote) { - if (body == nullptr || len < 2) { + if (packet_data == nullptr || len < kKnxNetIpHeaderSize + 2 + LEN_IPHPAI) { return; } - if (HasUnsupportedHpaiProtocolAt(body, len, 2, currentTransportAllowsTcpHpai())) { + std::vector packet(packet_data, packet_data + len); + KnxIpStateRequest request(packet.data(), static_cast(packet.size())); + auto& control_hpai = request.hpaiCtrl(); + if (OpenKnxHpaiUsesUnsupportedProtocol(control_hpai, currentTransportAllowsTcpHpai())) { ESP_LOGW(kTag, "reject KNXnet/IP connection-state request from %s: unsupported HPAI protocol", EndpointString(remote).c_str()); return; } - const uint8_t channel_id = body[0]; - const sockaddr_in control_remote = EndpointFromHpaiAt(body, len, 2, remote); + const uint8_t channel_id = request.channelId(); + const sockaddr_in control_remote = EndpointFromOpenKnxHpai(control_hpai, remote); TunnelClient* client = findTunnelClient(channel_id); const bool endpoint_matches = client != nullptr && ((client->tcp_sock >= 0 && active_tcp_sock_ == client->tcp_sock) || @@ -3185,18 +2964,21 @@ void GatewayKnxTpIpRouter::handleConnectionStateRequest(const uint8_t* body, siz channel_id, status, control_remote); } -void GatewayKnxTpIpRouter::handleDisconnectRequest(const uint8_t* body, size_t len, +void GatewayKnxTpIpRouter::handleDisconnectRequest(const uint8_t* packet_data, size_t len, const sockaddr_in& remote) { - if (body == nullptr || len < 2) { + if (packet_data == nullptr || len < kKnxNetIpHeaderSize + 2 + LEN_IPHPAI) { return; } - if (HasUnsupportedHpaiProtocolAt(body, len, 2, currentTransportAllowsTcpHpai())) { + std::vector packet(packet_data, packet_data + len); + KnxIpDisconnectRequest request(packet.data(), static_cast(packet.size())); + auto& control_hpai = request.hpaiCtrl(); + if (OpenKnxHpaiUsesUnsupportedProtocol(control_hpai, currentTransportAllowsTcpHpai())) { ESP_LOGW(kTag, "reject KNXnet/IP disconnect request from %s: unsupported HPAI protocol", EndpointString(remote).c_str()); return; } - const uint8_t channel_id = body[0]; - const sockaddr_in control_remote = EndpointFromHpaiAt(body, len, 2, remote); + const uint8_t channel_id = request.channelId(); + const sockaddr_in control_remote = EndpointFromOpenKnxHpai(control_hpai, remote); TunnelClient* client = findTunnelClient(channel_id); const bool endpoint_matches = client != nullptr && ((client->tcp_sock >= 0 && active_tcp_sock_ == client->tcp_sock) || @@ -3255,14 +3037,19 @@ void GatewayKnxTpIpRouter::sendDeviceConfigurationAck(uint8_t channel_id, uint8_ void GatewayKnxTpIpRouter::sendConnectionHeaderAck(uint16_t service, 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(service, body); + KnxIpTunnelingAck ack; + ack.serviceTypeIdentifier(service); + ack.connectionHeader().length(LEN_CH); + ack.connectionHeader().channelId(channel_id); + ack.connectionHeader().sequenceCounter(sequence); + ack.connectionHeader().status(status); + const std::vector packet(ack.data(), ack.data() + ack.totalLength()); sendPacket(packet, remote); } void GatewayKnxTpIpRouter::sendSecureSessionStatus(uint8_t status, const sockaddr_in& remote) { const std::vector body{status, 0x00}; - const auto packet = KnxNetIpPacket(kServiceSecureSessionStatus, body); + const auto packet = OpenKnxIpPacket(kServiceSecureSessionStatus, body); sendPacket(packet, remote); } @@ -3340,8 +3127,8 @@ std::vector GatewayKnxTpIpRouter::buildDeviceInfoDib( uint8_t mac[6]{}; if (ReadBaseMac(mac)) { - dib[8] = static_cast((kKnxManufacturerId >> 8) & 0xff); - dib[9] = static_cast(kKnxManufacturerId & 0xff); + dib[8] = static_cast((knx_internal::kReg1DaliManufacturerId >> 8) & 0xff); + dib[9] = static_cast(knx_internal::kReg1DaliManufacturerId & 0xff); std::memcpy(dib.data() + 10, mac + 2, 4); std::memcpy(dib.data() + 18, mac, 6); } @@ -3466,36 +3253,47 @@ void GatewayKnxTpIpRouter::sendTunnelIndication(const uint8_t* data, size_t len) void GatewayKnxTpIpRouter::sendTunnelIndicationToClient(TunnelClient& client, const uint8_t* data, size_t len) { + sendCemiFrameToClient(client, kServiceTunnellingRequest, data, len); +} + +void GatewayKnxTpIpRouter::sendCemiFrameToClient(TunnelClient& client, uint16_t service, + const uint8_t* data, size_t len) { if (!client.connected || data == nullptr || len == 0) { return; } - const uint16_t service = TunnelServiceForCemi(data, len); - std::vector body; - body.reserve(4 + len); - body.push_back(0x04); - body.push_back(client.channel_id); - body.push_back(client.send_sequence++); - body.push_back(0x00); - body.insert(body.end(), data, data + len); - const auto packet = KnxNetIpPacket(service, body); + 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 cEMI service=0x%04x len=%u to %s", + static_cast(service), static_cast(len), + EndpointString(client.data_remote).c_str()); + return; + } + KnxIpTunnelingRequest request(frame); + request.serviceTypeIdentifier(service); + request.connectionHeader().length(LEN_CH); + request.connectionHeader().channelId(client.channel_id); + request.connectionHeader().sequenceCounter(client.send_sequence++); + request.connectionHeader().status(kKnxNoError); + const std::vector packet(request.data(), request.data() + request.totalLength()); sendPacketToTunnelClient(client, 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(body[2]), static_cast(data[0]), + static_cast(request.connectionHeader().sequenceCounter()), static_cast(data[0]), static_cast(len), EndpointString(client.data_remote).c_str()); } 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); + KnxIpStateResponse response(channel_id, status); + const std::vector packet(response.data(), response.data() + response.totalLength()); sendPacket(packet, 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); + KnxIpDisconnectResponse response(channel_id, status); + const std::vector packet(response.data(), response.data() + response.totalLength()); sendPacket(packet, remote); } @@ -3503,41 +3301,38 @@ void GatewayKnxTpIpRouter::sendConnectResponse(uint8_t channel_id, uint8_t statu const sockaddr_in& remote, uint8_t connection_type, uint16_t tunnel_address) { - std::vector body; - body.reserve(16); - body.push_back(channel_id); - body.push_back(status); if (status != kKnxNoError) { - const auto packet = KnxNetIpPacket(kServiceConnectResponse, body); + KnxIpConnectResponse response(channel_id, status); + const std::vector packet(response.data(), response.data() + response.totalLength()); sendPacket(packet, remote); ESP_LOGI(kTag, "sent KNXnet/IP connect error channel=%u status=0x%02x to %s", static_cast(channel_id), static_cast(status), EndpointString(remote).c_str()); return; } - const auto data_endpoint = localHpaiForRemote(remote, currentTransportAllowsTcpHpai()); - if (!data_endpoint.has_value()) { + const auto netif = SelectKnxNetifForRemote(remote); + if (!netif.has_value() || knx_ip_parameters_ == nullptr) { ESP_LOGW(kTag, "cannot accept KNXnet/IP connect from %s: no active IPv4 interface", EndpointString(remote).c_str()); - const auto packet = KnxNetIpPacket(kServiceConnectResponse, - std::vector{channel_id, kKnxErrorConnectionType}); + KnxIpConnectResponse response(channel_id, kKnxErrorConnectionType); + const std::vector packet(response.data(), response.data() + response.totalLength()); sendPacket(packet, remote); return; } - body.insert(body.end(), data_endpoint->begin(), data_endpoint->end()); - body.push_back(connection_type == kKnxConnectionTypeTunnel ? 0x04 : 0x02); - body.push_back(connection_type); - if (connection_type == kKnxConnectionTypeTunnel) { - body.push_back(static_cast((tunnel_address >> 8) & 0xff)); - body.push_back(static_cast(tunnel_address & 0xff)); - } - const auto packet = KnxNetIpPacket(kServiceConnectResponse, body); + KnxIpConnectResponse response(*knx_ip_parameters_, tunnel_address, config_.udp_port, + channel_id, connection_type); + 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); + 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", static_cast(channel_id), static_cast(connection_type), - EndpointString(remote).c_str(), static_cast((*data_endpoint)[2]), - static_cast((*data_endpoint)[3]), static_cast((*data_endpoint)[4]), - static_cast((*data_endpoint)[5]), static_cast(config_.udp_port)); + 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)); } void GatewayKnxTpIpRouter::sendRoutingIndication(const uint8_t* data, size_t len) { @@ -3548,8 +3343,15 @@ void GatewayKnxTpIpRouter::sendRoutingIndication(const uint8_t* data, size_t len 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); + 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 routing cEMI len=%u", + static_cast(len)); + return; + } + KnxIpRoutingIndication routing(frame); + const std::vector packet(routing.data(), routing.data() + routing.totalLength()); const auto netifs = ActiveKnxNetifs(); if (netifs.empty()) { SendAll(udp_sock_, packet.data(), packet.size(), remote); @@ -3577,15 +3379,17 @@ void GatewayKnxTpIpRouter::selectOpenKnxNetworkInterface(const sockaddr_in& remo } bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t len, - TunnelClient* response_client) { + TunnelClient* response_client, + uint16_t response_service) { SemaphoreGuard guard(openknx_lock_); if (ets_device_ == nullptr) { return false; } const bool consumed = ets_device_->handleTunnelFrame( - data, len, [this, response_client](const uint8_t* response, size_t response_len) { + data, len, [this, response_client, response_service](const uint8_t* response, + size_t response_len) { if (response_client != nullptr && response_client->connected) { - sendTunnelIndicationToClient(*response_client, response, response_len); + sendCemiFrameToClient(*response_client, response_service, response, response_len); } else { sendTunnelIndication(response, response_len); } @@ -3594,6 +3398,16 @@ bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t return consumed; } +bool GatewayKnxTpIpRouter::transmitOpenKnxTpFrame(const uint8_t* data, size_t len) { + SemaphoreGuard guard(openknx_lock_); + if (ets_device_ == nullptr) { + return false; + } + const bool sent = ets_device_->transmitTpFrame(data, len); + tp_uart_online_ = ets_device_->tpUartOnline(); + return sent; +} + bool GatewayKnxTpIpRouter::handleOpenKnxBusFrame(const uint8_t* data, size_t len) { SemaphoreGuard guard(openknx_lock_); if (ets_device_ == nullptr) { @@ -3604,6 +3418,29 @@ bool GatewayKnxTpIpRouter::handleOpenKnxBusFrame(const uint8_t* data, size_t len return consumed; } +bool GatewayKnxTpIpRouter::routeOpenKnxGroupWrite(const uint8_t* data, size_t len, + const char* context) { + const auto decoded = DecodeOpenKnxGroupWrite(data, len); + if (!decoded.has_value()) { + return false; + } + if (!shouldRouteDaliApplicationFrames()) { + return true; + } + const DaliBridgeResult result = group_write_handler_ + ? group_write_handler_(decoded->group_address, + decoded->data.data(), + decoded->data.size()) + : bridge_.handleGroupWrite(decoded->group_address, + decoded->data.data(), + decoded->data.size()); + if (!result.ok && !result.error.empty()) { + ESP_LOGD(kTag, "%s not routed to DALI: %s", context == nullptr ? "KNX group write" : context, + result.error.c_str()); + } + return true; +} + bool GatewayKnxTpIpRouter::emitOpenKnxGroupValue(uint16_t group_object_number, const uint8_t* data, size_t len) { SemaphoreGuard guard(openknx_lock_); @@ -3614,7 +3451,10 @@ bool GatewayKnxTpIpRouter::emitOpenKnxGroupValue(uint16_t group_object_number, group_object_number, data, len, [this](const uint8_t* frame_data, size_t frame_len) { sendRoutingIndication(frame_data, frame_len); sendTunnelIndication(frame_data, frame_len); - forwardCemiToTp(frame_data, frame_len); + if (ets_device_ != nullptr) { + const bool sent_to_tp = ets_device_->transmitTpFrame(frame_data, frame_len); + tp_uart_online_ = sent_to_tp || ets_device_->tpUartOnline(); + } }); syncOpenKnxConfigFromDevice(); return emitted; @@ -3700,131 +3540,4 @@ uint16_t GatewayKnxTpIpRouter::effectiveTunnelAddress() const { return address; } -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) { - const uint8_t byte = buffer[static_cast(index)]; - if (tp_rx_frame_.empty()) { - if (IsTpUartControlByte(byte)) { - handleTpUartControlByte(byte); - continue; - } - if (byte == 0xcb || (byte & 0x17U) == 0x13U) { - continue; - } - } - - const TickType_t now = xTaskGetTickCount(); - if (!tp_rx_frame_.empty() && tp_uart_last_byte_tick_ != 0 && - now - tp_uart_last_byte_tick_ > pdMS_TO_TICKS(1000)) { - tp_rx_frame_.clear(); - } - - if (tp_rx_frame_.empty()) { - if (IsTpUartFrameStart(byte, &tp_uart_extended_frame_)) { - tp_rx_frame_.push_back(byte); - tp_uart_last_byte_tick_ = now; - } - continue; - } - - tp_rx_frame_.push_back(byte); - tp_uart_last_byte_tick_ = now; - const size_t expected = ExpectedTpFrameSize(tp_rx_frame_.data(), tp_rx_frame_.size()); - if (expected == 0) { - continue; - } - if (tp_rx_frame_.size() == expected) { - const uint8_t ack = kTpUartAckInfo; - uart_write_bytes(static_cast(tp_uart_port_), &ack, 1); - 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::handleTpUartControlByte(uint8_t byte) { - if (byte == kTpUartResetIndication) { - ESP_LOGW(kTag, "KNX TP-UART reset indication received; marking link offline"); - tp_uart_online_ = false; - return; - } - if (byte == kTpUartBusy) { - ESP_LOGW(kTag, "KNX TP-UART bus busy"); - return; - } - // 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; - } - // 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(frame_data, frame_len); - if (!cemi.has_value()) { - return; - } - const bool consumed_by_openknx = handleOpenKnxBusFrame(cemi->data(), cemi->size()); - if (!consumed_by_openknx) { - 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 || !tp_uart_online_) { - return; - } - const auto telegram = CemiToTpTelegram(data, len); - if (!telegram.has_value()) { - return; - } - tp_last_sent_telegram_ = *telegram; - const auto wrapped = WrapTpUartTelegram(*telegram); - uart_write_bytes(static_cast(tp_uart_port_), wrapped.data(), wrapped.size()); -} - } // namespace gateway \ No newline at end of file diff --git a/components/knx_dali_gw/include/knxprod.h b/components/knx_dali_gw/include/knxprod.h index 8acfb47..24c8a36 100644 --- a/components/knx_dali_gw/include/knxprod.h +++ b/components/knx_dali_gw/include/knxprod.h @@ -1,13 +1,27 @@ #pragma once +#include "sdkconfig.h" + +#ifndef CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID +#define CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID 0x00A4 +#endif + +#ifndef CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER +#define CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER 0x0001 +#endif + +#ifndef CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION +#define CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION 0x08 +#endif + // Minimal stub for knxprod.h — generated KNX product definitions. // The full file (1796 bytes of parameters, 1439 group objects) will be // adapted in Phase 3 to use the gateway/knx API directly. // Product identity -#define MAIN_OpenKnxId 0xA4 -#define MAIN_ApplicationNumber 1 -#define MAIN_ApplicationVersion 5 +#define MAIN_OpenKnxId (CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID & 0xff) +#define MAIN_ApplicationNumber CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER +#define MAIN_ApplicationVersion CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION #define MAIN_OrderNumber "REG1-Dali" #define MAIN_ParameterSize 1796 #define MAIN_MaxKoNumber 1439 @@ -17,8 +31,13 @@ enum PT_DeviceType : uint8_t { PT_deviceType_Deaktiviert = 0, PT_deviceType_DT0 = 1, PT_deviceType_DT1 = 2, - PT_deviceType_DT6 = 3, - PT_deviceType_DT8 = 4, + PT_deviceType_DT2 = 3, + PT_deviceType_DT3 = 4, + PT_deviceType_DT4 = 5, + PT_deviceType_DT5 = 6, + PT_deviceType_DT6 = 7, + PT_deviceType_DT7 = 8, + PT_deviceType_DT8 = 9, }; enum PT_ColorType : uint8_t { @@ -29,8 +48,8 @@ enum PT_ColorType : uint8_t { }; enum PT_ColorSpace : uint8_t { - PT_colorSpace_rgb = 0, - PT_colorSpace_xy = 1, + PT_colorSpace_rgb = 1, + PT_colorSpace_xy = 0, }; // Placeholder macros — will be replaced with direct Bau07B0 access in Phase 3. @@ -57,10 +76,10 @@ enum PT_ColorSpace : uint8_t { #define ParamHCL_type(channelIndex) (0) -// Group object offset placeholders -#define ADR_KoOffset 0 -#define GRP_KoOffset 0 -#define HCL_KoOffset 0 -#define ADR_KoBlockSize 0 -#define GRP_KoBlockSize 0 -#define HCL_KoBlockSize 0 +// Generated group object layout constants +#define ADR_KoOffset 12 +#define GRP_KoOffset 1164 +#define HCL_KoOffset 1436 +#define ADR_KoBlockSize 18 +#define GRP_KoBlockSize 17 +#define HCL_KoBlockSize 1 diff --git a/components/knx_dali_gw/src/knx_dali_gw.cpp b/components/knx_dali_gw/src/knx_dali_gw.cpp index 943e0d3..13cc5bb 100644 --- a/components/knx_dali_gw/src/knx_dali_gw.cpp +++ b/components/knx_dali_gw/src/knx_dali_gw.cpp @@ -2,6 +2,8 @@ #include "knx/bau07B0.h" +#include "sdkconfig.h" + #include "esp_log.h" namespace gateway { @@ -11,6 +13,38 @@ namespace { constexpr const char* kTag = "knx_dali_gw"; +#ifndef CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID +#define CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID 0x00A4 +#endif + +#ifndef CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER +#define CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER 0x0001 +#endif + +#ifndef CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION +#define CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION 0x08 +#endif + +constexpr uint16_t kKnxOemManufacturerId = + static_cast(CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID); +constexpr uint16_t kKnxOemApplicationNumber = + static_cast(CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER); +constexpr uint8_t kKnxOemApplicationVersion = + static_cast(CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION); +constexpr uint8_t kKnxOemHardwareType[6] = { + 0x00, + 0x00, + static_cast(kKnxOemManufacturerId & 0xff), + static_cast(kKnxOemApplicationNumber & 0xff), + kKnxOemApplicationVersion, + 0x00}; +constexpr uint8_t kKnxOemProgramVersion[5] = { + static_cast((kKnxOemManufacturerId >> 8) & 0xff), + static_cast(kKnxOemManufacturerId & 0xff), + static_cast((kKnxOemApplicationNumber >> 8) & 0xff), + static_cast(kKnxOemApplicationNumber & 0xff), + kKnxOemApplicationVersion}; + } // namespace // ============================================================================= @@ -31,12 +65,13 @@ struct KnxDaliGateway::Impl { bool init() { if (initialized) return true; - device.deviceObject().manufacturerId(0x00a4); + device.deviceObject().manufacturerId(kKnxOemManufacturerId); device.deviceObject().bauNumber(platform.uniqueSerialNumber()); - const uint8_t order_number[10] = {'R', 'E', 'G', '1', '-', 'D', 'a', 'l', 'i', 0}; + device.deviceObject().hardwareType(kKnxOemHardwareType); + const uint8_t order_number[10] = { + 'R', 'E', 'G', '1', '-', 'D', 'a', 'l', 'i', 0}; device.deviceObject().orderNumber(order_number); - const uint8_t program_version[5] = {0x00, 0xa4, 0x00, 0x01, 0x05}; - device.parameters().property(PID_PROG_VERSION)->write(program_version); + device.parameters().property(PID_PROG_VERSION)->write(kKnxOemProgramVersion); device.readMemory(); diff --git a/knx b/knx index dcf565d..23b0cdd 160000 --- a/knx +++ b/knx @@ -1 +1 @@ -Subproject commit dcf565dc03c5f0910e1e827ef1c0418b00fc06c6 +Subproject commit 23b0cddf24b6ea70f304361ca1064eb6351ee2ba diff --git a/knx_dali_gw b/knx_dali_gw deleted file mode 160000 index 6064d84..0000000 --- a/knx_dali_gw +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6064d845202c0971b121fbca23c7d378d52a5d41 diff --git a/tpuart b/tpuart index f8c01e6..d95248f 160000 --- a/tpuart +++ b/tpuart @@ -1 +1 @@ -Subproject commit f8c01e6a32b19f338b0b56ddb58a5d7a4135aece +Subproject commit d95248f994da5470242395c02fac1d4bd6887681