feat(gateway): add DALI bus ID configuration and enhance group object write handling

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-16 03:31:12 +08:00
parent e79223c87e
commit 277379abd7
13 changed files with 288 additions and 37 deletions
+18
View File
@@ -713,6 +713,24 @@ config GATEWAY_KNX_MAIN_GROUP
Main group used by the built-in KNX to DALI router. Middle groups select 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. 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 config GATEWAY_KNX_TUNNEL_ENABLED
bool "Enable KNXnet/IP tunneling mode" bool "Enable KNXnet/IP tunneling mode"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED depends on GATEWAY_KNX_BRIDGE_SUPPORTED
+21
View File
@@ -219,6 +219,10 @@
#define CONFIG_GATEWAY_KNX_MAIN_GROUP 0 #define CONFIG_GATEWAY_KNX_MAIN_GROUP 0
#endif #endif
#ifndef CONFIG_GATEWAY_KNX_DALI_BUS_ID
#define CONFIG_GATEWAY_KNX_DALI_BUS_ID 0
#endif
#ifndef CONFIG_GATEWAY_KNX_UDP_PORT #ifndef CONFIG_GATEWAY_KNX_UDP_PORT
#define CONFIG_GATEWAY_KNX_UDP_PORT 3671 #define CONFIG_GATEWAY_KNX_UDP_PORT 3671
#endif #endif
@@ -606,6 +610,22 @@ bool ValidateChannelBindings() {
if (kKnxBridgeSupported) { if (kKnxBridgeSupported) {
const int knx_uart = CONFIG_GATEWAY_KNX_TP_UART_PORT; const int knx_uart = CONFIG_GATEWAY_KNX_TP_UART_PORT;
if (kKnxBridgeStartupEnabled) {
const uint8_t knx_dali_bus_id = static_cast<uint8_t>(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) { if (knx_uart >= 0 && k485ControlEnabled && knx_uart == 0) {
ESP_LOGE(kTag, "KNX TP UART0 conflicts with the UART0 control bridge"); ESP_LOGE(kTag, "KNX TP UART0 conflicts with the UART0 control bridge");
return false; return false;
@@ -864,6 +884,7 @@ extern "C" void app_main(void) {
default_knx.tunnel_enabled = kKnxTunnelEnabled; default_knx.tunnel_enabled = kKnxTunnelEnabled;
default_knx.multicast_enabled = kKnxMulticastEnabled; default_knx.multicast_enabled = kKnxMulticastEnabled;
default_knx.main_group = static_cast<uint8_t>(CONFIG_GATEWAY_KNX_MAIN_GROUP); default_knx.main_group = static_cast<uint8_t>(CONFIG_GATEWAY_KNX_MAIN_GROUP);
default_knx.dali_bus_id = static_cast<uint8_t>(CONFIG_GATEWAY_KNX_DALI_BUS_ID);
default_knx.udp_port = static_cast<uint16_t>(CONFIG_GATEWAY_KNX_UDP_PORT); default_knx.udp_port = static_cast<uint16_t>(CONFIG_GATEWAY_KNX_UDP_PORT);
default_knx.multicast_address = CONFIG_GATEWAY_KNX_MULTICAST_ADDRESS; default_knx.multicast_address = CONFIG_GATEWAY_KNX_MULTICAST_ADDRESS;
default_knx.ip_interface_individual_address = default_knx.ip_interface_individual_address =
+2
View File
@@ -681,6 +681,8 @@ CONFIG_GATEWAY_KNX_OEM_HARDWARE_ID=0xa401
CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER=0x0001 CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER=0x0001
CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION=0x08 CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION=0x08
CONFIG_GATEWAY_KNX_MAIN_GROUP=0 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_TUNNEL_ENABLED=y
CONFIG_GATEWAY_KNX_MULTICAST_ENABLED=y CONFIG_GATEWAY_KNX_MULTICAST_ENABLED=y
CONFIG_GATEWAY_KNX_UDP_PORT=3671 CONFIG_GATEWAY_KNX_UDP_PORT=3671
+5 -2
View File
@@ -676,10 +676,13 @@ CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED=y
# CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED is not set # CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED is not set
# CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS is not set # CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS is not set
CONFIG_GATEWAY_KNX_SECURITY_PLAIN_NVS=y CONFIG_GATEWAY_KNX_SECURITY_PLAIN_NVS=y
CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID=0x00fa CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID=0x01e5
CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER=0xa401 CONFIG_GATEWAY_KNX_OEM_HARDWARE_ID=0xa401
CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER=0x0001
CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION=0x08 CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION=0x08
CONFIG_GATEWAY_KNX_MAIN_GROUP=0 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_TUNNEL_ENABLED=y
CONFIG_GATEWAY_KNX_MULTICAST_ENABLED=y CONFIG_GATEWAY_KNX_MULTICAST_ENABLED=y
CONFIG_GATEWAY_KNX_UDP_PORT=3671 CONFIG_GATEWAY_KNX_UDP_PORT=3671
@@ -68,6 +68,7 @@ struct DaliChannelInfo {
uint8_t gateway_id{0}; uint8_t gateway_id{0};
DaliPhyKind phy_kind{DaliPhyKind::kCustom}; DaliPhyKind phy_kind{DaliPhyKind::kCustom};
std::string name; std::string name;
std::optional<uint8_t> native_bus_id;
}; };
struct DaliRawFrame { struct DaliRawFrame {
+6 -2
View File
@@ -474,8 +474,12 @@ std::vector<DaliChannelInfo> DaliDomainService::channelInfo() const {
std::vector<DaliChannelInfo> info; std::vector<DaliChannelInfo> info;
info.reserve(channels_.size()); info.reserve(channels_.size());
for (const auto& channel : channels_) { for (const auto& channel : channels_) {
info.push_back(DaliChannelInfo{channel->config.channel_index, channel->config.gateway_id, DaliChannelInfo item{channel->config.channel_index, channel->config.gateway_id,
channel->phy_kind, channel->config.name}); 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; return info;
} }
@@ -74,6 +74,8 @@ class GatewayBridgeService {
esp_err_t stopKnxEndpoint(ChannelRuntime* requested_runtime); esp_err_t stopKnxEndpoint(ChannelRuntime* requested_runtime);
DaliBridgeResult routeKnxGroupWrite(uint16_t group_address, const uint8_t* data, DaliBridgeResult routeKnxGroupWrite(uint16_t group_address, const uint8_t* data,
size_t len); size_t len);
DaliBridgeResult routeKnxGroupObjectWrite(uint16_t group_object_number,
const uint8_t* data, size_t len);
void handleDaliRawFrame(const DaliRawFrame& frame); void handleDaliRawFrame(const DaliRawFrame& frame);
void collectUsedRuntimeResources(uint8_t except_gateway_id, void collectUsedRuntimeResources(uint8_t except_gateway_id,
std::set<uint16_t>* modbus_tcp_ports, std::set<uint16_t>* modbus_tcp_ports,
@@ -1438,6 +1438,10 @@ struct GatewayBridgeService::ChannelRuntime {
[this](uint16_t group_address, const uint8_t* data, size_t len) { [this](uint16_t group_address, const uint8_t* data, size_t len) {
return service.routeKnxGroupWrite(group_address, data, 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()) { if (const auto active_knx = activeKnxConfigLocked(); active_knx.has_value()) {
knx->setConfig(active_knx.value()); knx->setConfig(active_knx.value());
knx_router->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(); const bool commissioning_only = !knx_config.has_value();
ESP_LOGI(kTag, ESP_LOGI(kTag,
"gateway=%u KNX/IP start config namespace=%s storedConfig=%d udp=%u tunnel=%d " "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", "individual=0x%04x",
channel.gateway_id, openKnxNamespace().c_str(), !commissioning_only, channel.gateway_id, openKnxNamespace().c_str(), !commissioning_only,
static_cast<unsigned>(runtime_config.udp_port), runtime_config.tunnel_enabled, static_cast<unsigned>(runtime_config.udp_port), runtime_config.tunnel_enabled,
runtime_config.multicast_enabled, runtime_config.multicast_address.c_str(), runtime_config.multicast_enabled, runtime_config.multicast_address.c_str(),
static_cast<unsigned>(runtime_config.main_group), runtime_config.tp_uart.uart_port, static_cast<unsigned>(runtime_config.main_group),
static_cast<unsigned>(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.tx_pin, runtime_config.tp_uart.rx_pin,
runtime_config.tp_uart.nine_bit_mode, runtime_config.tp_uart.nine_bit_mode,
runtime_config.individual_address); runtime_config.individual_address);
@@ -3205,13 +3210,6 @@ struct GatewayBridgeService::ChannelRuntime {
return std::nullopt; return std::nullopt;
} }
GatewayKnxConfig config = service_config.default_knx_config.value(); GatewayKnxConfig config = service_config.default_knx_config.value();
const uint8_t channel_index = channel.channel_index;
config.main_group = static_cast<uint8_t>(std::min<int>(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<uint16_t>(
(config.ip_interface_individual_address & 0xff00) | (device + channel_index));
}
return config; return config;
} }
@@ -3945,7 +3943,9 @@ GatewayBridgeService::ChannelRuntime* GatewayBridgeService::selectKnxEndpointRun
} }
LockGuard guard(runtime->lock); LockGuard guard(runtime->lock);
const auto config = runtime->activeKnxConfigLocked(); 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_)) { if (eligible(knx_endpoint_runtime_)) {
@@ -3963,7 +3963,13 @@ GatewayBridgeService::ChannelRuntime* GatewayBridgeService::selectKnxEndpointRun
} }
knx_endpoint_runtime_ = selected; knx_endpoint_runtime_ = selected;
if (selected != nullptr) { 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<unsigned>(config->dali_bus_id) : 0U);
} else {
ESP_LOGW(kTag, "no native DALI channel matches the configured KNX DALI bus id");
} }
return selected; return selected;
} }
@@ -4026,37 +4032,45 @@ esp_err_t GatewayBridgeService::stopKnxEndpoint(ChannelRuntime* requested_runtim
DaliBridgeResult GatewayBridgeService::routeKnxGroupWrite(uint16_t group_address, DaliBridgeResult GatewayBridgeService::routeKnxGroupWrite(uint16_t group_address,
const uint8_t* data, size_t len) { const uint8_t* data, size_t len) {
std::vector<ChannelRuntime*> matches; ChannelRuntime* runtime = knx_endpoint_runtime_ != nullptr ? knx_endpoint_runtime_
for (const auto& runtime : runtimes_) { : selectKnxEndpointRuntime();
LockGuard guard(runtime->lock); if (runtime == nullptr) {
if (runtime->knx != nullptr && runtime->knx->matchesGroupAddress(group_address)) {
matches.push_back(runtime.get());
}
}
if (matches.empty()) {
DaliBridgeResult result; DaliBridgeResult result;
result.error = "No DALI bridge mapping matched KNX group " + result.error = "No DALI channel is selected for KNX group " +
GatewayKnxGroupAddressString(group_address); GatewayKnxGroupAddressString(group_address);
return result; 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); LockGuard guard(runtime->lock);
if (runtime->knx == nullptr || !runtime->knx->matchesGroupAddress(group_address)) { if (runtime->knx == nullptr || !runtime->knx->matchesGroupAddress(group_address)) {
DaliBridgeResult result; 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 result;
} }
return runtime->knx->handleGroupWrite(group_address, data, len); 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) { void GatewayBridgeService::handleDaliRawFrame(const DaliRawFrame& frame) {
const auto update = DecodeDaliKnxStatusUpdate(frame); const auto update = DecodeDaliKnxStatusUpdate(frame);
if (!update.has_value()) { if (!update.has_value()) {
@@ -13,6 +13,8 @@
#include <string> #include <string>
#include <vector> #include <vector>
class GroupObject;
namespace gateway::openknx { namespace gateway::openknx {
class TpuartUartInterface; class TpuartUartInterface;
@@ -22,6 +24,8 @@ class EtsDeviceRuntime {
using CemiFrameSender = std::function<void(const uint8_t* data, size_t len)>; using CemiFrameSender = std::function<void(const uint8_t* data, size_t len)>;
using GroupWriteHandler = std::function<void(uint16_t group_address, const uint8_t* data, using GroupWriteHandler = std::function<void(uint16_t group_address, const uint8_t* data,
size_t len)>; size_t len)>;
using GroupObjectWriteHandler = std::function<void(uint16_t group_object_number,
const uint8_t* data, size_t len)>;
using FunctionPropertyHandler = std::function<bool(uint8_t object_index, uint8_t property_id, using FunctionPropertyHandler = std::function<bool(uint8_t object_index, uint8_t property_id,
const uint8_t* data, size_t len, const uint8_t* data, size_t len,
std::vector<uint8_t>* response)>; std::vector<uint8_t>* response)>;
@@ -47,6 +51,7 @@ class EtsDeviceRuntime {
void setFunctionPropertyHandlers(FunctionPropertyHandler command_handler, void setFunctionPropertyHandlers(FunctionPropertyHandler command_handler,
FunctionPropertyHandler state_handler); FunctionPropertyHandler state_handler);
void setGroupWriteHandler(GroupWriteHandler handler); void setGroupWriteHandler(GroupWriteHandler handler);
void setGroupObjectWriteHandler(GroupObjectWriteHandler handler);
void setBusFrameSender(CemiFrameSender sender); void setBusFrameSender(CemiFrameSender sender);
void setNetworkInterface(esp_netif_t* netif); void setNetworkInterface(esp_netif_t* netif);
bool hasTpUart() const; bool hasTpUart() const;
@@ -65,6 +70,7 @@ class EtsDeviceRuntime {
static void EmitTunnelFrame(CemiFrame& frame, void* context); static void EmitTunnelFrame(CemiFrame& frame, void* context);
static void HandleSecureGroupWrite(uint16_t group_address, const uint8_t* data, static void HandleSecureGroupWrite(uint16_t group_address, const uint8_t* data,
uint8_t data_length, void* context); uint8_t data_length, void* context);
static void HandleGroupObjectWrite(GroupObject& ko);
static bool HandleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id, static bool HandleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id,
uint8_t length, uint8_t* data, uint8_t length, uint8_t* data,
uint8_t* result_data, uint8_t& result_length); uint8_t* result_data, uint8_t& result_length);
@@ -75,6 +81,7 @@ class EtsDeviceRuntime {
static bool DispatchFunctionProperty(FunctionPropertyHandler* handler, uint8_t object_index, static bool DispatchFunctionProperty(FunctionPropertyHandler* handler, uint8_t object_index,
uint8_t property_id, uint8_t length, uint8_t* data, uint8_t property_id, uint8_t length, uint8_t* data,
uint8_t* result_data, uint8_t& result_length); uint8_t* result_data, uint8_t& result_length);
void installGroupObjectCallbacks();
bool shouldConsumeTunnelFrame(CemiFrame& frame) const; bool shouldConsumeTunnelFrame(CemiFrame& frame) const;
bool shouldConsumeBusFrame(CemiFrame& frame) const; bool shouldConsumeBusFrame(CemiFrame& frame) const;
@@ -85,8 +92,11 @@ class EtsDeviceRuntime {
CemiFrameSender sender_; CemiFrameSender sender_;
CemiFrameSender bus_frame_sender_; CemiFrameSender bus_frame_sender_;
GroupWriteHandler group_write_handler_; GroupWriteHandler group_write_handler_;
GroupObjectWriteHandler group_object_write_handler_;
FunctionPropertyHandler command_handler_; FunctionPropertyHandler command_handler_;
FunctionPropertyHandler state_handler_; FunctionPropertyHandler state_handler_;
bool suppress_group_object_write_callback_{false};
uint16_t group_object_callback_count_{0};
}; };
} // namespace gateway::openknx } // namespace gateway::openknx
@@ -65,6 +65,7 @@ struct GatewayKnxConfig {
bool ets_database_enabled{true}; bool ets_database_enabled{true};
GatewayKnxMappingMode mapping_mode{GatewayKnxMappingMode::kFormula}; GatewayKnxMappingMode mapping_mode{GatewayKnxMappingMode::kFormula};
uint8_t main_group{0}; uint8_t main_group{0};
uint8_t dali_bus_id{0};
uint16_t udp_port{kGatewayKnxDefaultUdpPort}; uint16_t udp_port{kGatewayKnxDefaultUdpPort};
std::string multicast_address{kGatewayKnxDefaultMulticastAddress}; std::string multicast_address{kGatewayKnxDefaultMulticastAddress};
uint16_t ip_interface_individual_address{0xff01}; uint16_t ip_interface_individual_address{0xff01};
@@ -147,6 +148,8 @@ class GatewayKnxBridge {
bool matchesGroupAddress(uint16_t group_address) const; bool matchesGroupAddress(uint16_t group_address) const;
DaliBridgeResult handleGroupWrite(uint16_t group_address, const uint8_t* data, DaliBridgeResult handleGroupWrite(uint16_t group_address, const uint8_t* data,
size_t len); 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, bool handleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id,
const uint8_t* data, size_t len, const uint8_t* data, size_t len,
std::vector<uint8_t>* response); std::vector<uint8_t>* response);
@@ -200,6 +203,9 @@ class GatewayKnxTpIpRouter {
using GroupWriteHandler = std::function<DaliBridgeResult(uint16_t group_address, using GroupWriteHandler = std::function<DaliBridgeResult(uint16_t group_address,
const uint8_t* data, const uint8_t* data,
size_t len)>; size_t len)>;
using GroupObjectWriteHandler = std::function<DaliBridgeResult(uint16_t group_object_number,
const uint8_t* data,
size_t len)>;
GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, GatewayKnxTpIpRouter(GatewayKnxBridge& bridge,
std::string openknx_namespace = "openknx"); std::string openknx_namespace = "openknx");
@@ -208,6 +214,7 @@ class GatewayKnxTpIpRouter {
void setConfig(const GatewayKnxConfig& config); void setConfig(const GatewayKnxConfig& config);
void setCommissioningOnly(bool enabled); void setCommissioningOnly(bool enabled);
void setGroupWriteHandler(GroupWriteHandler handler); void setGroupWriteHandler(GroupWriteHandler handler);
void setGroupObjectWriteHandler(GroupObjectWriteHandler handler);
const GatewayKnxConfig& config() const; const GatewayKnxConfig& config() const;
bool tpUartOnline() const; bool tpUartOnline() const;
bool programmingMode(); bool programmingMode();
@@ -338,6 +345,7 @@ class GatewayKnxTpIpRouter {
GatewayKnxBridge& bridge_; GatewayKnxBridge& bridge_;
GroupWriteHandler group_write_handler_; GroupWriteHandler group_write_handler_;
GroupObjectWriteHandler group_object_write_handler_;
std::string openknx_namespace_; std::string openknx_namespace_;
GatewayKnxConfig config_; GatewayKnxConfig config_;
std::unique_ptr<openknx::EtsDeviceRuntime> ets_device_; std::unique_ptr<openknx::EtsDeviceRuntime> ets_device_;
@@ -4,6 +4,7 @@
#include "esp_log.h" #include "esp_log.h"
#include "knx/cemi_server.h" #include "knx/cemi_server.h"
#include "knx/group_object.h"
#include "knx/secure_application_layer.h" #include "knx/secure_application_layer.h"
#include "knx/property.h" #include "knx/property.h"
#include "tpuart_uart_interface.h" #include "tpuart_uart_interface.h"
@@ -19,6 +20,7 @@ namespace gateway::openknx {
namespace { namespace {
thread_local EtsDeviceRuntime* active_function_property_runtime = nullptr; thread_local EtsDeviceRuntime* active_function_property_runtime = nullptr;
EtsDeviceRuntime* active_group_object_runtime = nullptr;
class ActiveFunctionPropertyRuntimeScope { class ActiveFunctionPropertyRuntimeScope {
public: public:
@@ -121,6 +123,7 @@ EtsDeviceRuntime::EtsDeviceRuntime(std::string nvs_namespace,
} }
ESP_LOGI("gateway_knx", "OpenKNX loading memory namespace=%s", nvs_namespace_.c_str()); ESP_LOGI("gateway_knx", "OpenKNX loading memory namespace=%s", nvs_namespace_.c_str());
device_.readMemory(); device_.readMemory();
installGroupObjectCallbacks();
if (!IsUsableIndividualAddress(device_.deviceObject().individualAddress()) && if (!IsUsableIndividualAddress(device_.deviceObject().individualAddress()) &&
IsUsableIndividualAddress(fallback_individual_address)) { IsUsableIndividualAddress(fallback_individual_address)) {
device_.deviceObject().individualAddress(fallback_individual_address); device_.deviceObject().individualAddress(fallback_individual_address);
@@ -149,6 +152,19 @@ EtsDeviceRuntime::~EtsDeviceRuntime() {
#ifdef USE_DATASECURE #ifdef USE_DATASECURE
device_.secureGroupWriteCallback(nullptr, nullptr); device_.secureGroupWriteCallback(nullptr, nullptr);
#endif #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_.functionPropertyCallback(nullptr);
device_.functionPropertyStateCallback(nullptr); device_.functionPropertyStateCallback(nullptr);
if (auto* server = device_.getCemiServer()) { if (auto* server = device_.getCemiServer()) {
@@ -223,6 +239,11 @@ void EtsDeviceRuntime::setGroupWriteHandler(GroupWriteHandler handler) {
group_write_handler_ = std::move(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) { void EtsDeviceRuntime::setBusFrameSender(CemiFrameSender sender) {
bus_frame_sender_ = std::move(sender); bus_frame_sender_ = std::move(sender);
} }
@@ -272,11 +293,20 @@ bool EtsDeviceRuntime::handleTunnelFrame(const uint8_t* data, size_t len,
if (!consumed) { if (!consumed) {
return false; 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); sender_ = std::move(sender);
ActiveFunctionPropertyRuntimeScope callback_scope(this); ActiveFunctionPropertyRuntimeScope callback_scope(this);
server->frameReceived(frame); server->frameReceived(frame);
loop(); loop();
sender_ = nullptr; sender_ = nullptr;
suppress_group_object_write_callback_ = previous_suppression;
installGroupObjectCallbacks();
return consumed; return consumed;
} }
@@ -293,6 +323,7 @@ bool EtsDeviceRuntime::handleBusFrame(const uint8_t* data, size_t len) {
} }
data_link_layer->externalFrameReceived(frame); data_link_layer->externalFrameReceived(frame);
loop(); loop();
installGroupObjectCallbacks();
return consumed; return consumed;
} }
@@ -314,10 +345,13 @@ bool EtsDeviceRuntime::emitGroupValue(uint16_t group_object_number, const uint8_
} else { } else {
std::copy_n(data, len, group_object.valueRef()); 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); sender_ = std::move(sender);
group_object.objectWritten(); group_object.objectWritten();
loop(); loop();
sender_ = nullptr; sender_ = nullptr;
suppress_group_object_write_callback_ = previous_suppression;
return true; return true;
} }
@@ -349,10 +383,36 @@ void EtsDeviceRuntime::EmitTunnelFrame(CemiFrame& frame, void* context) {
void EtsDeviceRuntime::HandleSecureGroupWrite(uint16_t group_address, const uint8_t* data, void EtsDeviceRuntime::HandleSecureGroupWrite(uint16_t group_address, const uint8_t* data,
uint8_t data_length, void* context) { uint8_t data_length, void* context) {
auto* self = static_cast<EtsDeviceRuntime*>(context); auto* self = static_cast<EtsDeviceRuntime*>(context);
if (self == nullptr || !self->group_write_handler_) { if (self == nullptr) {
return; 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<unsigned>(ko.asap()),
static_cast<unsigned>(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<unsigned>(ko.asap()),
static_cast<unsigned>(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, bool EtsDeviceRuntime::HandleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id,
@@ -395,6 +455,24 @@ bool EtsDeviceRuntime::DispatchFunctionProperty(FunctionPropertyHandler* handler
return true; 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<unsigned>(count));
group_object_callback_count_ = count;
}
}
uint16_t EtsDeviceRuntime::DefaultTunnelClientAddress(uint16_t individual_address) { uint16_t EtsDeviceRuntime::DefaultTunnelClientAddress(uint16_t individual_address) {
if (!IsUsableIndividualAddress(individual_address)) { if (!IsUsableIndividualAddress(individual_address)) {
return 0x1101; return 0x1101;
+91 -1
View File
@@ -610,6 +610,23 @@ std::optional<int> MetadataInt(const DaliBridgeResult& result, const std::string
return getObjectInt(result.metadata, key); 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, DaliBridgeRequest RequestForTarget(uint16_t group_address,
const GatewayKnxDaliTarget& target, const GatewayKnxDaliTarget& target,
BridgeOperation operation) { BridgeOperation operation) {
@@ -642,6 +659,17 @@ DaliBridgeResult ErrorResult(uint16_t group_address, const char* message) {
return result; 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<int>(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) { bool SendAll(int sock, const uint8_t* data, size_t len, const sockaddr_in& remote) {
return sendto(sock, data, len, 0, reinterpret_cast<const sockaddr*>(&remote), return sendto(sock, data, len, 0, reinterpret_cast<const sockaddr*>(&remote),
sizeof(remote)) == static_cast<int>(len); sizeof(remote)) == static_cast<int>(len);
@@ -716,6 +744,11 @@ std::optional<GatewayKnxConfig> GatewayKnxConfigFromValue(const DaliValue* value
config.main_group = static_cast<uint8_t>( config.main_group = static_cast<uint8_t>(
std::clamp(ObjectIntAny(object, {"mainGroup", "main_group"}).value_or(config.main_group), std::clamp(ObjectIntAny(object, {"mainGroup", "main_group"}).value_or(config.main_group),
0, 31)); 0, 31));
config.dali_bus_id = static_cast<uint8_t>(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<uint16_t>(std::clamp( config.udp_port = static_cast<uint16_t>(std::clamp(
ObjectIntAny(object, {"udpPort", "port", "udp_port"}).value_or(config.udp_port), 1, ObjectIntAny(object, {"udpPort", "port", "udp_port"}).value_or(config.udp_port), 1,
65535)); 65535));
@@ -790,6 +823,7 @@ DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config) {
out["etsDatabaseEnabled"] = config.ets_database_enabled; out["etsDatabaseEnabled"] = config.ets_database_enabled;
out["mappingMode"] = GatewayKnxMappingModeToString(config.mapping_mode); out["mappingMode"] = GatewayKnxMappingModeToString(config.mapping_mode);
out["mainGroup"] = static_cast<int>(config.main_group); out["mainGroup"] = static_cast<int>(config.main_group);
out["daliBusId"] = static_cast<int>(config.dali_bus_id);
out["udpPort"] = static_cast<int>(config.udp_port); out["udpPort"] = static_cast<int>(config.udp_port);
out["multicastAddress"] = config.multicast_address; out["multicastAddress"] = config.multicast_address;
out["ipInterfaceIndividualAddress"] = 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); 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<unsigned>(group_object_number),
GatewayKnxGroupAddressString(group_address).c_str(), static_cast<unsigned>(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<unsigned>(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<int>(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<unsigned>(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<unsigned>(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, bool GatewayKnxBridge::handleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id,
const uint8_t* data, size_t len, const uint8_t* data, size_t len,
std::vector<uint8_t>* response) { std::vector<uint8_t>* response) {
@@ -1719,6 +1789,10 @@ void GatewayKnxTpIpRouter::setGroupWriteHandler(GroupWriteHandler handler) {
group_write_handler_ = std::move(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_; } const GatewayKnxConfig& GatewayKnxTpIpRouter::config() const { return config_; }
bool GatewayKnxTpIpRouter::tpUartOnline() const { return tp_uart_online_; } 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()); 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<unsigned>(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<unsigned>(group_object_number), result.error.c_str());
}
});
ets_device_->setBusFrameSender([this](const uint8_t* data, size_t len) { ets_device_->setBusFrameSender([this](const uint8_t* data, size_t len) {
routeOpenKnxGroupWrite(data, len, "KNX TP frame");
sendTunnelIndication(data, len); sendTunnelIndication(data, len);
sendRoutingIndication(data, len); sendRoutingIndication(data, len);
}); });
+1 -1
Submodule knx updated: 23b0cddf24...82f22cf571