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,
+1
View File
@@ -7,6 +7,7 @@ idf_component_register(
"src/gateway_knx_router_packets.cpp"
"src/gateway_knx_router_services.cpp"
"src/gateway_knx_secure_transport.cpp"
"src/knx_device_broker.cpp"
"src/oam_router_runtime.cpp"
"src/ets_device_runtime.cpp"
"src/ets_memory_loader.cpp"
@@ -22,6 +22,7 @@ class TpuartUartInterface;
class EtsDeviceRuntime {
public:
using CemiFrameSender = std::function<void(const uint8_t* data, size_t len)>;
using CemiFrameReceiver = std::function<bool(const uint8_t* data, size_t len)>;
using GroupWriteHandler = std::function<void(uint16_t group_address, const uint8_t* data,
size_t len)>;
using GroupObjectWriteHandler = std::function<void(uint16_t group_object_number,
@@ -62,6 +63,7 @@ class EtsDeviceRuntime {
void setGroupWriteHandler(GroupWriteHandler handler);
void setGroupObjectWriteHandler(GroupObjectWriteHandler handler);
void setBusFrameSender(CemiFrameSender sender);
void setTpFrameReceiver(CemiFrameReceiver receiver);
void setNetworkInterface(esp_netif_t* netif);
bool hasTpUart() const;
bool enableTpUart(bool enabled = true);
@@ -121,6 +123,7 @@ class EtsDeviceRuntime {
Bau07B0 device_;
CemiFrameSender sender_;
CemiFrameSender bus_frame_sender_;
CemiFrameReceiver tp_frame_receiver_;
GroupWriteHandler group_write_handler_;
GroupObjectWriteHandler group_object_write_handler_;
FunctionPropertyHandler command_handler_;
@@ -340,8 +340,10 @@ class GatewayKnxTpIpRouter {
::sockaddr_in data_remote{};
std::vector<uint8_t> last_received_cemi;
std::vector<uint8_t> last_tunnel_confirmation_packet;
std::vector<uint8_t> last_sent_cemi;
uint16_t secure_session_id{0};
bool oam_router_persona{false};
TickType_t last_sent_cemi_tick{0};
};
struct SecureSession {
@@ -468,12 +470,16 @@ class GatewayKnxTpIpRouter {
size_t suppress_routing_echo_len = 0);
bool handleOpenKnxBusFrame(const uint8_t* data, size_t len);
bool transmitOpenKnxTpFrame(const uint8_t* data, size_t len);
bool transmitOpenKnxTpFrameLocked(const uint8_t* data, size_t len);
bool handleOpenKnxTpIngressFrame(const uint8_t* data, size_t len);
void publishCloudCemiFrame(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;
const char* ipInterfaceName() const;
const char* ipInterfaceFriendlyName() const;
void syncOpenKnxConfigFromDevice();
uint16_t effectiveIpInterfaceIndividualAddress() const;
uint16_t effectiveKnxDeviceIndividualAddress() const;
@@ -0,0 +1,38 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <vector>
namespace gateway {
enum class KnxPortKind : uint8_t {
kIpTunnel,
kIpRouting,
kTpUart,
kCloud,
kRfReserved,
};
struct KnxIngressContext {
KnxPortKind port{KnxPortKind::kIpTunnel};
bool oam_persona_hint{false};
bool broadcast_management{false};
};
class KnxResponseDeduplicator {
public:
KnxResponseDeduplicator() = default;
KnxResponseDeduplicator(const uint8_t* original, size_t len);
bool remember(const uint8_t* data, size_t len);
bool remember(const std::vector<uint8_t>& data);
size_t suppressedCount() const;
private:
std::vector<uint8_t> original_;
std::vector<std::vector<uint8_t>> sent_;
size_t suppressed_count_{0};
};
} // namespace gateway
@@ -43,9 +43,11 @@ class OamRouterRuntime {
DeviceObject* deviceObject();
Platform* platform();
void setNetworkInterface(esp_netif_t* netif);
void setBusFrameSender(CemiFrameSender sender);
bool handleTunnelFrame(const uint8_t* data, size_t len, CemiFrameSender sender);
bool handleLocalBroadcastManagementFrame(const uint8_t* data, size_t len,
CemiFrameSender sender);
bool handleBusFrame(const uint8_t* data, size_t len, CemiFrameSender sender);
void loop();
private:
@@ -53,9 +55,11 @@ class OamRouterRuntime {
static void EmitTunnelFrame(CemiFrame& frame, void* context);
static uint16_t DefaultTunnelClientAddress(uint16_t individual_address);
bool shouldConsumeTunnelFrame(CemiFrame& frame) const;
bool shouldConsumeBusFrame(CemiFrame& frame) const;
std::string nvs_namespace_;
CemiFrameSender sender_;
CemiFrameSender bus_frame_sender_;
#if defined(ENABLE_BAU091A_PERSONA)
EspIdfPlatform platform_;
Bau091A device_;
@@ -163,6 +163,9 @@ EtsDeviceRuntime::EtsDeviceRuntime(std::string nvs_namespace,
}
EtsDeviceRuntime::~EtsDeviceRuntime() {
if (auto* data_link_layer = device_.getDataLinkLayer()) {
data_link_layer->receiveFrameHandler({});
}
if (tp_uart_interface_ != nullptr) {
device_.enabled(false);
}
@@ -289,6 +292,15 @@ void EtsDeviceRuntime::setBusFrameSender(CemiFrameSender sender) {
bus_frame_sender_ = std::move(sender);
}
void EtsDeviceRuntime::setTpFrameReceiver(CemiFrameReceiver receiver) {
tp_frame_receiver_ = std::move(receiver);
if (auto* data_link_layer = device_.getDataLinkLayer()) {
data_link_layer->receiveFrameHandler([this](CemiFrame& frame) {
return tp_frame_receiver_ ? tp_frame_receiver_(frame.data(), frame.dataLength()) : false;
});
}
}
void EtsDeviceRuntime::setNetworkInterface(esp_netif_t* netif) {
platform_.networkInterface(netif);
}
@@ -647,12 +659,27 @@ bool EtsDeviceRuntime::shouldConsumeTunnelFrame(CemiFrame& frame) const {
}
bool EtsDeviceRuntime::shouldConsumeBusFrame(CemiFrame& frame) const {
if (frame.messageCode() != L_data_ind && frame.messageCode() != L_data_req) {
return false;
}
#ifdef USE_DATASECURE
return frame.messageCode() == L_data_ind && frame.addressType() == GroupAddress &&
frame.apdu().type() == SecureService;
#else
return false;
if (frame.addressType() == GroupAddress && frame.apdu().type() == SecureService) {
return true;
}
#endif
if (IsBroadcastManagementRequest(frame)) {
return true;
}
if (frame.addressType() != IndividualAddress) {
return false;
}
const uint16_t dest = frame.destinationAddress();
const bool commissioning = !const_cast<Bau07B0&>(device_).configured() || programmingMode();
return dest == individualAddress() || dest == tunnelClientAddress() ||
(commissioning && dest == kKnxUnconfiguredBroadcastAddress);
}
} // namespace gateway::openknx
@@ -824,7 +824,8 @@ std::optional<MessageCode> CemiMessageCode(const uint8_t* data, size_t len) {
}
bool IsKnxBroadcastManagementRequest(CemiFrame& frame) {
if (frame.messageCode() != L_data_req || frame.addressType() != GroupAddress ||
if ((frame.messageCode() != L_data_req && frame.messageCode() != L_data_ind) ||
frame.addressType() != GroupAddress ||
frame.destinationAddress() != 0x0000) {
return false;
}
@@ -154,9 +154,9 @@ esp_err_t GatewayKnxTpIpRouter::start(uint32_t task_stack_size, UBaseType_t task
&log_tp_uart_rx_pin);
}
ESP_LOGI(kTag,
"starting KNXnet/IP router namespace=%s udp=%u tunnel=%d multicast=%d group=%s "
"starting KNXnet/IP router ipInterface=%s runtimeNamespace=%s udp=%u tunnel=%d multicast=%d group=%s "
"tpUart=%d tx=%s rx=%s nineBit=%d commissioningOnly=%d",
openknx_namespace_.c_str(), static_cast<unsigned>(config_.udp_port),
ipInterfaceName(), openknx_namespace_.c_str(), static_cast<unsigned>(config_.udp_port),
config_.tunnel_enabled, config_.multicast_enabled,
config_.multicast_address.c_str(), config_.tp_uart.uart_port,
UartPinDescription(config_.tp_uart.tx_pin, log_tp_uart_tx_pin).c_str(),
@@ -351,6 +351,16 @@ esp_err_t GatewayKnxTpIpRouter::initializeRuntime() {
sendTunnelIndication(data, len);
sendRoutingIndication(data, len);
});
ets_device_->setTpFrameReceiver([this](const uint8_t* data, size_t len) {
return handleOpenKnxTpIngressFrame(data, len);
});
if (oam_router_ != nullptr) {
oam_router_->setBusFrameSender([this](const uint8_t* data, size_t len) {
publishCloudCemiFrame(data, len);
sendTunnelIndication(data, len);
sendRoutingIndication(data, len);
});
}
syncOpenKnxConfigFromDevice();
}
if (!configureTpUart()) {
@@ -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) {
@@ -141,16 +141,28 @@ std::vector<uint8_t> GatewayKnxTpIpRouter::buildDeviceInfoDib(
uint8_t mac[6]{};
if (ReadBaseMac(mac)) {
dib[8] = static_cast<uint8_t>((knx_internal::kReg1DaliManufacturerId >> 8) & 0xff);
dib[9] = static_cast<uint8_t>(knx_internal::kReg1DaliManufacturerId & 0xff);
std::memcpy(dib.data() + 10, mac + 2, 4);
const bool oam_ip_owner = config_.oam_router.enabled;
const uint16_t manufacturer = oam_ip_owner ? knx_internal::kOamRouterManufacturerId
: knx_internal::kReg1DaliManufacturerId;
uint32_t serial_suffix = (static_cast<uint32_t>(mac[2]) << 24) |
(static_cast<uint32_t>(mac[3]) << 16) |
(static_cast<uint32_t>(mac[4]) << 8) |
static_cast<uint32_t>(mac[5]);
if (oam_ip_owner) {
serial_suffix += knx_internal::kOamRouterSerialMacIncrement;
}
dib[8] = static_cast<uint8_t>((manufacturer >> 8) & 0xff);
dib[9] = static_cast<uint8_t>(manufacturer & 0xff);
dib[10] = static_cast<uint8_t>((serial_suffix >> 24) & 0xff);
dib[11] = static_cast<uint8_t>((serial_suffix >> 16) & 0xff);
dib[12] = static_cast<uint8_t>((serial_suffix >> 8) & 0xff);
dib[13] = static_cast<uint8_t>(serial_suffix & 0xff);
std::memcpy(dib.data() + 18, mac, 6);
}
WriteIp(dib.data() + 14, inet_addr(config_.multicast_address.c_str()));
char friendly[31]{};
std::snprintf(friendly, sizeof(friendly), "DALI GW MG%u %s",
static_cast<unsigned>(config_.main_group), openknx_namespace_.c_str());
std::snprintf(friendly, sizeof(friendly), "%s", ipInterfaceFriendlyName());
std::memcpy(dib.data() + 24, friendly, std::min<size_t>(30, std::strlen(friendly)));
(void)remote;
return dib;
@@ -336,6 +348,14 @@ bool GatewayKnxTpIpRouter::sendCemiFrameToClient(TunnelClient& client, uint16_t
request.connectionHeader().length(LEN_CH);
request.connectionHeader().channelId(client.channel_id);
const auto message_code = CemiMessageCode(data, len);
if (service == kServiceTunnellingRequest && message_code.has_value() &&
message_code.value() != L_data_con && client.last_sent_cemi == frame_data &&
xTaskGetTickCount() - client.last_sent_cemi_tick < pdMS_TO_TICKS(500)) {
ESP_LOGD(kTag, "suppress duplicate KNXnet/IP cEMI channel=%u cemi=0x%02x len=%u to %s",
static_cast<unsigned>(client.channel_id), static_cast<unsigned>(data[0]),
static_cast<unsigned>(len), EndpointString(client.data_remote).c_str());
return true;
}
const uint8_t send_sequence = client.send_sequence++;
request.connectionHeader().sequenceCounter(send_sequence);
request.connectionHeader().status(kKnxNoError);
@@ -357,6 +377,11 @@ bool GatewayKnxTpIpRouter::sendCemiFrameToClient(TunnelClient& client, uint16_t
client.last_tunnel_confirmation_packet = packet;
}
}
if (service == kServiceTunnellingRequest && message_code.has_value() &&
message_code.value() != L_data_con) {
client.last_sent_cemi = std::move(frame_data);
client.last_sent_cemi_tick = xTaskGetTickCount();
}
ESP_LOGI(kTag, "sent KNXnet/IP cEMI service=0x%04x channel=%u seq=%u cemi=0x%02x len=%u to %s",
static_cast<unsigned>(service), static_cast<unsigned>(client.channel_id),
static_cast<unsigned>(request.connectionHeader().sequenceCounter()), static_cast<unsigned>(data[0]),
@@ -119,9 +119,9 @@ void GatewayKnxTpIpRouter::handleSearchRequest(uint16_t service, const uint8_t*
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<unsigned>(service), openknx_namespace_.c_str(),
static_cast<unsigned>(config_.main_group),
ESP_LOGI(kTag, "sent KNXnet/IP search response service=0x%04x ipInterface=%s ia=0x%04x to %s:%u endpoint=%u.%u.%u.%u:%u",
static_cast<unsigned>(service), ipInterfaceName(),
static_cast<unsigned>(effectiveIpInterfaceIndividualAddress()),
Ipv4String(response_remote.sin_addr.s_addr).c_str(), static_cast<unsigned>(ntohs(response_remote.sin_port)),
static_cast<unsigned>((*hpai)[2]), static_cast<unsigned>((*hpai)[3]),
static_cast<unsigned>((*hpai)[4]), static_cast<unsigned>((*hpai)[5]),
@@ -164,8 +164,9 @@ void GatewayKnxTpIpRouter::handleDescriptionRequest(const uint8_t* packet_data,
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<unsigned>(advertisedMedium()),
ESP_LOGI(kTag, "sent KNXnet/IP description response ipInterface=%s ia=0x%04x medium=0x%02x tpOnline=%d to %s:%u",
ipInterfaceName(), static_cast<unsigned>(effectiveIpInterfaceIndividualAddress()),
static_cast<unsigned>(advertisedMedium()),
tp_uart_online_, Ipv4String(response_remote.sin_addr.s_addr).c_str(),
static_cast<unsigned>(ntohs(response_remote.sin_port)));
}
@@ -314,9 +315,13 @@ GatewayKnxTpIpRouter::TunnelClient* GatewayKnxTpIpRouter::allocateTunnelClient(
free_client->connection_type = connection_type;
free_client->received_sequence = 255;
free_client->send_sequence = 0;
const bool oam_router_persona =
config_.oam_router.enabled && config_.oam_router.secure_tunnel_enabled &&
active_secure_session_id_ != 0;
const bool oam_runtime_available = config_.oam_router.enabled && oam_router_ != nullptr;
const bool oam_secure_persona = oam_runtime_available &&
config_.oam_router.secure_tunnel_enabled && active_secure_session_id_ != 0;
const bool oam_programming_persona = oam_runtime_available && oam_programming_mode_ &&
(connection_type == kKnxConnectionTypeDeviceManagement ||
connection_type == kKnxConnectionTypeTunnel);
const bool oam_router_persona = oam_secure_persona || oam_programming_persona;
if (oam_router_persona) {
const uint16_t first = config_.oam_router.tunnel_address_base;
const uint16_t line = first & 0xff00;
@@ -582,9 +587,11 @@ void GatewayKnxTpIpRouter::handleConnectRequest(const uint8_t* packet_data, size
return;
}
ESP_LOGI(kTag,
"accepted KNXnet/IP connect namespace=%s channel=%u type=0x%02x tunnelPa=0x%04x ctrl=%s data=%s remote=%s active=%u/%u",
openknx_namespace_.c_str(), static_cast<unsigned>(client->channel_id),
static_cast<unsigned>(connection_type), static_cast<unsigned>(client->individual_address),
"accepted KNXnet/IP connect ipInterface=%s channel=%u type=0x%02x persona=%s tunnelPa=0x%04x ctrl=%s data=%s remote=%s active=%u/%u",
ipInterfaceName(), static_cast<unsigned>(client->channel_id),
static_cast<unsigned>(connection_type),
client->oam_router_persona ? "oam" : "reg1",
static_cast<unsigned>(client->individual_address),
EndpointString(control_remote).c_str(), EndpointString(data_remote).c_str(),
EndpointString(remote).c_str(),
static_cast<unsigned>(std::count_if(tunnel_clients_.begin(), tunnel_clients_.end(),
@@ -0,0 +1,33 @@
#include "knx_device_broker.h"
#include <algorithm>
namespace gateway {
KnxResponseDeduplicator::KnxResponseDeduplicator(const uint8_t* original, size_t len) {
if (original != nullptr && len > 0) {
original_.assign(original, original + len);
}
}
bool KnxResponseDeduplicator::remember(const uint8_t* data, size_t len) {
if (data == nullptr || len == 0) {
++suppressed_count_;
return false;
}
return remember(std::vector<uint8_t>(data, data + len));
}
bool KnxResponseDeduplicator::remember(const std::vector<uint8_t>& data) {
if (data.empty() || (!original_.empty() && data == original_) ||
std::find(sent_.begin(), sent_.end(), data) != sent_.end()) {
++suppressed_count_;
return false;
}
sent_.push_back(data);
return true;
}
size_t KnxResponseDeduplicator::suppressedCount() const { return suppressed_count_; }
} // namespace gateway
@@ -208,6 +208,10 @@ void OamRouterRuntime::setNetworkInterface(esp_netif_t* netif) {
#endif
}
void OamRouterRuntime::setBusFrameSender(CemiFrameSender sender) {
bus_frame_sender_ = std::move(sender);
}
bool OamRouterRuntime::handleTunnelFrame(const uint8_t* data, size_t len,
CemiFrameSender sender) {
#if defined(ENABLE_BAU091A_PERSONA) && defined(USE_CEMI_SERVER)
@@ -260,6 +264,34 @@ bool OamRouterRuntime::handleLocalBroadcastManagementFrame(const uint8_t* data,
#endif
}
bool OamRouterRuntime::handleBusFrame(const uint8_t* data, size_t len,
CemiFrameSender sender) {
#if defined(ENABLE_BAU091A_PERSONA)
auto* data_link_layer = device_.getSecondaryDataLinkLayer();
if (data_link_layer == nullptr || data == nullptr || len < 2) {
return false;
}
std::vector<uint8_t> frame_data(data, data + len);
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
if (!shouldConsumeBusFrame(frame)) {
return false;
}
if (frame.messageCode() == L_data_req) {
frame.messageCode(L_data_ind);
}
sender_ = std::move(sender);
data_link_layer->externalFrameReceived(frame);
loop();
sender_ = nullptr;
return true;
#else
(void)data;
(void)len;
(void)sender;
return false;
#endif
}
void OamRouterRuntime::loop() {
#if defined(ENABLE_BAU091A_PERSONA)
device_.loop();
@@ -277,10 +309,16 @@ bool OamRouterRuntime::HandleOutboundCemiFrame(CemiFrame& frame, void* context)
void OamRouterRuntime::EmitTunnelFrame(CemiFrame& frame, void* context) {
auto* self = static_cast<OamRouterRuntime*>(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());
}
}
uint16_t OamRouterRuntime::DefaultTunnelClientAddress(uint16_t individual_address) {
@@ -320,4 +358,20 @@ bool OamRouterRuntime::shouldConsumeTunnelFrame(CemiFrame& frame) const {
}
}
bool OamRouterRuntime::shouldConsumeBusFrame(CemiFrame& frame) const {
if (frame.messageCode() != L_data_ind && frame.messageCode() != L_data_req) {
return false;
}
if (IsBroadcastManagementRequest(frame)) {
return true;
}
if (frame.addressType() != IndividualAddress) {
return false;
}
const uint16_t dest = frame.destinationAddress();
const bool commissioning = !configured() || programmingMode();
return dest == individualAddress() || dest == tunnelClientAddress() ||
(commissioning && dest == kKnxUnconfiguredBroadcastAddress);
}
} // namespace gateway::openknx