feat: Enhance OAM router functionality and improve KNX device handling

- Added support for OAM router configuration in app_main, allowing the IP interface individual address to be set based on the OAM router's individual address.
- Updated GatewayBridgeService to validate IP interface addresses only when OAM router is disabled, ensuring proper address management.
- Introduced KnxResponseDeduplicator to prevent duplicate responses in KNX communication.
- Enhanced ETS device runtime to handle bus frames and set up frame receivers for OAM router.
- Improved GatewayKnxTpIpRouter to manage OAM router interactions, including handling tunnel frames and bus frames.
- Updated CMakeLists to include new knx_device_broker source file.
- Refined logging messages to provide clearer context regarding the IP interface being used.
- Added methods to retrieve IP interface names and friendly names based on the OAM router configuration.

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-28 15:44:17 +08:00
parent 078c37a20f
commit 8211514fe3
17 changed files with 432 additions and 75 deletions
@@ -3427,8 +3427,9 @@ struct GatewayBridgeService::ChannelRuntime {
}
return ESP_ERR_INVALID_ARG;
}
if (config.ip_interface_individual_address == 0 ||
config.ip_interface_individual_address == 0xffff) {
if (!config.oam_router.enabled &&
(config.ip_interface_individual_address == 0 ||
config.ip_interface_individual_address == 0xffff)) {
if (error_message != nullptr) {
*error_message = "KNX IP interface individual address must be a configured address";
}
@@ -3440,7 +3441,8 @@ struct GatewayBridgeService::ChannelRuntime {
}
return ESP_ERR_INVALID_ARG;
}
if (config.ip_interface_individual_address == config.individual_address) {
if (!config.oam_router.enabled &&
config.ip_interface_individual_address == config.individual_address) {
if (error_message != nullptr) {
*error_message = "KNX IP interface and KNX-DALI gateway addresses must differ";
}
@@ -3463,11 +3465,10 @@ struct GatewayBridgeService::ChannelRuntime {
return ESP_ERR_INVALID_ARG;
}
if (config.oam_router.individual_address == config.individual_address ||
config.oam_router.individual_address == config.ip_interface_individual_address ||
config.oam_router.tunnel_address_base == config.individual_address ||
config.oam_router.tunnel_address_base == config.ip_interface_individual_address) {
config.oam_router.tunnel_address_base == config.oam_router.individual_address) {
if (error_message != nullptr) {
*error_message = "OAM KNX/IP router addresses must differ from the shared IP interface and KNX-DALI gateway addresses";
*error_message = "OAM KNX/IP router addresses must differ from KNX-DALI gateway and OAM tunnel addresses";
}
return ESP_ERR_INVALID_ARG;
}
@@ -4364,6 +4365,19 @@ esp_err_t GatewayBridgeService::startKnxEndpoint(ChannelRuntime* requested_runti
requested_runtime->channel.gateway_id, owner->channel.gateway_id);
}
for (const auto& runtime : runtimes_) {
if (runtime.get() == owner) {
continue;
}
if (runtime->knx_started ||
(runtime->knx_router != nullptr && runtime->knx_router->started())) {
ESP_LOGI(kTag,
"gateway=%u stopping non-owner KNXnet/IP router; shared endpoint owner is gateway=%u",
runtime->channel.gateway_id, owner->channel.gateway_id);
runtime->stopKnx();
}
}
if (used_uarts != nullptr) {
LockGuard guard(owner->lock);
const auto owner_config = owner->activeKnxConfigLocked();
@@ -4404,23 +4418,33 @@ esp_err_t GatewayBridgeService::stopKnxEndpoint(ChannelRuntime* requested_runtim
DaliBridgeResult GatewayBridgeService::routeKnxGroupWrite(uint16_t group_address,
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 " +
GatewayKnxGroupAddressString(group_address);
return result;
DaliBridgeResult aggregate;
aggregate.error = "No DALI channel maps KNX group " +
GatewayKnxGroupAddressString(group_address);
size_t matched_count = 0;
for (const auto& candidate : runtimes_) {
LockGuard guard(candidate->lock);
const auto config = candidate->activeKnxConfigLocked();
if (candidate->knx == nullptr || !config.has_value() || !config->dali_router_enabled ||
!candidate->channel.native_bus_id.has_value() ||
candidate->channel.native_bus_id.value() != config->dali_bus_id ||
!candidate->knx->matchesGroupAddress(group_address)) {
continue;
}
++matched_count;
DaliBridgeResult child = candidate->knx->handleGroupWrite(group_address, data, len);
child.metadata["gatewayId"] = static_cast<int>(candidate->channel.gateway_id);
child.metadata["channelIndex"] = static_cast<int>(candidate->channel.channel_index);
aggregate.results.emplace_back(child.toJson());
if (child.ok) {
aggregate.ok = true;
aggregate.error.clear();
} else if (!child.error.empty() && aggregate.error.empty()) {
aggregate.error = child.error;
}
}
LockGuard guard(runtime->lock);
if (runtime->knx == nullptr || !runtime->knx->matchesGroupAddress(group_address)) {
DaliBridgeResult result;
result.error = "Selected DALI bus does not map KNX group " +
GatewayKnxGroupAddressString(group_address);
return result;
}
return runtime->knx->handleGroupWrite(group_address, data, len);
aggregate.metadata["matchedChannels"] = static_cast<int>(matched_count);
return aggregate;
}
DaliBridgeResult GatewayBridgeService::routeKnxGroupObjectWrite(uint16_t group_object_number,