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
@@ -1,5 +1,7 @@
#include "gateway_knx_private.hpp"
#include "knx_device_broker.h"
namespace gateway {
void GatewayKnxTpIpRouter::selectOpenKnxNetworkInterface(const sockaddr_in& remote) {
@@ -18,7 +20,13 @@ bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t
uint16_t response_service,
const uint8_t* suppress_routing_echo,
size_t suppress_routing_echo_len) {
bool route_to_oam = response_client != nullptr && response_client->oam_router_persona;
const KnxIngressContext ingress_context{
response_service == kServiceRoutingIndication ? KnxPortKind::kIpRouting
: KnxPortKind::kIpTunnel,
response_client != nullptr && response_client->oam_router_persona,
false,
};
bool route_to_oam = ingress_context.oam_persona_hint;
bool route_to_all_internal_instances = false;
if (data != nullptr && len >= 2) {
std::vector<uint8_t> frame_data(data, data + len);
@@ -36,25 +44,35 @@ bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t
if (ets_device_ == nullptr && oam_router_ == nullptr) {
return false;
}
if (response_client != nullptr && oam_router_ != nullptr &&
(oam_router_->programmingMode() || !oam_router_->configured())) {
response_client->oam_router_persona = true;
}
std::vector<uint8_t> tunnel_confirmation;
const bool needs_tunnel_confirmation =
response_client != nullptr && response_client->connected &&
response_service == kServiceTunnellingRequest &&
BuildTunnelConfirmationFrame(data, len, &tunnel_confirmation);
bool sent_tunnel_confirmation = false;
KnxResponseDeduplicator response_broker;
const auto send_response =
[this, response_client, response_service, needs_tunnel_confirmation,
&tunnel_confirmation, &sent_tunnel_confirmation, suppress_routing_echo,
suppress_routing_echo_len](const uint8_t* response, size_t response_len) {
&tunnel_confirmation, &sent_tunnel_confirmation, &response_broker,
suppress_routing_echo, suppress_routing_echo_len](const uint8_t* response,
size_t response_len) {
if (response == nullptr || response_len == 0) {
return;
}
publishCloudCemiFrame(response, response_len);
std::vector<uint8_t> response_data(response, response + response_len);
if (!response_broker.remember(response_data)) {
return;
}
publishCloudCemiFrame(response_data.data(), response_data.size());
const bool routing_context =
response_client == nullptr && response_service == kServiceRoutingIndication;
const auto message_code = CemiMessageCode(response, response_len);
const auto message_code = CemiMessageCode(response_data.data(), response_data.size());
if (routing_context && suppress_routing_echo != nullptr &&
IsLocalRoutingEchoIndication(response, response_len, suppress_routing_echo,
IsLocalRoutingEchoIndication(response_data.data(), response_data.size(), suppress_routing_echo,
suppress_routing_echo_len)) {
return;
}
@@ -65,12 +83,14 @@ bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t
tunnel_confirmation.data(), tunnel_confirmation.size());
}
const uint16_t service = KnxIpServiceForCemi(response, response_len, response_service);
const uint16_t service = KnxIpServiceForCemi(response_data.data(), response_data.size(),
response_service);
if (service == kServiceDeviceConfigurationRequest) {
if (response_client != nullptr && response_client->connected) {
sendCemiFrameToClient(*response_client, service, response, response_len);
sendCemiFrameToClient(*response_client, service, response_data.data(),
response_data.size());
} else if (routing_context) {
sendRoutingIndication(response, response_len);
sendRoutingIndication(response_data.data(), response_data.size());
}
return;
}
@@ -80,20 +100,22 @@ bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t
}
if (response_client != nullptr && response_client->connected) {
sent_tunnel_confirmation =
sendCemiFrameToClient(*response_client, service, response, response_len) ||
sendCemiFrameToClient(*response_client, service, response_data.data(),
response_data.size()) ||
sent_tunnel_confirmation;
}
return;
}
if (routing_context) {
sendRoutingIndication(response, response_len);
sendRoutingIndication(response_data.data(), response_data.size());
return;
}
if (response_client != nullptr && response_client->connected) {
sendCemiFrameToClient(*response_client, service, response, response_len);
sendCemiFrameToClient(*response_client, service, response_data.data(),
response_data.size());
return;
}
sendTunnelIndication(response, response_len);
sendTunnelIndication(response_data.data(), response_data.size());
};
bool consumed = false;
if (ets_device_ != nullptr) {
@@ -112,6 +134,9 @@ bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t
return consumed;
}
if (route_to_oam) {
if (response_client != nullptr) {
response_client->oam_router_persona = true;
}
return handleOamRouterTunnelFrame(data, len, response_client, response_service,
suppress_routing_echo, suppress_routing_echo_len);
}
@@ -204,21 +229,26 @@ bool GatewayKnxTpIpRouter::handleOamRouterTunnelFrame(const uint8_t* data, size_
response_service == kServiceTunnellingRequest &&
BuildTunnelConfirmationFrame(data, len, &tunnel_confirmation);
bool sent_tunnel_confirmation = false;
KnxResponseDeduplicator response_broker;
const bool consumed = oam_router_->handleTunnelFrame(
data, len,
[this, response_client, response_service, needs_tunnel_confirmation,
&tunnel_confirmation, &sent_tunnel_confirmation,
&tunnel_confirmation, &sent_tunnel_confirmation, &response_broker,
suppress_routing_echo, suppress_routing_echo_len](const uint8_t* response,
size_t response_len) {
if (response == nullptr || response_len == 0) {
return;
}
publishCloudCemiFrame(response, response_len);
std::vector<uint8_t> response_data(response, response + response_len);
if (!response_broker.remember(response_data)) {
return;
}
publishCloudCemiFrame(response_data.data(), response_data.size());
const bool routing_context =
response_client == nullptr && response_service == kServiceRoutingIndication;
const auto message_code = CemiMessageCode(response, response_len);
const auto message_code = CemiMessageCode(response_data.data(), response_data.size());
if (routing_context && suppress_routing_echo != nullptr &&
IsLocalRoutingEchoIndication(response, response_len, suppress_routing_echo,
IsLocalRoutingEchoIndication(response_data.data(), response_data.size(), suppress_routing_echo,
suppress_routing_echo_len)) {
return;
}
@@ -229,12 +259,14 @@ bool GatewayKnxTpIpRouter::handleOamRouterTunnelFrame(const uint8_t* data, size_
tunnel_confirmation.data(), tunnel_confirmation.size());
}
const uint16_t service = KnxIpServiceForCemi(response, response_len, response_service);
const uint16_t service = KnxIpServiceForCemi(response_data.data(), response_data.size(),
response_service);
if (service == kServiceDeviceConfigurationRequest) {
if (response_client != nullptr && response_client->connected) {
sendCemiFrameToClient(*response_client, service, response, response_len);
sendCemiFrameToClient(*response_client, service, response_data.data(),
response_data.size());
} else if (routing_context) {
sendRoutingIndication(response, response_len);
sendRoutingIndication(response_data.data(), response_data.size());
}
return;
}
@@ -244,20 +276,22 @@ bool GatewayKnxTpIpRouter::handleOamRouterTunnelFrame(const uint8_t* data, size_
}
if (response_client != nullptr && response_client->connected) {
sent_tunnel_confirmation =
sendCemiFrameToClient(*response_client, service, response, response_len) ||
sendCemiFrameToClient(*response_client, service, response_data.data(),
response_data.size()) ||
sent_tunnel_confirmation;
}
return;
}
if (routing_context) {
sendRoutingIndication(response, response_len);
sendRoutingIndication(response_data.data(), response_data.size());
return;
}
if (response_client != nullptr && response_client->connected) {
sendCemiFrameToClient(*response_client, service, response, response_len);
sendCemiFrameToClient(*response_client, service, response_data.data(),
response_data.size());
return;
}
sendTunnelIndication(response, response_len);
sendTunnelIndication(response_data.data(), response_data.size());
});
if (needs_tunnel_confirmation && consumed && !sent_tunnel_confirmation) {
sendCemiFrameToClient(*response_client, kServiceTunnellingRequest,
@@ -268,6 +302,10 @@ bool GatewayKnxTpIpRouter::handleOamRouterTunnelFrame(const uint8_t* data, size_
bool GatewayKnxTpIpRouter::transmitOpenKnxTpFrame(const uint8_t* data, size_t len) {
SemaphoreGuard guard(openknx_lock_);
return transmitOpenKnxTpFrameLocked(data, len);
}
bool GatewayKnxTpIpRouter::transmitOpenKnxTpFrameLocked(const uint8_t* data, size_t len) {
if (ets_device_ == nullptr) {
return false;
}
@@ -276,6 +314,56 @@ bool GatewayKnxTpIpRouter::transmitOpenKnxTpFrame(const uint8_t* data, size_t le
return sent;
}
bool GatewayKnxTpIpRouter::handleOpenKnxTpIngressFrame(const uint8_t* data, size_t len) {
if (data == nullptr || len < 2 || (ets_device_ == nullptr && oam_router_ == nullptr)) {
return false;
}
std::vector<uint8_t> frame_data(data, data + len);
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
if (!frame.valid()) {
return false;
}
const bool broadcast_management = IsKnxBroadcastManagementRequest(frame);
const bool addressed_to_oam =
oam_router_ != nullptr && MatchesOamRouterLocalIndividualAddress(frame, *oam_router_);
if (!broadcast_management && !addressed_to_oam) {
return false;
}
const KnxIngressContext ingress_context{KnxPortKind::kTpUart, addressed_to_oam,
broadcast_management};
KnxResponseDeduplicator response_broker(frame_data.data(), frame_data.size());
const auto send_response = [this, &response_broker](const uint8_t* response,
size_t response_len) {
if (response == nullptr || response_len == 0) {
return;
}
std::vector<uint8_t> response_data(response, response + response_len);
if (!response_broker.remember(response_data)) {
return;
}
publishCloudCemiFrame(response_data.data(), response_data.size());
transmitOpenKnxTpFrameLocked(response_data.data(), response_data.size());
sendTunnelIndication(response_data.data(), response_data.size());
sendRoutingIndication(response_data.data(), response_data.size());
};
bool consumed = false;
if (ingress_context.broadcast_management && ets_device_ != nullptr) {
consumed = ets_device_->handleBusFrame(data, len) || consumed;
}
if ((ingress_context.broadcast_management || ingress_context.oam_persona_hint) &&
oam_router_ != nullptr) {
consumed = oam_router_->handleBusFrame(data, len, send_response) || consumed;
}
if (consumed) {
publishCloudCemiFrame(data, len);
syncOpenKnxConfigFromDevice();
}
return consumed;
}
bool GatewayKnxTpIpRouter::handleOpenKnxBusFrame(const uint8_t* data, size_t len) {
bool consumed = false;
{
@@ -441,6 +529,14 @@ uint8_t GatewayKnxTpIpRouter::advertisedMedium() const {
return (config_.tunnel_enabled || tp_uart_online_) ? kKnxMediumTp1 : kKnxMediumIp;
}
const char* GatewayKnxTpIpRouter::ipInterfaceName() const {
return config_.oam_router.enabled ? "oam_ip_router" : "knx_ip_interface";
}
const char* GatewayKnxTpIpRouter::ipInterfaceFriendlyName() const {
return config_.oam_router.enabled ? "OAM IP Router" : "DaliMaster KNX IP";
}
void GatewayKnxTpIpRouter::syncOpenKnxConfigFromDevice() {
if (ets_device_ == nullptr) {
return;
@@ -454,6 +550,15 @@ void GatewayKnxTpIpRouter::syncOpenKnxConfigFromDevice() {
updated.individual_address = snapshot.individual_address;
changed = true;
}
if (oam_router_ != nullptr) {
const auto oam_snapshot = oam_router_->snapshot();
if (oam_snapshot.individual_address != 0 && oam_snapshot.individual_address != 0xffff &&
oam_snapshot.individual_address != updated.oam_router.individual_address) {
updated.oam_router.individual_address = oam_snapshot.individual_address;
updated.ip_interface_individual_address = oam_snapshot.individual_address;
changed = true;
}
}
if (snapshot.configured || !snapshot.associations.empty()) {
std::vector<GatewayKnxEtsAssociation> associations;
associations.reserve(snapshot.associations.size());
@@ -480,6 +585,18 @@ void GatewayKnxTpIpRouter::syncOpenKnxConfigFromDevice() {
}
uint16_t GatewayKnxTpIpRouter::effectiveIpInterfaceIndividualAddress() const {
if (config_.oam_router.enabled) {
if (oam_router_ != nullptr) {
const uint16_t address = oam_router_->individualAddress();
if (address != 0 && address != 0xffff) {
return address;
}
}
if (config_.oam_router.individual_address != 0 &&
config_.oam_router.individual_address != 0xffff) {
return config_.oam_router.individual_address;
}
}
if (config_.ip_interface_individual_address != 0 &&
config_.ip_interface_individual_address != 0xffff) {
return config_.ip_interface_individual_address;
@@ -498,6 +615,10 @@ uint16_t GatewayKnxTpIpRouter::effectiveKnxDeviceIndividualAddress() const {
}
uint16_t GatewayKnxTpIpRouter::effectiveTunnelAddress() const {
if (config_.oam_router.enabled && config_.oam_router.tunnel_address_base != 0 &&
config_.oam_router.tunnel_address_base != 0xffff) {
return config_.oam_router.tunnel_address_base;
}
const uint16_t interface_address = effectiveIpInterfaceIndividualAddress();
uint16_t device = static_cast<uint16_t>((interface_address & 0x00ff) + 1);
if (device == 0 || device > 0xff) {