Files
gateway/components/gateway_knx/src/gateway_knx_router_openknx.cpp
T

640 lines
27 KiB
C++

#include "gateway_knx_private.hpp"
#include "knx_device_broker.h"
namespace gateway {
void GatewayKnxTpIpRouter::selectOpenKnxNetworkInterface(const sockaddr_in& remote) {
const auto netif = SelectKnxNetifForRemote(remote);
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ != nullptr) {
ets_device_->setNetworkInterface(netif.has_value() ? netif->netif : nullptr);
}
if (oam_router_ != nullptr) {
oam_router_->setNetworkInterface(netif.has_value() ? netif->netif : nullptr);
}
}
bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t len,
TunnelClient* response_client,
uint16_t response_service,
const uint8_t* suppress_routing_echo,
size_t suppress_routing_echo_len) {
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);
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
if (frame.valid()) {
route_to_all_internal_instances = IsKnxBroadcastManagementRequest(frame);
if (!route_to_oam && oam_router_ != nullptr &&
(MatchesOamRouterLocalIndividualAddress(frame, *oam_router_) ||
oam_router_->matchesSecureSyncSerial(frame) ||
oam_router_->matchesRecentSecureToolAccess(frame))) {
route_to_oam = true;
}
}
}
if (route_to_all_internal_instances) {
SemaphoreGuard guard(openknx_lock_);
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, &response_broker,
suppress_routing_echo, suppress_routing_echo_len](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());
const bool routing_context =
response_client == nullptr && response_service == kServiceRoutingIndication;
const auto message_code = CemiMessageCode(response_data.data(), response_data.size());
if (routing_context && suppress_routing_echo != nullptr &&
IsLocalRoutingEchoIndication(response_data.data(), response_data.size(), suppress_routing_echo,
suppress_routing_echo_len)) {
return;
}
if (needs_tunnel_confirmation && !sent_tunnel_confirmation &&
message_code.has_value() && message_code.value() != L_data_con) {
sent_tunnel_confirmation = sendCemiFrameToClient(
*response_client, kServiceTunnellingRequest,
tunnel_confirmation.data(), tunnel_confirmation.size());
}
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_data.data(),
response_data.size());
} else if (routing_context) {
sendRoutingIndication(response_data.data(), response_data.size());
}
return;
}
if (message_code.has_value() && message_code.value() == L_data_con) {
if (routing_context) {
return;
}
if (response_client != nullptr && response_client->connected) {
sent_tunnel_confirmation =
sendCemiFrameToClient(*response_client, service, response_data.data(),
response_data.size()) ||
sent_tunnel_confirmation;
}
return;
}
if (routing_context) {
sendRoutingIndication(response_data.data(), response_data.size());
return;
}
if (response_client != nullptr && response_client->connected) {
sendCemiFrameToClient(*response_client, service, response_data.data(),
response_data.size());
return;
}
sendTunnelIndication(response_data.data(), response_data.size());
};
bool consumed = false;
if (ets_device_ != nullptr) {
consumed = ets_device_->handleLocalBroadcastManagementFrame(data, len, send_response) ||
consumed;
}
if (oam_router_ != nullptr) {
consumed = oam_router_->handleLocalBroadcastManagementFrame(data, len, send_response) ||
consumed;
}
if (needs_tunnel_confirmation && consumed && !sent_tunnel_confirmation) {
sendCemiFrameToClient(*response_client, kServiceTunnellingRequest,
tunnel_confirmation.data(), tunnel_confirmation.size());
}
syncOpenKnxConfigFromDevice();
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);
}
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ == nullptr) {
return false;
}
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;
const bool consumed = ets_device_->handleTunnelFrame(
data, len,
[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) {
if (response == nullptr || response_len == 0) {
return;
}
publishCloudCemiFrame(response, response_len);
const bool routing_context =
response_client == nullptr && response_service == kServiceRoutingIndication;
const auto message_code = CemiMessageCode(response, response_len);
if (routing_context && suppress_routing_echo != nullptr &&
IsLocalRoutingEchoIndication(response, response_len, suppress_routing_echo,
suppress_routing_echo_len)) {
return;
}
if (needs_tunnel_confirmation && !sent_tunnel_confirmation &&
message_code.has_value() && message_code.value() != L_data_con) {
sent_tunnel_confirmation = sendCemiFrameToClient(
*response_client, kServiceTunnellingRequest,
tunnel_confirmation.data(), tunnel_confirmation.size());
}
const uint16_t service = KnxIpServiceForCemi(response, response_len, response_service);
if (service == kServiceDeviceConfigurationRequest) {
if (response_client != nullptr && response_client->connected) {
sendCemiFrameToClient(*response_client, service, response, response_len);
} else if (routing_context) {
sendRoutingIndication(response, response_len);
}
return;
}
if (message_code.has_value() && message_code.value() == L_data_con) {
if (routing_context) {
return;
}
if (response_client != nullptr && response_client->connected) {
sent_tunnel_confirmation =
sendCemiFrameToClient(*response_client, service, response, response_len) ||
sent_tunnel_confirmation;
}
return;
}
if (routing_context) {
sendRoutingIndication(response, response_len);
return;
}
if (response_client != nullptr && response_client->connected) {
sendCemiFrameToClient(*response_client, service, response, response_len);
return;
}
sendTunnelIndication(response, response_len);
});
if (needs_tunnel_confirmation && consumed && !sent_tunnel_confirmation) {
sendCemiFrameToClient(*response_client, kServiceTunnellingRequest,
tunnel_confirmation.data(), tunnel_confirmation.size());
}
syncOpenKnxConfigFromDevice();
return consumed;
}
bool GatewayKnxTpIpRouter::handleOamRouterTunnelFrame(const uint8_t* data, size_t len,
TunnelClient* response_client,
uint16_t response_service,
const uint8_t* suppress_routing_echo,
size_t suppress_routing_echo_len) {
SemaphoreGuard guard(openknx_lock_);
if (oam_router_ == nullptr) {
return false;
}
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 bool consumed = oam_router_->handleTunnelFrame(
data, len,
[this, response_client, response_service, needs_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;
}
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_data.data(), response_data.size());
if (routing_context && suppress_routing_echo != nullptr &&
IsLocalRoutingEchoIndication(response_data.data(), response_data.size(), suppress_routing_echo,
suppress_routing_echo_len)) {
return;
}
if (needs_tunnel_confirmation && !sent_tunnel_confirmation &&
message_code.has_value() && message_code.value() != L_data_con) {
sent_tunnel_confirmation = sendCemiFrameToClient(
*response_client, kServiceTunnellingRequest,
tunnel_confirmation.data(), tunnel_confirmation.size());
}
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_data.data(),
response_data.size());
} else if (routing_context) {
sendRoutingIndication(response_data.data(), response_data.size());
}
return;
}
if (message_code.has_value() && message_code.value() == L_data_con) {
if (routing_context) {
return;
}
if (response_client != nullptr && response_client->connected) {
sent_tunnel_confirmation =
sendCemiFrameToClient(*response_client, service, response_data.data(),
response_data.size()) ||
sent_tunnel_confirmation;
}
return;
}
if (routing_context) {
sendRoutingIndication(response_data.data(), response_data.size());
return;
}
if (response_client != nullptr && response_client->connected) {
sendCemiFrameToClient(*response_client, service, response_data.data(),
response_data.size());
return;
}
sendTunnelIndication(response_data.data(), response_data.size());
});
if (needs_tunnel_confirmation && consumed && !sent_tunnel_confirmation) {
sendCemiFrameToClient(*response_client, kServiceTunnellingRequest,
tunnel_confirmation.data(), tunnel_confirmation.size());
}
return consumed;
}
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;
}
const bool sent = ets_device_->transmitTpFrame(data, len);
tp_uart_online_ = ets_device_->tpUartOnline();
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_) ||
oam_router_->matchesSecureSyncSerial(frame) ||
oam_router_->matchesRecentSecureToolAccess(frame));
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;
{
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ == nullptr) {
return false;
}
consumed = ets_device_->handleBusFrame(data, len);
syncOpenKnxConfigFromDevice();
}
if (consumed) {
publishCloudCemiFrame(data, len);
}
return consumed;
}
void GatewayKnxTpIpRouter::publishCloudCemiFrame(const uint8_t* data, size_t len) {
if (data == nullptr || len == 0 || !config_.oam_router.cloud_remote.enabled ||
!cloud_cemi_publisher_) {
return;
}
cloud_cemi_uplink_frames_.fetch_add(1, std::memory_order_relaxed);
cloud_cemi_publisher_(data, len);
}
bool GatewayKnxTpIpRouter::routeOpenKnxGroupWrite(const uint8_t* data, size_t len,
const char* context) {
const auto decoded = DecodeOpenKnxGroupWrite(data, len);
if (!decoded.has_value()) {
return false;
}
if (!shouldRouteDaliApplicationFrames()) {
return true;
}
const DaliBridgeResult result = group_write_handler_
? group_write_handler_(decoded->group_address,
decoded->data.data(),
decoded->data.size())
: bridge_.handleGroupWrite(decoded->group_address,
decoded->data.data(),
decoded->data.size());
if (!result.ok && !result.error.empty()) {
ESP_LOGD(kTag, "%s not routed to DALI: %s", context == nullptr ? "KNX group write" : context,
result.error.c_str());
}
return true;
}
bool GatewayKnxTpIpRouter::handleFunctionPropertyExtCommand(
uint16_t object_type, uint8_t object_instance, uint8_t property_id,
const uint8_t* data, size_t len, std::vector<uint8_t>* response) {
if (response == nullptr || object_type != kGroupObjectTableObjectType ||
property_id != kPidGoDiagnostics) {
return false;
}
const auto decoded = DecodeGoDiagnosticsGroupWrite(data, len);
if (!decoded.has_value()) {
const std::string payload = HexBytes(data, len);
ESP_LOGW(kTag,
"OpenKNX GO diagnostics write malformed objType=0x%04X objInst=%u property=0x%02X len=%u payload=%s",
static_cast<unsigned>(object_type), static_cast<unsigned>(object_instance),
static_cast<unsigned>(property_id), static_cast<unsigned>(len), payload.c_str());
*response = {ReturnCodes::DataVoid};
return true;
}
const std::string group_address_text =
GatewayKnxGroupAddressString(decoded->group_address);
const std::string payload = HexBytes(decoded->payload, decoded->payload_len);
ESP_LOGI(kTag,
"OpenKNX GO diagnostics group write ga=0x%04X (%s) len=%u payload=%s",
static_cast<unsigned>(decoded->group_address), group_address_text.c_str(),
static_cast<unsigned>(decoded->payload_len), payload.c_str());
if (!shouldRouteDaliApplicationFrames()) {
ESP_LOGW(kTag,
"OpenKNX GO diagnostics group write ga=0x%04X (%s) blocked by commissioning-only routing state",
static_cast<unsigned>(decoded->group_address), group_address_text.c_str());
*response = {ReturnCodes::TemporarilyNotAvailable};
return true;
}
const DaliBridgeResult result =
group_write_handler_ ? group_write_handler_(decoded->group_address, decoded->payload,
decoded->payload_len)
: bridge_.handleGroupWrite(decoded->group_address,
decoded->payload,
decoded->payload_len);
const uint8_t return_code = GoDiagnosticsReturnCode(result);
if (return_code == ReturnCodes::AddressVoid) {
ESP_LOGW(kTag,
"OpenKNX GO diagnostics group write ga=0x%04X (%s) returning AddressVoid: %s",
static_cast<unsigned>(decoded->group_address), group_address_text.c_str(),
result.error.empty() ? "unmapped KNX group address"
: result.error.c_str());
} else if (!result.ok) {
ESP_LOGW(kTag,
"OpenKNX GO diagnostics group write ga=0x%04X (%s) failed rc=0x%02X: %s",
static_cast<unsigned>(decoded->group_address), group_address_text.c_str(),
static_cast<unsigned>(return_code),
result.error.empty() ? "command routing failed" : result.error.c_str());
}
response->assign(1, return_code);
return true;
}
bool GatewayKnxTpIpRouter::handleFunctionPropertyExtState(
uint16_t object_type, uint8_t object_instance, uint8_t property_id,
const uint8_t* data, size_t len, std::vector<uint8_t>* response) {
if (response == nullptr || object_type != kGroupObjectTableObjectType ||
property_id != kPidGoDiagnostics) {
return false;
}
const auto decoded = DecodeGoDiagnosticsGroupWrite(data, len);
if (!decoded.has_value()) {
const std::string payload = HexBytes(data, len);
ESP_LOGW(kTag,
"OpenKNX GO diagnostics state request malformed objType=0x%04X objInst=%u property=0x%02X len=%u payload=%s",
static_cast<unsigned>(object_type), static_cast<unsigned>(object_instance),
static_cast<unsigned>(property_id), static_cast<unsigned>(len), payload.c_str());
*response = {ReturnCodes::DataVoid};
return true;
}
const std::string group_address_text =
GatewayKnxGroupAddressString(decoded->group_address);
ESP_LOGW(kTag,
"OpenKNX GO diagnostics state request unsupported ga=0x%04X (%s)",
static_cast<unsigned>(decoded->group_address), group_address_text.c_str());
*response = {ReturnCodes::InvalidCommand};
return true;
}
bool GatewayKnxTpIpRouter::emitOpenKnxGroupValue(uint16_t group_object_number,
const uint8_t* data, size_t len) {
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ == nullptr) {
return false;
}
const bool emitted = ets_device_->emitGroupValue(
group_object_number, data, len, [this](const uint8_t* frame_data, size_t frame_len) {
sendRoutingIndication(frame_data, frame_len);
sendTunnelIndication(frame_data, frame_len);
if (ets_device_ != nullptr) {
const bool sent_to_tp = ets_device_->transmitTpFrame(frame_data, frame_len);
tp_uart_online_ = sent_to_tp || ets_device_->tpUartOnline();
}
});
syncOpenKnxConfigFromDevice();
return emitted;
}
bool GatewayKnxTpIpRouter::shouldRouteDaliApplicationFrames() const {
if (!commissioning_only_) {
return true;
}
return openknx_configured_.load();
}
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;
}
const auto snapshot = ets_device_->snapshot();
openknx_configured_.store(snapshot.configured);
bool changed = false;
GatewayKnxConfig updated = config_;
if (snapshot.individual_address != 0 && snapshot.individual_address != 0xffff &&
snapshot.individual_address != updated.individual_address) {
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());
for (const auto& association : snapshot.associations) {
associations.push_back(GatewayKnxEtsAssociation{association.group_address,
association.group_object_number});
}
if (associations.size() != updated.ets_associations.size() ||
!std::equal(associations.begin(), associations.end(), updated.ets_associations.begin(),
[](const GatewayKnxEtsAssociation& lhs,
const GatewayKnxEtsAssociation& rhs) {
return lhs.group_address == rhs.group_address &&
lhs.group_object_number == rhs.group_object_number;
})) {
updated.ets_associations = std::move(associations);
changed = true;
}
}
if (!changed) {
return;
}
config_ = updated;
bridge_.setConfig(config_);
}
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;
}
return 0xff01;
}
uint16_t GatewayKnxTpIpRouter::effectiveKnxDeviceIndividualAddress() const {
if (ets_device_ != nullptr) {
const uint16_t address = ets_device_->individualAddress();
if (address != 0 && address != 0xffff) {
return address;
}
}
return config_.individual_address;
}
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) {
device = 1;
}
uint16_t address = static_cast<uint16_t>((interface_address & 0xff00) | device);
if (address == 0xffff) {
address = static_cast<uint16_t>((interface_address & 0xff00) | 0x0001);
}
return address;
}
} // namespace gateway