#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 frame_data(data, data + len); CemiFrame frame(frame_data.data(), static_cast(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 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 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 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 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 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 frame_data(data, data + len); CemiFrame frame(frame_data.data(), static_cast(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 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* 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(object_type), static_cast(object_instance), static_cast(property_id), static_cast(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(decoded->group_address), group_address_text.c_str(), static_cast(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(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(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(decoded->group_address), group_address_text.c_str(), static_cast(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* 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(object_type), static_cast(object_instance), static_cast(property_id), static_cast(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(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 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((interface_address & 0x00ff) + 1); if (device == 0 || device > 0xff) { device = 1; } uint16_t address = static_cast((interface_address & 0xff00) | device); if (address == 0xffff) { address = static_cast((interface_address & 0xff00) | 0x0001); } return address; } } // namespace gateway