diff --git a/apps/gateway/main/Kconfig.projbuild b/apps/gateway/main/Kconfig.projbuild index eb260f7..2e91f54 100644 --- a/apps/gateway/main/Kconfig.projbuild +++ b/apps/gateway/main/Kconfig.projbuild @@ -713,6 +713,24 @@ config GATEWAY_KNX_MAIN_GROUP Main group used by the built-in KNX to DALI router. Middle groups select the data type and subgroups select broadcast, short-address, or group targets. +config GATEWAY_KNX_DALI_BUS_ID + int "KNX database target DALI bus id" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED + range 0 15 + default 0 + help + Selects the native DALI HAL bus targeted by the ETS KNX product database. + The current KNX-DALI application supports one ETS-controlled DALI bus. + +config GATEWAY_KNX_DEBUG_DUMP_MEMORY + bool "Dump full OpenKNX memory for debugging" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED + default n + help + Prints the complete OpenKNX non-volatile memory image when it is restored + or committed. Enable only while debugging ETS download and association + table issues, because the log is large and may include KNX configuration data. + config GATEWAY_KNX_TUNNEL_ENABLED bool "Enable KNXnet/IP tunneling mode" depends on GATEWAY_KNX_BRIDGE_SUPPORTED diff --git a/apps/gateway/main/app_main.cpp b/apps/gateway/main/app_main.cpp index f3b6b69..758e838 100644 --- a/apps/gateway/main/app_main.cpp +++ b/apps/gateway/main/app_main.cpp @@ -219,6 +219,10 @@ #define CONFIG_GATEWAY_KNX_MAIN_GROUP 0 #endif +#ifndef CONFIG_GATEWAY_KNX_DALI_BUS_ID +#define CONFIG_GATEWAY_KNX_DALI_BUS_ID 0 +#endif + #ifndef CONFIG_GATEWAY_KNX_UDP_PORT #define CONFIG_GATEWAY_KNX_UDP_PORT 3671 #endif @@ -606,6 +610,22 @@ bool ValidateChannelBindings() { if (kKnxBridgeSupported) { const int knx_uart = CONFIG_GATEWAY_KNX_TP_UART_PORT; + if (kKnxBridgeStartupEnabled) { + const uint8_t knx_dali_bus_id = static_cast(CONFIG_GATEWAY_KNX_DALI_BUS_ID); + int matches = 0; + for (int i = 0; i < CONFIG_GATEWAY_CHANNEL_COUNT; ++i) { + if (channels[i].enabled && channels[i].native_phy && + channels[i].native_bus_id == knx_dali_bus_id) { + ++matches; + } + } + if (matches != 1) { + ESP_LOGE(kTag, + "KNX DALI bus id %u must match exactly one enabled native DALI channel", + knx_dali_bus_id); + return false; + } + } if (knx_uart >= 0 && k485ControlEnabled && knx_uart == 0) { ESP_LOGE(kTag, "KNX TP UART0 conflicts with the UART0 control bridge"); return false; @@ -864,6 +884,7 @@ extern "C" void app_main(void) { default_knx.tunnel_enabled = kKnxTunnelEnabled; default_knx.multicast_enabled = kKnxMulticastEnabled; default_knx.main_group = static_cast(CONFIG_GATEWAY_KNX_MAIN_GROUP); + default_knx.dali_bus_id = static_cast(CONFIG_GATEWAY_KNX_DALI_BUS_ID); default_knx.udp_port = static_cast(CONFIG_GATEWAY_KNX_UDP_PORT); default_knx.multicast_address = CONFIG_GATEWAY_KNX_MULTICAST_ADDRESS; default_knx.ip_interface_individual_address = diff --git a/apps/gateway/sdkconfig b/apps/gateway/sdkconfig index 36466c6..73f4a0c 100644 --- a/apps/gateway/sdkconfig +++ b/apps/gateway/sdkconfig @@ -681,6 +681,8 @@ 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_DALI_BUS_ID=0 +# CONFIG_GATEWAY_KNX_DEBUG_DUMP_MEMORY is not set CONFIG_GATEWAY_KNX_TUNNEL_ENABLED=y CONFIG_GATEWAY_KNX_MULTICAST_ENABLED=y CONFIG_GATEWAY_KNX_UDP_PORT=3671 diff --git a/apps/gateway/sdkconfig.old b/apps/gateway/sdkconfig.old index 9491e43..fa5257f 100644 --- a/apps/gateway/sdkconfig.old +++ b/apps/gateway/sdkconfig.old @@ -676,10 +676,13 @@ 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_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_DALI_BUS_ID=0 +# CONFIG_GATEWAY_KNX_DEBUG_DUMP_MEMORY is not set CONFIG_GATEWAY_KNX_TUNNEL_ENABLED=y CONFIG_GATEWAY_KNX_MULTICAST_ENABLED=y CONFIG_GATEWAY_KNX_UDP_PORT=3671 diff --git a/components/dali_domain/include/dali_domain.hpp b/components/dali_domain/include/dali_domain.hpp index 98a4d04..a8e4d89 100644 --- a/components/dali_domain/include/dali_domain.hpp +++ b/components/dali_domain/include/dali_domain.hpp @@ -68,6 +68,7 @@ struct DaliChannelInfo { uint8_t gateway_id{0}; DaliPhyKind phy_kind{DaliPhyKind::kCustom}; std::string name; + std::optional native_bus_id; }; struct DaliRawFrame { diff --git a/components/dali_domain/src/dali_domain.cpp b/components/dali_domain/src/dali_domain.cpp index 5a894fd..7ea44a8 100644 --- a/components/dali_domain/src/dali_domain.cpp +++ b/components/dali_domain/src/dali_domain.cpp @@ -474,8 +474,12 @@ std::vector DaliDomainService::channelInfo() const { std::vector info; info.reserve(channels_.size()); for (const auto& channel : channels_) { - info.push_back(DaliChannelInfo{channel->config.channel_index, channel->config.gateway_id, - channel->phy_kind, channel->config.name}); + DaliChannelInfo item{channel->config.channel_index, channel->config.gateway_id, + channel->phy_kind, channel->config.name}; + if (channel->hardware_bus.has_value()) { + item.native_bus_id = channel->hardware_bus->bus_id; + } + info.push_back(std::move(item)); } return info; } diff --git a/components/gateway_bridge/include/gateway_bridge.hpp b/components/gateway_bridge/include/gateway_bridge.hpp index 73cc420..eaca6c1 100644 --- a/components/gateway_bridge/include/gateway_bridge.hpp +++ b/components/gateway_bridge/include/gateway_bridge.hpp @@ -74,6 +74,8 @@ class GatewayBridgeService { esp_err_t stopKnxEndpoint(ChannelRuntime* requested_runtime); DaliBridgeResult routeKnxGroupWrite(uint16_t group_address, const uint8_t* data, size_t len); + DaliBridgeResult routeKnxGroupObjectWrite(uint16_t group_object_number, + const uint8_t* data, size_t len); void handleDaliRawFrame(const DaliRawFrame& frame); void collectUsedRuntimeResources(uint8_t except_gateway_id, std::set* modbus_tcp_ports, diff --git a/components/gateway_bridge/src/gateway_bridge.cpp b/components/gateway_bridge/src/gateway_bridge.cpp index 6156532..dda67d1 100644 --- a/components/gateway_bridge/src/gateway_bridge.cpp +++ b/components/gateway_bridge/src/gateway_bridge.cpp @@ -1438,6 +1438,10 @@ struct GatewayBridgeService::ChannelRuntime { [this](uint16_t group_address, const uint8_t* data, size_t len) { return service.routeKnxGroupWrite(group_address, data, len); }); + knx_router->setGroupObjectWriteHandler( + [this](uint16_t group_object_number, const uint8_t* data, size_t len) { + return service.routeKnxGroupObjectWrite(group_object_number, data, len); + }); if (const auto active_knx = activeKnxConfigLocked(); active_knx.has_value()) { knx->setConfig(active_knx.value()); knx_router->setConfig(active_knx.value()); @@ -2058,12 +2062,13 @@ struct GatewayBridgeService::ChannelRuntime { const bool commissioning_only = !knx_config.has_value(); ESP_LOGI(kTag, "gateway=%u KNX/IP start config namespace=%s storedConfig=%d udp=%u tunnel=%d " - "multicast=%d multicastGroup=%s mainGroup=%u tpUart=%d tx=%d rx=%d nineBit=%d " + "multicast=%d multicastGroup=%s mainGroup=%u daliBus=%u tpUart=%d tx=%d rx=%d nineBit=%d " "individual=0x%04x", channel.gateway_id, openKnxNamespace().c_str(), !commissioning_only, static_cast(runtime_config.udp_port), runtime_config.tunnel_enabled, runtime_config.multicast_enabled, runtime_config.multicast_address.c_str(), - static_cast(runtime_config.main_group), runtime_config.tp_uart.uart_port, + static_cast(runtime_config.main_group), + static_cast(runtime_config.dali_bus_id), runtime_config.tp_uart.uart_port, runtime_config.tp_uart.tx_pin, runtime_config.tp_uart.rx_pin, runtime_config.tp_uart.nine_bit_mode, runtime_config.individual_address); @@ -3205,13 +3210,6 @@ struct GatewayBridgeService::ChannelRuntime { return std::nullopt; } GatewayKnxConfig config = service_config.default_knx_config.value(); - const uint8_t channel_index = channel.channel_index; - config.main_group = static_cast(std::min(31, config.main_group + channel_index)); - const uint16_t device = config.ip_interface_individual_address & 0x00ff; - if (device > 0 && device + channel_index <= 0x00ff) { - config.ip_interface_individual_address = static_cast( - (config.ip_interface_individual_address & 0xff00) | (device + channel_index)); - } return config; } @@ -3945,7 +3943,9 @@ GatewayBridgeService::ChannelRuntime* GatewayBridgeService::selectKnxEndpointRun } LockGuard guard(runtime->lock); const auto config = runtime->activeKnxConfigLocked(); - return config.has_value() && config->ip_router_enabled; + return config.has_value() && config->ip_router_enabled && + runtime->channel.native_bus_id.has_value() && + runtime->channel.native_bus_id.value() == config->dali_bus_id; }; if (eligible(knx_endpoint_runtime_)) { @@ -3963,7 +3963,13 @@ GatewayBridgeService::ChannelRuntime* GatewayBridgeService::selectKnxEndpointRun } knx_endpoint_runtime_ = selected; if (selected != nullptr) { - ESP_LOGI(kTag, "gateway=%u owns shared KNXnet/IP endpoint", selected->channel.gateway_id); + LockGuard guard(selected->lock); + const auto config = selected->activeKnxConfigLocked(); + ESP_LOGI(kTag, "gateway=%u owns shared KNXnet/IP endpoint daliBus=%u", + selected->channel.gateway_id, + config.has_value() ? static_cast(config->dali_bus_id) : 0U); + } else { + ESP_LOGW(kTag, "no native DALI channel matches the configured KNX DALI bus id"); } return selected; } @@ -4026,37 +4032,45 @@ esp_err_t GatewayBridgeService::stopKnxEndpoint(ChannelRuntime* requested_runtim DaliBridgeResult GatewayBridgeService::routeKnxGroupWrite(uint16_t group_address, 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->matchesGroupAddress(group_address)) { - matches.push_back(runtime.get()); - } - } - if (matches.empty()) { + ChannelRuntime* runtime = knx_endpoint_runtime_ != nullptr ? knx_endpoint_runtime_ + : selectKnxEndpointRuntime(); + if (runtime == nullptr) { DaliBridgeResult result; - result.error = "No DALI bridge mapping matched KNX group " + + result.error = "No DALI channel is selected for KNX group " + GatewayKnxGroupAddressString(group_address); return result; } - if (matches.size() > 1) { - DaliBridgeResult result; - result.error = "KNX group " + GatewayKnxGroupAddressString(group_address) + - " 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->matchesGroupAddress(group_address)) { DaliBridgeResult result; - result.error = "DALI bridge mapping changed before KNX group dispatch"; + result.error = "Selected DALI bus does not map KNX group " + + GatewayKnxGroupAddressString(group_address); return result; } return runtime->knx->handleGroupWrite(group_address, data, len); } +DaliBridgeResult GatewayBridgeService::routeKnxGroupObjectWrite(uint16_t group_object_number, + const uint8_t* data, size_t len) { + ChannelRuntime* runtime = knx_endpoint_runtime_ != nullptr ? knx_endpoint_runtime_ + : selectKnxEndpointRuntime(); + if (runtime == nullptr) { + DaliBridgeResult result; + result.error = "No DALI channel is selected for KNX group object " + + std::to_string(group_object_number); + return result; + } + LockGuard guard(runtime->lock); + if (runtime->knx == nullptr) { + DaliBridgeResult result; + result.error = "Selected DALI bus has no KNX bridge for group object " + + std::to_string(group_object_number); + return result; + } + return runtime->knx->handleGroupObjectWrite(group_object_number, data, len); +} + void GatewayBridgeService::handleDaliRawFrame(const DaliRawFrame& frame) { const auto update = DecodeDaliKnxStatusUpdate(frame); if (!update.has_value()) { diff --git a/components/gateway_knx/include/ets_device_runtime.h b/components/gateway_knx/include/ets_device_runtime.h index 06e7769..b5f3a38 100644 --- a/components/gateway_knx/include/ets_device_runtime.h +++ b/components/gateway_knx/include/ets_device_runtime.h @@ -13,6 +13,8 @@ #include #include +class GroupObject; + namespace gateway::openknx { class TpuartUartInterface; @@ -22,6 +24,8 @@ class EtsDeviceRuntime { using CemiFrameSender = std::function; using GroupWriteHandler = std::function; + using GroupObjectWriteHandler = std::function; using FunctionPropertyHandler = std::function* response)>; @@ -47,6 +51,7 @@ class EtsDeviceRuntime { void setFunctionPropertyHandlers(FunctionPropertyHandler command_handler, FunctionPropertyHandler state_handler); void setGroupWriteHandler(GroupWriteHandler handler); + void setGroupObjectWriteHandler(GroupObjectWriteHandler handler); void setBusFrameSender(CemiFrameSender sender); void setNetworkInterface(esp_netif_t* netif); bool hasTpUart() const; @@ -65,6 +70,7 @@ class EtsDeviceRuntime { static void EmitTunnelFrame(CemiFrame& frame, void* context); static void HandleSecureGroupWrite(uint16_t group_address, const uint8_t* data, uint8_t data_length, void* context); + static void HandleGroupObjectWrite(GroupObject& ko); static bool HandleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id, uint8_t length, uint8_t* data, uint8_t* result_data, uint8_t& result_length); @@ -75,6 +81,7 @@ class EtsDeviceRuntime { static bool DispatchFunctionProperty(FunctionPropertyHandler* handler, uint8_t object_index, uint8_t property_id, uint8_t length, uint8_t* data, uint8_t* result_data, uint8_t& result_length); + void installGroupObjectCallbacks(); bool shouldConsumeTunnelFrame(CemiFrame& frame) const; bool shouldConsumeBusFrame(CemiFrame& frame) const; @@ -85,8 +92,11 @@ class EtsDeviceRuntime { CemiFrameSender sender_; CemiFrameSender bus_frame_sender_; GroupWriteHandler group_write_handler_; + GroupObjectWriteHandler group_object_write_handler_; FunctionPropertyHandler command_handler_; FunctionPropertyHandler state_handler_; + bool suppress_group_object_write_callback_{false}; + uint16_t group_object_callback_count_{0}; }; } // namespace gateway::openknx diff --git a/components/gateway_knx/include/gateway_knx.hpp b/components/gateway_knx/include/gateway_knx.hpp index 85da596..2a89724 100644 --- a/components/gateway_knx/include/gateway_knx.hpp +++ b/components/gateway_knx/include/gateway_knx.hpp @@ -65,6 +65,7 @@ struct GatewayKnxConfig { bool ets_database_enabled{true}; GatewayKnxMappingMode mapping_mode{GatewayKnxMappingMode::kFormula}; uint8_t main_group{0}; + uint8_t dali_bus_id{0}; uint16_t udp_port{kGatewayKnxDefaultUdpPort}; std::string multicast_address{kGatewayKnxDefaultMulticastAddress}; uint16_t ip_interface_individual_address{0xff01}; @@ -147,6 +148,8 @@ class GatewayKnxBridge { bool matchesGroupAddress(uint16_t group_address) const; DaliBridgeResult handleGroupWrite(uint16_t group_address, const uint8_t* data, size_t len); + DaliBridgeResult handleGroupObjectWrite(uint16_t group_object_number, + const uint8_t* data, size_t len); bool handleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len, std::vector* response); @@ -200,6 +203,9 @@ class GatewayKnxTpIpRouter { using GroupWriteHandler = std::function; + using GroupObjectWriteHandler = std::function; GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, std::string openknx_namespace = "openknx"); @@ -208,6 +214,7 @@ class GatewayKnxTpIpRouter { void setConfig(const GatewayKnxConfig& config); void setCommissioningOnly(bool enabled); void setGroupWriteHandler(GroupWriteHandler handler); + void setGroupObjectWriteHandler(GroupObjectWriteHandler handler); const GatewayKnxConfig& config() const; bool tpUartOnline() const; bool programmingMode(); @@ -338,6 +345,7 @@ class GatewayKnxTpIpRouter { GatewayKnxBridge& bridge_; GroupWriteHandler group_write_handler_; + GroupObjectWriteHandler group_object_write_handler_; std::string openknx_namespace_; GatewayKnxConfig config_; std::unique_ptr ets_device_; diff --git a/components/gateway_knx/src/ets_device_runtime.cpp b/components/gateway_knx/src/ets_device_runtime.cpp index 5b9b6df..51a8147 100644 --- a/components/gateway_knx/src/ets_device_runtime.cpp +++ b/components/gateway_knx/src/ets_device_runtime.cpp @@ -4,6 +4,7 @@ #include "esp_log.h" #include "knx/cemi_server.h" +#include "knx/group_object.h" #include "knx/secure_application_layer.h" #include "knx/property.h" #include "tpuart_uart_interface.h" @@ -19,6 +20,7 @@ namespace gateway::openknx { namespace { thread_local EtsDeviceRuntime* active_function_property_runtime = nullptr; +EtsDeviceRuntime* active_group_object_runtime = nullptr; class ActiveFunctionPropertyRuntimeScope { public: @@ -121,6 +123,7 @@ EtsDeviceRuntime::EtsDeviceRuntime(std::string nvs_namespace, } ESP_LOGI("gateway_knx", "OpenKNX loading memory namespace=%s", nvs_namespace_.c_str()); device_.readMemory(); + installGroupObjectCallbacks(); if (!IsUsableIndividualAddress(device_.deviceObject().individualAddress()) && IsUsableIndividualAddress(fallback_individual_address)) { device_.deviceObject().individualAddress(fallback_individual_address); @@ -149,6 +152,19 @@ EtsDeviceRuntime::~EtsDeviceRuntime() { #ifdef USE_DATASECURE device_.secureGroupWriteCallback(nullptr, nullptr); #endif +#ifdef SMALL_GROUPOBJECT + if (active_group_object_runtime == this) { + GroupObject::classCallback(GroupObjectUpdatedHandler{}); + } +#else + auto& table = device_.groupObjectTable(); + for (uint16_t asap = 1; asap <= table.entryCount(); ++asap) { + table.get(asap).callback(GroupObjectUpdatedHandler{}); + } +#endif + if (active_group_object_runtime == this) { + active_group_object_runtime = nullptr; + } device_.functionPropertyCallback(nullptr); device_.functionPropertyStateCallback(nullptr); if (auto* server = device_.getCemiServer()) { @@ -223,6 +239,11 @@ void EtsDeviceRuntime::setGroupWriteHandler(GroupWriteHandler handler) { group_write_handler_ = std::move(handler); } +void EtsDeviceRuntime::setGroupObjectWriteHandler(GroupObjectWriteHandler handler) { + group_object_write_handler_ = std::move(handler); + installGroupObjectCallbacks(); +} + void EtsDeviceRuntime::setBusFrameSender(CemiFrameSender sender) { bus_frame_sender_ = std::move(sender); } @@ -272,11 +293,20 @@ bool EtsDeviceRuntime::handleTunnelFrame(const uint8_t* data, size_t len, if (!consumed) { return false; } + const bool suppress_group_object_route = + frame.messageCode() == L_data_req && frame.addressType() == GroupAddress && + frame.apdu().type() == GroupValueWrite; + const bool previous_suppression = suppress_group_object_write_callback_; + if (suppress_group_object_route) { + suppress_group_object_write_callback_ = true; + } sender_ = std::move(sender); ActiveFunctionPropertyRuntimeScope callback_scope(this); server->frameReceived(frame); loop(); sender_ = nullptr; + suppress_group_object_write_callback_ = previous_suppression; + installGroupObjectCallbacks(); return consumed; } @@ -293,6 +323,7 @@ bool EtsDeviceRuntime::handleBusFrame(const uint8_t* data, size_t len) { } data_link_layer->externalFrameReceived(frame); loop(); + installGroupObjectCallbacks(); return consumed; } @@ -314,10 +345,13 @@ bool EtsDeviceRuntime::emitGroupValue(uint16_t group_object_number, const uint8_ } else { std::copy_n(data, len, group_object.valueRef()); } + const bool previous_suppression = suppress_group_object_write_callback_; + suppress_group_object_write_callback_ = true; sender_ = std::move(sender); group_object.objectWritten(); loop(); sender_ = nullptr; + suppress_group_object_write_callback_ = previous_suppression; return true; } @@ -349,10 +383,36 @@ void EtsDeviceRuntime::EmitTunnelFrame(CemiFrame& frame, void* context) { void EtsDeviceRuntime::HandleSecureGroupWrite(uint16_t group_address, const uint8_t* data, uint8_t data_length, void* context) { auto* self = static_cast(context); - if (self == nullptr || !self->group_write_handler_) { + if (self == nullptr) { return; } - self->group_write_handler_(group_address, data, data_length); + if (self->group_object_write_handler_) { + return; + } + if (self->group_write_handler_) { + self->group_write_handler_(group_address, data, data_length); + } +} + +void EtsDeviceRuntime::HandleGroupObjectWrite(GroupObject& ko) { + auto* self = active_group_object_runtime; + if (self == nullptr || self->suppress_group_object_write_callback_ || + !self->group_object_write_handler_) { + return; + } + const size_t value_size = ko.valueSize(); + const uint8_t* value = ko.valueRef(); + if (value == nullptr || value_size == 0) { + ESP_LOGW("gateway_knx", "OpenKNX group-object callback ignored namespace=%s ko=%u len=%u", + self->nvs_namespace_.c_str(), static_cast(ko.asap()), + static_cast(value_size)); + return; + } + const std::string value_hex = HexBytesString(value, value_size); + ESP_LOGI("gateway_knx", "OpenKNX group-object callback namespace=%s ko=%u len=%u value=%s", + self->nvs_namespace_.c_str(), static_cast(ko.asap()), + static_cast(value_size), value_hex.c_str()); + self->group_object_write_handler_(ko.asap(), value, value_size); } bool EtsDeviceRuntime::HandleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id, @@ -395,6 +455,24 @@ bool EtsDeviceRuntime::DispatchFunctionProperty(FunctionPropertyHandler* handler return true; } +void EtsDeviceRuntime::installGroupObjectCallbacks() { + active_group_object_runtime = this; + auto& table = device_.groupObjectTable(); + const uint16_t count = table.entryCount(); +#ifdef SMALL_GROUPOBJECT + GroupObject::classCallback(&EtsDeviceRuntime::HandleGroupObjectWrite); +#else + for (uint16_t asap = 1; asap <= count; ++asap) { + table.get(asap).callback(&EtsDeviceRuntime::HandleGroupObjectWrite); + } +#endif + if (count != group_object_callback_count_) { + ESP_LOGI("gateway_knx", "OpenKNX group-object callbacks namespace=%s count=%u", + nvs_namespace_.c_str(), static_cast(count)); + group_object_callback_count_ = count; + } +} + uint16_t EtsDeviceRuntime::DefaultTunnelClientAddress(uint16_t individual_address) { if (!IsUsableIndividualAddress(individual_address)) { return 0x1101; diff --git a/components/gateway_knx/src/gateway_knx.cpp b/components/gateway_knx/src/gateway_knx.cpp index 4e8dced..d50f66c 100644 --- a/components/gateway_knx/src/gateway_knx.cpp +++ b/components/gateway_knx/src/gateway_knx.cpp @@ -610,6 +610,23 @@ std::optional MetadataInt(const DaliBridgeResult& result, const std::string return getObjectInt(result.metadata, key); } +std::string HexBytes(const uint8_t* data, size_t len) { + if (data == nullptr || len == 0) { + return {}; + } + std::string out; + out.reserve(len * 3); + char buffer[4] = {0}; + for (size_t index = 0; index < len; ++index) { + std::snprintf(buffer, sizeof(buffer), "%02X", data[index]); + out += buffer; + if (index + 1 < len) { + out.push_back(' '); + } + } + return out; +} + DaliBridgeRequest RequestForTarget(uint16_t group_address, const GatewayKnxDaliTarget& target, BridgeOperation operation) { @@ -642,6 +659,17 @@ DaliBridgeResult ErrorResult(uint16_t group_address, const char* message) { return result; } +DaliBridgeResult IgnoredResult(uint16_t group_address, uint16_t group_object_number, + const char* reason) { + DaliBridgeResult result; + result.sequence = "knx-" + GatewayKnxGroupAddressString(group_address); + result.ok = true; + result.metadata["ignored"] = true; + result.metadata["groupObjectNumber"] = static_cast(group_object_number); + result.metadata["reason"] = reason == nullptr ? "ignored" : reason; + return result; +} + bool SendAll(int sock, const uint8_t* data, size_t len, const sockaddr_in& remote) { return sendto(sock, data, len, 0, reinterpret_cast(&remote), sizeof(remote)) == static_cast(len); @@ -716,6 +744,11 @@ std::optional GatewayKnxConfigFromValue(const DaliValue* value config.main_group = static_cast( std::clamp(ObjectIntAny(object, {"mainGroup", "main_group"}).value_or(config.main_group), 0, 31)); + config.dali_bus_id = static_cast(std::clamp( + ObjectIntAny(object, {"daliBusId", "dali_bus_id", "targetDaliBusId", + "target_dali_bus_id"}) + .value_or(config.dali_bus_id), + 0, 15)); config.udp_port = static_cast(std::clamp( ObjectIntAny(object, {"udpPort", "port", "udp_port"}).value_or(config.udp_port), 1, 65535)); @@ -790,6 +823,7 @@ DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config) { out["etsDatabaseEnabled"] = config.ets_database_enabled; out["mappingMode"] = GatewayKnxMappingModeToString(config.mapping_mode); out["mainGroup"] = static_cast(config.main_group); + out["daliBusId"] = static_cast(config.dali_bus_id); out["udpPort"] = static_cast(config.udp_port); out["multicastAddress"] = config.multicast_address; out["ipInterfaceIndividualAddress"] = @@ -1186,6 +1220,42 @@ DaliBridgeResult GatewayKnxBridge::handleGroupWrite(uint16_t group_address, cons return executeForDecodedWrite(group_address, data_type.value(), target.value(), data, len); } +DaliBridgeResult GatewayKnxBridge::handleGroupObjectWrite(uint16_t group_object_number, + const uint8_t* data, size_t len) { + const uint16_t group_address = GwReg1GroupAddressForObject(config_.main_group, + group_object_number); + const std::string payload = HexBytes(data, len); + ESP_LOGI(kTag, "OpenKNX KO write ko=%u derivedGa=%s len=%u payload=%s", + static_cast(group_object_number), + GatewayKnxGroupAddressString(group_address).c_str(), static_cast(len), + payload.c_str()); + if (!config_.dali_router_enabled) { + return ErrorResult(group_address, "KNX to DALI router disabled"); + } + const auto binding = GwReg1BindingForObject(config_.main_group, group_object_number); + if (!binding.has_value()) { + ESP_LOGW(kTag, "OpenKNX KO write ignored ko=%u: unsupported GW-REG1 object", + static_cast(group_object_number)); + return IgnoredResult(group_address, group_object_number, + "unsupported GW-REG1 group object"); + } + DaliBridgeResult result = executeForDecodedWrite(binding->group_address, binding->data_type, + binding->target, data, len); + result.metadata["source"] = "openknx_group_object"; + result.metadata["groupObjectNumber"] = static_cast(group_object_number); + result.metadata["objectRole"] = binding->object_role; + if (result.ok) { + ESP_LOGI(kTag, "OpenKNX KO write routed ko=%u role=%s target=%s", + static_cast(group_object_number), binding->object_role.c_str(), + TargetName(binding->target).c_str()); + } else { + ESP_LOGW(kTag, "OpenKNX KO write failed ko=%u role=%s error=%s", + static_cast(group_object_number), binding->object_role.c_str(), + result.error.c_str()); + } + return result; +} + bool GatewayKnxBridge::handleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len, std::vector* response) { @@ -1719,6 +1789,10 @@ void GatewayKnxTpIpRouter::setGroupWriteHandler(GroupWriteHandler handler) { group_write_handler_ = std::move(handler); } +void GatewayKnxTpIpRouter::setGroupObjectWriteHandler(GroupObjectWriteHandler handler) { + group_object_write_handler_ = std::move(handler); +} + const GatewayKnxConfig& GatewayKnxTpIpRouter::config() const { return config_; } bool GatewayKnxTpIpRouter::tpUartOnline() const { return tp_uart_online_; } @@ -1908,8 +1982,24 @@ esp_err_t GatewayKnxTpIpRouter::initializeRuntime() { ESP_LOGD(kTag, "secure KNX group write not routed to DALI: %s", result.error.c_str()); } }); + ets_device_->setGroupObjectWriteHandler( + [this](uint16_t group_object_number, const uint8_t* data, size_t len) { + const DaliBridgeResult result = group_object_write_handler_ + ? group_object_write_handler_(group_object_number, + data, len) + : bridge_.handleGroupObjectWrite(group_object_number, + data, len); + const bool ignored = getObjectBool(result.metadata, "ignored").value_or(false); + if (ignored) { + const auto reason = getObjectString(result.metadata, "reason").value_or("ignored"); + ESP_LOGW(kTag, "OpenKNX group object %u accepted by ETS but ignored: %s", + static_cast(group_object_number), reason.c_str()); + } else if (!result.ok && !result.error.empty()) { + ESP_LOGW(kTag, "OpenKNX group object %u not routed to DALI: %s", + static_cast(group_object_number), 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); }); diff --git a/knx b/knx index 23b0cdd..82f22cf 160000 --- a/knx +++ b/knx @@ -1 +1 @@ -Subproject commit 23b0cddf24b6ea70f304361ca1064eb6351ee2ba +Subproject commit 82f22cf5715de98e3f89512a641da28b90f628ff