#include "gateway_knx.hpp" #include "dali_define.hpp" #include "driver/gpio.h" #include "driver/uart.h" #include "esp_mac.h" #include "esp_netif.h" #include "esp_log.h" #include "lwip/inet.h" #include "lwip/sockets.h" #include "ets_device_runtime.h" #include "gateway_knx_internal.h" #include "soc/uart_periph.h" #include "tpuart_uart_interface.h" #include "knx/cemi_frame.h" #include "knx/knx_ip_connect_request.h" #include "knx/knx_ip_connect_response.h" #include "knx/knx_ip_config_request.h" #include "knx/knx_ip_description_request.h" #include "knx/knx_ip_routing_indication.h" #include "knx/knx_ip_disconnect_request.h" #include "knx/knx_ip_disconnect_response.h" #include "knx/knx_ip_search_request.h" #include "knx/knx_ip_search_response.h" #include "knx/knx_ip_description_response.h" #include "knx/knx_ip_state_request.h" #include "knx/knx_ip_state_response.h" #include "knx/knx_ip_tunneling_ack.h" #include "knx/knx_ip_tunneling_request.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace gateway { namespace { constexpr const char* kTag = "gateway_knx"; constexpr uint16_t kServiceSearchRequest = 0x0201; constexpr uint16_t kServiceSearchResponse = 0x0202; constexpr uint16_t kServiceDescriptionRequest = 0x0203; constexpr uint16_t kServiceDescriptionResponse = 0x0204; constexpr uint16_t kServiceConnectRequest = 0x0205; constexpr uint16_t kServiceConnectResponse = 0x0206; constexpr uint16_t kServiceConnectionStateRequest = 0x0207; constexpr uint16_t kServiceConnectionStateResponse = 0x0208; constexpr uint16_t kServiceDisconnectRequest = 0x0209; constexpr uint16_t kServiceDisconnectResponse = 0x020a; constexpr uint16_t kServiceSearchRequestExt = 0x020b; constexpr uint16_t kServiceSearchResponseExt = 0x020c; constexpr uint16_t kServiceDeviceConfigurationRequest = 0x0310; constexpr uint16_t kServiceDeviceConfigurationAck = 0x0311; constexpr uint16_t kServiceTunnellingRequest = 0x0420; constexpr uint16_t kServiceTunnellingAck = 0x0421; constexpr uint16_t kServiceRoutingIndication = 0x0530; constexpr uint16_t kServiceSecureWrapper = 0x0950; constexpr uint16_t kServiceSecureSessionRequest = 0x0951; constexpr uint16_t kServiceSecureSessionResponse = 0x0952; constexpr uint16_t kServiceSecureSessionAuth = 0x0953; constexpr uint16_t kServiceSecureSessionStatus = 0x0954; constexpr uint16_t kServiceSecureGroupSync = 0x0955; constexpr uint8_t kKnxNetIpHeaderSize = 0x06; constexpr uint8_t kKnxNetIpVersion10 = 0x10; constexpr uint8_t kKnxNoError = 0x00; constexpr uint8_t kKnxErrorConnectionId = 0x21; constexpr uint8_t kKnxErrorConnectionType = 0x22; constexpr uint8_t kKnxErrorNoMoreConnections = 0x24; constexpr uint8_t kKnxErrorTunnellingLayer = 0x29; constexpr uint8_t kKnxErrorSequenceNumber = 0x04; constexpr uint8_t kKnxSecureStatusAuthFailed = 0x01; constexpr uint8_t kKnxSecureStatusUnauthenticated = 0x02; constexpr uint8_t kKnxConnectionTypeDeviceManagement = 0x03; constexpr uint8_t kKnxConnectionTypeTunnel = 0x04; constexpr uint8_t kKnxTunnelLayerLink = 0x02; constexpr uint8_t kKnxHpaiIpv4Udp = 0x01; constexpr uint8_t kKnxHpaiIpv4Tcp = 0x02; constexpr uint8_t kKnxDibDeviceInfo = 0x01; constexpr uint8_t kKnxDibSupportedServices = 0x02; constexpr uint8_t kKnxDibIpConfig = 0x03; constexpr uint8_t kKnxDibCurrentIpConfig = 0x04; constexpr uint8_t kKnxDibKnxAddresses = 0x05; constexpr uint8_t kKnxDibTunnellingInfo = 0x07; constexpr uint8_t kKnxDibExtendedDeviceInfo = 0x08; constexpr uint8_t kKnxMediumTp1 = 0x02; constexpr uint8_t kKnxMediumIp = 0x20; constexpr uint8_t kKnxServiceFamilyCore = 0x02; constexpr uint8_t kKnxServiceFamilyDeviceManagement = 0x03; constexpr uint8_t kKnxServiceFamilyTunnelling = 0x04; constexpr uint8_t kKnxServiceFamilyRouting = 0x05; constexpr uint16_t kKnxIpOnlyDeviceDescriptor = 0x57b0; constexpr uint16_t kKnxTpIpInterfaceDeviceDescriptor = 0x091a; constexpr uint8_t kKnxIpAssignmentManual = 0x01; constexpr uint8_t kKnxIpCapabilityManual = 0x01; constexpr uint16_t kGwReg1AdrKoOffset = 12; constexpr uint16_t kGwReg1AdrKoBlockSize = 18; constexpr uint16_t kGwReg1GrpKoOffset = 1164; constexpr uint16_t kGwReg1GrpKoBlockSize = 17; constexpr uint16_t kGwReg1AppKoBroadcastSwitch = 1; constexpr uint16_t kGwReg1AppKoBroadcastDimm = 2; constexpr uint16_t kGwReg1AppKoScene = 5; constexpr uint8_t kGwReg1KoSwitch = 0; constexpr uint8_t kGwReg1KoDimmRelative = 2; constexpr uint8_t kGwReg1KoDimmAbsolute = 3; constexpr uint8_t kGwReg1KoColor = 6; constexpr uint8_t kGwReg1KoSwitchState = 1; constexpr uint8_t kGwReg1KoDimmState = 4; constexpr uint8_t kReg1SceneTelegramNumberMask = 0x3f; constexpr uint8_t kReg1SceneTelegramStoreMask = 0x80; constexpr size_t kReg1SceneEntryCount = 64; constexpr uint32_t kReg1SceneParamBlockOffset = 47; constexpr uint32_t kReg1SceneParamBlockSize = 4; constexpr uint8_t kReg1SceneTypeNone = 0; constexpr uint8_t kReg1SceneTypeAddress = 1; constexpr uint8_t kReg1SceneTypeGroup = 2; constexpr uint8_t kReg1SceneTypeBroadcast = 3; constexpr uint8_t kDaliCmdStepDownOff = 0x07; constexpr uint8_t kDaliCmdOnStepUp = 0x08; constexpr uint8_t kDaliCmdStopFade = 0xff; constexpr uint8_t kReg1DaliFunctionObjectIndex = 160; constexpr uint8_t kReg1DaliFunctionPropertyId = 1; constexpr uint8_t kReg1FunctionType = 2; constexpr uint8_t kReg1FunctionScan = 3; constexpr uint8_t kReg1FunctionAssign = 4; constexpr uint8_t kReg1FunctionEvgWrite = 10; constexpr uint8_t kReg1FunctionEvgRead = 11; constexpr uint8_t kReg1FunctionSetScene = 12; constexpr uint8_t kReg1FunctionGetScene = 13; constexpr uint8_t kReg1FunctionIdentify = 14; constexpr uint8_t kReg1DeviceTypeDt8 = 8; constexpr uint8_t kReg1ColorTypeTw = 1; constexpr uint8_t kDaliDeviceTypeNone = 0xfe; constexpr uint8_t kDaliDeviceTypeMultiple = 0xff; struct DecodedGroupWrite { uint16_t group_address{0}; std::vector data; }; struct KnxNetifInfo { const char* key{nullptr}; esp_netif_t* netif{nullptr}; uint32_t address{0}; uint32_t netmask{0}; uint32_t gateway{0}; }; class SemaphoreGuard { public: explicit SemaphoreGuard(SemaphoreHandle_t semaphore) : semaphore_(semaphore) { if (semaphore_ != nullptr) { xSemaphoreTake(semaphore_, portMAX_DELAY); locked_ = true; } } ~SemaphoreGuard() { if (locked_) { xSemaphoreGive(semaphore_); } } private: SemaphoreHandle_t semaphore_{nullptr}; bool locked_{false}; }; uint8_t DaliArcLevelToDpt5(uint8_t actual_level) { return static_cast( std::clamp(static_cast(std::lround(actual_level * 255.0 / 254.0)), 0, 255)); } uint16_t ReadBe16(const uint8_t* data) { return static_cast((static_cast(data[0]) << 8) | data[1]); } std::string EspErrDetail(const std::string& message, esp_err_t err) { return std::string(message) + ": " + esp_err_to_name(err) + "(" + std::to_string(err) + ")"; } std::string ErrnoDetail(const std::string& message, int err) { return std::string(message) + ": errno=" + std::to_string(err) + " (" + std::strerror(err) + ")"; } bool ResolveUartIoPin(uart_port_t uart_port, int configured_pin, uint32_t pin_index, int* resolved_pin) { if (resolved_pin == nullptr) { return false; } if (configured_pin >= 0) { *resolved_pin = configured_pin; return true; } if (uart_port < 0 || uart_port >= SOC_UART_NUM || pin_index >= SOC_UART_PINS_COUNT) { *resolved_pin = UART_PIN_NO_CHANGE; return false; } const int default_pin = uart_periph_signal[uart_port].pins[pin_index].default_gpio; if (default_pin < 0) { *resolved_pin = UART_PIN_NO_CHANGE; return false; } *resolved_pin = default_pin; return true; } std::string UartPinDescription(int configured_pin, int resolved_pin) { if (configured_pin >= 0) { return std::to_string(configured_pin); } if (resolved_pin >= 0) { return std::to_string(resolved_pin) + " (default from -1)"; } return "unrouted (-1 with no target default)"; } std::string Ipv4String(uint32_t network_address) { const uint32_t address = ntohl(network_address); char buffer[16]{}; std::snprintf(buffer, sizeof(buffer), "%u.%u.%u.%u", static_cast((address >> 24) & 0xff), static_cast((address >> 16) & 0xff), static_cast((address >> 8) & 0xff), static_cast(address & 0xff)); return buffer; } std::string EndpointString(const sockaddr_in& endpoint) { return Ipv4String(endpoint.sin_addr.s_addr) + ":" + std::to_string(ntohs(endpoint.sin_port)); } bool EndpointEquals(const sockaddr_in& lhs, const sockaddr_in& rhs) { return lhs.sin_family == rhs.sin_family && lhs.sin_addr.s_addr == rhs.sin_addr.s_addr && lhs.sin_port == rhs.sin_port; } void WriteIp(uint8_t* data, uint32_t network_address) { const uint32_t address = ntohl(network_address); data[0] = static_cast((address >> 24) & 0xff); data[1] = static_cast((address >> 16) & 0xff); data[2] = static_cast((address >> 8) & 0xff); data[3] = static_cast(address & 0xff); } bool ReadBaseMac(uint8_t* data) { if (data == nullptr) { return false; } if (esp_efuse_mac_get_default(data) == ESP_OK) { return true; } return esp_read_mac(data, ESP_MAC_WIFI_STA) == ESP_OK; } std::vector ActiveKnxNetifs() { std::vector out; constexpr std::array kIfKeys{"ETH_DEF", "WIFI_STA_DEF", "WIFI_AP_DEF"}; for (const char* key : kIfKeys) { esp_netif_t* netif = esp_netif_get_handle_from_ifkey(key); if (netif == nullptr || !esp_netif_is_netif_up(netif)) { continue; } esp_netif_ip_info_t ip_info{}; if (esp_netif_get_ip_info(netif, &ip_info) != ESP_OK || ip_info.ip.addr == 0) { continue; } out.push_back(KnxNetifInfo{key, netif, ip_info.ip.addr, ip_info.netmask.addr, ip_info.gw.addr}); } return out; } std::optional SelectKnxNetifForRemote(const sockaddr_in& remote) { const auto netifs = ActiveKnxNetifs(); if (netifs.empty()) { return std::nullopt; } const uint32_t remote_address = remote.sin_addr.s_addr; for (const auto& netif : netifs) { if ((remote_address & netif.netmask) == (netif.address & netif.netmask)) { return netif; } } return netifs.front(); } sockaddr_in EndpointFromOpenKnxHpai(const IpHostProtocolAddressInformation& hpai, const sockaddr_in& fallback) { sockaddr_in out = fallback; if (hpai.length() != LEN_IPHPAI || (hpai.code() != IPV4_UDP && hpai.code() != IPV4_TCP)) { return out; } const uint32_t address = hpai.ipAddress(); const uint16_t port = hpai.ipPortNumber(); if (address != 0) { out.sin_addr.s_addr = htonl(address); } if (port != 0) { out.sin_port = htons(port); } return out; } bool OpenKnxHpaiUsesUnsupportedProtocol(const IpHostProtocolAddressInformation& hpai, bool allow_tcp) { if (hpai.length() != LEN_IPHPAI) { return false; } const HostProtocolCode protocol = hpai.code(); return protocol != IPV4_UDP && !(allow_tcp && protocol == IPV4_TCP); } void WriteBe16(uint8_t* data, uint16_t value) { data[0] = static_cast((value >> 8) & 0xff); data[1] = static_cast(value & 0xff); } std::optional ObjectIntAny(const DaliValue::Object& object, std::initializer_list keys) { for (const char* key : keys) { if (const auto value = getObjectInt(object, key)) { return value; } } return std::nullopt; } std::optional ObjectBoolAny(const DaliValue::Object& object, std::initializer_list keys) { for (const char* key : keys) { if (const auto value = getObjectBool(object, key)) { return value; } } return std::nullopt; } std::optional ObjectStringAny(const DaliValue::Object& object, std::initializer_list keys) { for (const char* key : keys) { if (const auto value = getObjectString(object, key)) { return value; } } return std::nullopt; } const DaliValue* ObjectValueAny(const DaliValue::Object& object, std::initializer_list keys) { for (const char* key : keys) { if (const auto* value = getObjectValue(object, key)) { return value; } } return nullptr; } std::string NormalizeModeString(std::string value) { value.erase(std::remove_if(value.begin(), value.end(), [](unsigned char ch) { return ch == '_' || ch == '-' || std::isspace(ch) != 0; }), value.end()); std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) { return static_cast(std::tolower(ch)); }); return value; } std::optional ParseGroupAddressString(const std::string& value) { int parts[3] = {-1, -1, -1}; size_t start = 0; for (int index = 0; index < 3; ++index) { const size_t slash = value.find('/', start); const bool last = index == 2; if ((slash == std::string::npos) != last) { return std::nullopt; } const std::string token = value.substr(start, last ? std::string::npos : slash - start); if (token.empty()) { return std::nullopt; } char* end = nullptr; errno = 0; const long parsed = std::strtol(token.c_str(), &end, 10); if (errno != 0 || end == token.c_str() || *end != '\0') { return std::nullopt; } parts[index] = static_cast(parsed); start = slash + 1; } if (parts[0] < 0 || parts[0] > 31 || parts[1] < 0 || parts[1] > 7 || parts[2] < 0 || parts[2] > 255) { return std::nullopt; } return static_cast(((parts[0] & 0x1f) << 11) | ((parts[1] & 0x07) << 8) | (parts[2] & 0xff)); } std::optional ObjectGroupAddressAny(const DaliValue::Object& object, std::initializer_list keys) { for (const char* key : keys) { const auto* value = getObjectValue(object, key); if (value == nullptr) { continue; } if (const auto raw = value->asInt()) { if (raw.value() >= 0 && raw.value() <= 0xffff) { return static_cast(raw.value()); } } if (const auto raw = value->asString()) { if (const auto parsed = ParseGroupAddressString(raw.value())) { return parsed.value(); } } } return std::nullopt; } std::vector ParseEtsAssociations(const DaliValue::Object& object) { std::vector associations; const auto* raw_associations = ObjectValueAny( object, {"etsAssociations", "ets_associations", "etsBindings", "ets_bindings", "associationTable", "association_table"}); const auto* array = raw_associations == nullptr ? nullptr : raw_associations->asArray(); if (array == nullptr) { return associations; } associations.reserve(array->size()); for (const auto& item : *array) { const auto* entry = item.asObject(); if (entry == nullptr) { continue; } const auto group_address = ObjectGroupAddressAny( *entry, {"groupAddress", "group_address", "address", "rawAddress", "raw_address"}); const auto object_number = ObjectIntAny( *entry, {"objectNumber", "object_number", "groupObjectNumber", "group_object_number", "ko", "asap"}); if (!group_address.has_value() || !object_number.has_value() || object_number.value() < 0 || object_number.value() > kGwReg1GrpKoOffset + (kGwReg1GrpKoBlockSize * 16)) { continue; } associations.push_back(GatewayKnxEtsAssociation{ group_address.value(), static_cast(object_number.value())}); } return associations; } std::string TargetName(const GatewayKnxDaliTarget& target) { switch (target.kind) { case GatewayKnxDaliTargetKind::kBroadcast: return "Broadcast"; case GatewayKnxDaliTargetKind::kShortAddress: return "A" + std::to_string(target.address); case GatewayKnxDaliTargetKind::kGroup: return "Group " + std::to_string(target.address); case GatewayKnxDaliTargetKind::kNone: default: return "Unmapped"; } } std::string DataTypeName(GatewayKnxDaliDataType data_type) { switch (data_type) { case GatewayKnxDaliDataType::kSwitch: return "Switch"; case GatewayKnxDaliDataType::kBrightness: return "Dimmer"; case GatewayKnxDaliDataType::kBrightnessRelative: return "Dimmer Relative"; case GatewayKnxDaliDataType::kColorTemperature: return "Color Temperature"; case GatewayKnxDaliDataType::kRgb: return "RGB"; case GatewayKnxDaliDataType::kScene: return "Scene"; case GatewayKnxDaliDataType::kUnknown: default: return "Unknown"; } } const char* DataTypeDpt(GatewayKnxDaliDataType data_type) { switch (data_type) { case GatewayKnxDaliDataType::kSwitch: return "DPST-1-1"; case GatewayKnxDaliDataType::kBrightness: return "DPST-5-1"; case GatewayKnxDaliDataType::kBrightnessRelative: return "DPST-3-7"; case GatewayKnxDaliDataType::kColorTemperature: return "DPST-7-600"; case GatewayKnxDaliDataType::kRgb: return "DPST-232-600"; case GatewayKnxDaliDataType::kScene: return "DPST-17-1"; case GatewayKnxDaliDataType::kUnknown: default: return ""; } } std::optional DecodeOpenKnxGroupWrite(const uint8_t* data, size_t len) { if (data == nullptr || len < 10) { return std::nullopt; } std::vector frame_data(data, data + len); CemiFrame frame(frame_data.data(), static_cast(frame_data.size())); if (!frame.valid()) { return std::nullopt; } const MessageCode message_code = frame.messageCode(); if (message_code != L_data_req && message_code != L_data_ind && message_code != L_data_con) { return std::nullopt; } if (frame.addressType() != GroupAddress) { return std::nullopt; } const TpduType tpdu_type = frame.tpdu().type(); if (tpdu_type != DataGroup && tpdu_type != DataBroadcast) { return std::nullopt; } if (frame.apdu().type() != GroupValueWrite) { return std::nullopt; } DecodedGroupWrite out; out.group_address = frame.destinationAddress(); const uint8_t apdu_length = frame.apdu().length(); const uint8_t* apdu_data = frame.apdu().data(); if (apdu_data == nullptr || apdu_length == 0) { return std::nullopt; } if (apdu_length == 1U) { out.data.push_back(apdu_data[0] & 0x3f); } else { out.data.assign(apdu_data + 1, apdu_data + apdu_length); } return out; } uint8_t Reg1PercentToArc(uint8_t value) { if (value == 0 || value == 0xff) { return value; } const double arc = ((253.0 / 3.0) * (std::log10(static_cast(value)) + 1.0)) + 1.0; return static_cast(std::clamp(static_cast(arc + 0.5), 0, 254)); } uint8_t Reg1ArcToPercent(uint8_t value) { if (value == 0 || value == 0xff) { return value; } const double percent = std::pow(10.0, ((static_cast(value) - 1.0) / (253.0 / 3.0)) - 1.0); return static_cast(std::clamp(static_cast(percent + 0.5), 0, 100)); } GatewayKnxDaliTarget Reg1SceneTarget(uint8_t encoded_target) { if ((encoded_target & 0x80) != 0) { return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kGroup, static_cast(encoded_target & 0x0f)}; } return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kShortAddress, static_cast(encoded_target & 0x3f)}; } DaliBridgeRequest FunctionRequest(const char* sequence, BridgeOperation operation) { DaliBridgeRequest request; request.sequence = sequence == nullptr ? "knx-function-property" : sequence; request.operation = operation; return request; } void ApplyTargetToRequest(const GatewayKnxDaliTarget& target, DaliBridgeRequest* request) { if (request == nullptr) { return; } switch (target.kind) { case GatewayKnxDaliTargetKind::kBroadcast: request->metadata["broadcast"] = true; break; case GatewayKnxDaliTargetKind::kShortAddress: request->shortAddress = target.address; break; case GatewayKnxDaliTargetKind::kGroup: request->metadata["group"] = target.address; break; case GatewayKnxDaliTargetKind::kNone: default: break; } } DaliBridgeResult ExecuteRaw(DaliBridgeEngine& engine, BridgeOperation operation, uint8_t addr, uint8_t cmd, const char* sequence) { DaliBridgeRequest request = FunctionRequest(sequence, operation); request.rawAddress = addr; request.rawCommand = cmd; return engine.execute(request); } std::optional QueryShort(DaliBridgeEngine& engine, uint8_t short_address, uint8_t command, const char* sequence) { const auto result = ExecuteRaw(engine, BridgeOperation::query, DaliComm::toCmdAddr(short_address), command, sequence); if (!result.ok || !result.data.has_value()) { return std::nullopt; } return result.data.value(); } bool SendRaw(DaliBridgeEngine& engine, uint8_t addr, uint8_t cmd, const char* sequence) { return ExecuteRaw(engine, BridgeOperation::send, addr, cmd, sequence).ok; } bool SendRawExt(DaliBridgeEngine& engine, uint8_t addr, uint8_t cmd, const char* sequence) { return ExecuteRaw(engine, BridgeOperation::sendExt, addr, cmd, sequence).ok; } std::optional ExecuteRawQuery(DaliBridgeEngine& engine, uint8_t addr, uint8_t cmd, const char* sequence) { const auto result = ExecuteRaw(engine, BridgeOperation::query, addr, cmd, sequence); if (!result.ok || !result.data.has_value()) { return std::nullopt; } return result.data.value(); } std::optional RawCommandAddressForTarget(const GatewayKnxDaliTarget& target) { switch (target.kind) { case GatewayKnxDaliTargetKind::kBroadcast: return static_cast(0xff); case GatewayKnxDaliTargetKind::kShortAddress: if (target.address < 0 || target.address > 63) { return std::nullopt; } return DaliComm::toCmdAddr(target.address); case GatewayKnxDaliTargetKind::kGroup: if (target.address < 0 || target.address > 15) { return std::nullopt; } return static_cast(0x80 + (target.address * 2) + 1); case GatewayKnxDaliTargetKind::kNone: default: return std::nullopt; } } DaliBridgeResult SendRawForTarget(DaliBridgeEngine& engine, uint16_t group_address, const GatewayKnxDaliTarget& target, uint8_t cmd) { const auto raw_addr = RawCommandAddressForTarget(target); if (!raw_addr.has_value()) { DaliBridgeResult result; result.sequence = "knx-" + GatewayKnxGroupAddressString(group_address); result.error = "invalid DALI target for raw command"; return result; } DaliBridgeRequest request; request.sequence = "knx-" + GatewayKnxGroupAddressString(group_address); request.operation = BridgeOperation::send; request.rawAddress = raw_addr.value(); request.rawCommand = cmd; request.metadata["sourceProtocol"] = "knx"; request.metadata["knxGroupAddress"] = GatewayKnxGroupAddressString(group_address); request.metadata["daliTarget"] = TargetName(target); return engine.execute(request); } DaliBridgeResult SendRawExtForTarget(DaliBridgeEngine& engine, uint16_t group_address, const GatewayKnxDaliTarget& target, uint8_t cmd) { const auto raw_addr = RawCommandAddressForTarget(target); if (!raw_addr.has_value()) { DaliBridgeResult result; result.sequence = "knx-" + GatewayKnxGroupAddressString(group_address); result.error = "invalid DALI target for raw command"; return result; } DaliBridgeRequest request; request.sequence = "knx-" + GatewayKnxGroupAddressString(group_address); request.operation = BridgeOperation::sendExt; request.rawAddress = raw_addr.value(); request.rawCommand = cmd; request.metadata["sourceProtocol"] = "knx"; request.metadata["knxGroupAddress"] = GatewayKnxGroupAddressString(group_address); request.metadata["daliTarget"] = TargetName(target); return engine.execute(request); } std::optional MetadataInt(const DaliBridgeResult& result, const std::string& key) { return getObjectInt(result.metadata, key); } std::string HexBytes(const uint8_t* data, size_t len) { if (data == nullptr || len == 0) { return {}; } std::string out; out.reserve(len * 3); char buffer[4] = {0}; for (size_t index = 0; index < len; ++index) { std::snprintf(buffer, sizeof(buffer), "%02X", data[index]); out += buffer; if (index + 1 < len) { out.push_back(' '); } } return out; } std::optional CemiMessageCode(const uint8_t* data, size_t len) { if (data == nullptr || len == 0) { return std::nullopt; } return static_cast(data[0]); } uint16_t KnxIpServiceForCemi(const uint8_t* data, size_t len, uint16_t fallback_service) { const auto message_code = CemiMessageCode(data, len); if (!message_code.has_value()) { return fallback_service; } switch (message_code.value()) { case L_data_req: case L_data_con: case L_data_ind: return kServiceTunnellingRequest; default: return kServiceDeviceConfigurationRequest; } } bool MatchesOpenKnxLocalIndividualAddress(const CemiFrame& frame, const openknx::EtsDeviceRuntime& ets_device) { if (frame.addressType() != IndividualAddress) { return false; } const uint16_t dest = frame.destinationAddress(); const uint16_t own_address = ets_device.individualAddress(); const uint16_t client_address = ets_device.tunnelClientAddress(); const bool commissioning = !ets_device.configured() || ets_device.programmingMode(); return dest == own_address || dest == client_address || (commissioning && dest == 0xffff); } bool BuildLocalRoutingTunnelFrame(const uint8_t* data, size_t len, std::vector* local_frame) { if (data == nullptr || local_frame == nullptr || len < 2) { return false; } std::vector frame_data(data, data + len); CemiFrame frame(frame_data.data(), static_cast(frame_data.size())); if (!frame.valid()) { return false; } switch (frame.messageCode()) { case L_data_req: *local_frame = std::move(frame_data); return true; case L_data_ind: frame.messageCode(L_data_req); *local_frame = std::move(frame_data); return true; default: return false; } } bool IsLocalRoutingEchoIndication(const uint8_t* response, size_t response_len, const uint8_t* original_request, size_t original_request_len) { if (response == nullptr || original_request == nullptr || response_len < 2 || original_request_len < 2) { return false; } std::vector response_data(response, response + response_len); std::vector original_data(original_request, original_request + original_request_len); CemiFrame response_frame(response_data.data(), static_cast(response_data.size())); CemiFrame original_frame(original_data.data(), static_cast(original_data.size())); if (!response_frame.valid() || !original_frame.valid() || response_frame.messageCode() != L_data_ind || original_frame.addressType() != IndividualAddress || response_frame.addressType() != original_frame.addressType() || response_frame.sourceAddress() != original_frame.sourceAddress() || response_frame.destinationAddress() != original_frame.destinationAddress() || response_frame.tpdu().type() != original_frame.tpdu().type() || response_frame.apdu().type() != original_frame.apdu().type()) { return false; } const uint8_t response_apdu_length = response_frame.apdu().length(); const uint8_t original_apdu_length = original_frame.apdu().length(); if (response_apdu_length != original_apdu_length) { return false; } const uint8_t* response_apdu = response_frame.apdu().data(); const uint8_t* original_apdu = original_frame.apdu().data(); if (response_apdu_length == 0) { return true; } if (response_apdu == nullptr || original_apdu == nullptr) { return false; } return std::memcmp(response_apdu, original_apdu, response_apdu_length) == 0; } bool BuildTunnelConfirmationFrame(const uint8_t* data, size_t len, std::vector* confirmation) { if (data == nullptr || confirmation == nullptr || len < 2) { return false; } std::vector frame_data(data, data + len); CemiFrame frame(frame_data.data(), static_cast(frame_data.size())); if (!frame.valid() || frame.messageCode() != L_data_req) { return false; } frame.messageCode(L_data_con); frame.confirm(ConfirmNoError); *confirmation = std::move(frame_data); return true; } DaliBridgeRequest RequestForTarget(uint16_t group_address, const GatewayKnxDaliTarget& target, BridgeOperation operation) { DaliBridgeRequest request; request.sequence = "knx-" + GatewayKnxGroupAddressString(group_address); request.operation = operation; switch (target.kind) { case GatewayKnxDaliTargetKind::kBroadcast: request.metadata["broadcast"] = true; break; case GatewayKnxDaliTargetKind::kShortAddress: request.shortAddress = target.address; break; case GatewayKnxDaliTargetKind::kGroup: request.metadata["group"] = target.address; break; case GatewayKnxDaliTargetKind::kNone: default: break; } request.metadata["sourceProtocol"] = "knx"; request.metadata["knxGroupAddress"] = GatewayKnxGroupAddressString(group_address); return request; } DaliBridgeResult ErrorResult(uint16_t group_address, const char* message) { DaliBridgeResult result; result.sequence = "knx-" + GatewayKnxGroupAddressString(group_address); result.error = message == nullptr ? "KNX error" : message; return result; } DaliBridgeResult IgnoredResult(uint16_t group_address, uint16_t group_object_number, const char* reason) { DaliBridgeResult result; result.sequence = "knx-" + GatewayKnxGroupAddressString(group_address); result.ok = true; result.metadata["ignored"] = true; result.metadata["groupObjectNumber"] = static_cast(group_object_number); result.metadata["reason"] = reason == nullptr ? "ignored" : reason; return result; } bool SetSearchAddress(DaliBridgeEngine& engine, uint32_t search_address, const char* sequence) { return SendRaw(engine, DALI_CMD_SPECIAL_SEARCHADDRH, static_cast((search_address >> 16) & 0xff), sequence) && SendRaw(engine, DALI_CMD_SPECIAL_SEARCHADDRM, static_cast((search_address >> 8) & 0xff), sequence) && SendRaw(engine, DALI_CMD_SPECIAL_SEARCHADDRL, static_cast(search_address & 0xff), sequence); } std::optional CompareSelectedSearchAddress(DaliBridgeEngine& engine, uint32_t search_address, const char* sequence) { if (!SetSearchAddress(engine, search_address, sequence)) { return std::nullopt; } const auto raw = ExecuteRawQuery(engine, DALI_CMD_SPECIAL_COMPARE, DALI_CMD_OFF, sequence); if (!raw.has_value()) { return std::nullopt; } return raw.value() == 0xff; } std::optional FindLowestSelectedRandomAddress(DaliBridgeEngine& engine) { const auto any = CompareSelectedSearchAddress(engine, 0x00ffffffu, "knx-function-scan-compare-any"); if (!any.has_value() || !any.value()) { return std::nullopt; } uint32_t low = 0; uint32_t high = 0x00ffffffu; while (low < high) { const uint32_t mid = low + ((high - low) / 2); const auto match = CompareSelectedSearchAddress(engine, mid, "knx-function-scan-compare-binary"); if (!match.has_value()) { return std::nullopt; } if (match.value()) { high = mid; } else { low = mid + 1; } } if (!SetSearchAddress(engine, low, "knx-function-scan-compare-final")) { return std::nullopt; } return low; } std::optional QuerySelectedShortAddress(DaliBridgeEngine& engine) { const auto raw = ExecuteRawQuery(engine, DALI_CMD_SPECIAL_QUERY_SHORT_ADDRESS, DALI_CMD_OFF, "knx-function-scan-query-short"); if (!raw.has_value() || raw.value() < 0 || raw.value() > 0xff || raw.value() == 0xff) { return std::nullopt; } return static_cast((raw.value() >> 1) & 0x3f); } bool VerifyShortAddress(DaliBridgeEngine& engine, uint8_t short_address) { const auto raw = ExecuteRawQuery(engine, DALI_CMD_SPECIAL_VERIFY_SHORT_ADDRESS, DaliComm::toCmdAddr(short_address), "knx-function-scan-verify-short"); return raw.has_value() && raw.value() == 0xff; } std::array QueryUsedShortAddresses(DaliBridgeEngine& engine) { std::array used{}; for (int short_address = 0; short_address < static_cast(used.size()); ++short_address) { used[short_address] = QueryShort(engine, static_cast(short_address), DALI_CMD_QUERY_STATUS, "knx-function-scan-query-used") .has_value(); } return used; } std::optional NextFreeShortAddress(const std::array& used) { for (size_t index = 0; index < used.size(); ++index) { if (!used[index]) { return static_cast(index); } } return std::nullopt; } uint8_t Reg1SceneTypeForEntry(const openknx::EtsDeviceRuntime& runtime, size_t index) { const uint32_t addr = kReg1SceneParamBlockOffset + (kReg1SceneParamBlockSize * static_cast(index)); return static_cast((runtime.paramByte(addr) >> 6) & 0x03); } bool Reg1SceneSaveAllowedForEntry(const openknx::EtsDeviceRuntime& runtime, size_t index) { const uint32_t addr = kReg1SceneParamBlockOffset + (kReg1SceneParamBlockSize * static_cast(index)); return runtime.paramBit(addr, 2); } uint8_t Reg1KnxSceneNumberForEntry(const openknx::EtsDeviceRuntime& runtime, size_t index) { const uint32_t addr = kReg1SceneParamBlockOffset + (kReg1SceneParamBlockSize * static_cast(index)) + 1; return static_cast((runtime.paramByte(addr) >> 1) & 0x7f); } uint8_t Reg1DaliSceneNumberForEntry(const openknx::EtsDeviceRuntime& runtime, size_t index) { const uint32_t addr = kReg1SceneParamBlockOffset + (kReg1SceneParamBlockSize * static_cast(index)); return static_cast((runtime.paramByte(addr) >> 1) & 0x0f); } std::optional Reg1SceneTargetForEntry( const openknx::EtsDeviceRuntime& runtime, size_t index) { const uint32_t base = kReg1SceneParamBlockOffset + (kReg1SceneParamBlockSize * static_cast(index)); switch (Reg1SceneTypeForEntry(runtime, index)) { case kReg1SceneTypeAddress: return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kShortAddress, static_cast((runtime.paramByte(base + 2) >> 2) & 0x3f)}; case kReg1SceneTypeGroup: return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kGroup, static_cast((runtime.paramByte(base + 3) >> 4) & 0x0f)}; case kReg1SceneTypeBroadcast: return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kBroadcast, 127}; case kReg1SceneTypeNone: default: return std::nullopt; } } bool SendAll(int sock, const uint8_t* data, size_t len, const sockaddr_in& remote) { return sendto(sock, data, len, 0, reinterpret_cast(&remote), sizeof(remote)) == static_cast(len); } bool SendStream(int sock, const uint8_t* data, size_t len) { size_t sent = 0; while (sent < len) { const int written = send(sock, data + sent, len - sent, 0); if (written <= 0) { return false; } sent += static_cast(written); } return true; } std::vector OpenKnxIpPacket(uint16_t service, const std::vector& body) { KnxIpFrame frame(static_cast(LEN_KNXIP_HEADER + body.size())); frame.serviceTypeIdentifier(service); std::copy(body.begin(), body.end(), frame.data() + LEN_KNXIP_HEADER); return std::vector(frame.data(), frame.data() + frame.totalLength()); } bool ParseKnxNetIpHeader(const uint8_t* data, size_t len, uint16_t* service, uint16_t* total_len) { if (data == nullptr || len < 6 || data[0] != kKnxNetIpHeaderSize || data[1] != kKnxNetIpVersion10) { return false; } *service = ReadBe16(data + 2); *total_len = ReadBe16(data + 4); return *total_len >= 6 && *total_len <= len; } bool IsKnxNetIpSecureService(uint16_t service) { switch (service) { case kServiceSecureWrapper: case kServiceSecureSessionRequest: case kServiceSecureSessionResponse: case kServiceSecureSessionAuth: case kServiceSecureSessionStatus: case kServiceSecureGroupSync: return true; default: return false; } } } // namespace std::optional GatewayKnxConfigFromValue(const DaliValue* value) { if (value == nullptr || value->asObject() == nullptr) { return std::nullopt; } const auto& object = *value->asObject(); GatewayKnxConfig config; config.dali_router_enabled = ObjectBoolAny(object, {"daliRouterEnabled", "dali_router_enabled"}) .value_or(config.dali_router_enabled); config.ip_router_enabled = ObjectBoolAny(object, {"ipRouterEnabled", "ip_router_enabled"}) .value_or(config.ip_router_enabled); config.tunnel_enabled = ObjectBoolAny(object, {"tunnelEnabled", "tunnel_enabled"}) .value_or(config.tunnel_enabled); config.multicast_enabled = ObjectBoolAny(object, {"multicastEnabled", "multicast_enabled"}) .value_or(config.multicast_enabled); if (const auto mode = ObjectStringAny(object, {"mappingMode", "mapping_mode"})) { config.mapping_mode = GatewayKnxMappingModeFromString(mode.value()); } config.ets_database_enabled = ObjectBoolAny(object, {"etsDatabaseEnabled", "ets_database_enabled"}) .value_or(config.ets_database_enabled); config.ets_associations = ParseEtsAssociations(object); config.main_group = static_cast( std::clamp(ObjectIntAny(object, {"mainGroup", "main_group"}).value_or(config.main_group), 0, 31)); config.dali_bus_id = static_cast(std::clamp( ObjectIntAny(object, {"daliBusId", "dali_bus_id", "targetDaliBusId", "target_dali_bus_id"}) .value_or(config.dali_bus_id), 0, 15)); config.udp_port = static_cast(std::clamp( ObjectIntAny(object, {"udpPort", "port", "udp_port"}).value_or(config.udp_port), 1, 65535)); config.multicast_address = ObjectStringAny(object, {"multicastAddress", "multicast_address"}) .value_or(config.multicast_address); config.ip_interface_individual_address = static_cast(std::clamp( ObjectIntAny(object, {"ipInterfaceIndividualAddress", "ip_interface_individual_address", "ipInterfaceAddress", "ip_interface_address"}) .value_or(config.ip_interface_individual_address), 0, 0xffff)); config.individual_address = static_cast(std::clamp( ObjectIntAny(object, {"individualAddress", "individual_address", "knxDaliGatewayIndividualAddress", "knx_dali_gateway_individual_address", "deviceIndividualAddress", "device_individual_address"}) .value_or(config.individual_address), 0, 0xffff)); config.programming_button_gpio = std::clamp( ObjectIntAny(object, {"programmingButtonGpio", "programming_button_gpio"}) .value_or(config.programming_button_gpio), -1, 48); config.programming_button_active_low = ObjectBoolAny(object, {"programmingButtonActiveLow", "programming_button_active_low"}) .value_or(config.programming_button_active_low); config.programming_led_gpio = std::clamp( ObjectIntAny(object, {"programmingLedGpio", "programming_led_gpio"}) .value_or(config.programming_led_gpio), -1, 48); config.programming_led_active_high = ObjectBoolAny(object, {"programmingLedActiveHigh", "programming_led_active_high"}) .value_or(config.programming_led_active_high); const auto* tp_uart = getObjectValue(object, "tpUart"); if (tp_uart == nullptr) { tp_uart = getObjectValue(object, "tp_uart"); } if (tp_uart != nullptr && tp_uart->asObject() != nullptr) { const auto& serial = *tp_uart->asObject(); config.tp_uart.uart_port = std::clamp( ObjectIntAny(serial, {"uartPort", "uart_port"}).value_or(config.tp_uart.uart_port), -1, 2); config.tp_uart.tx_pin = ObjectIntAny(serial, {"txPin", "tx_pin"}).value_or(config.tp_uart.tx_pin); config.tp_uart.rx_pin = ObjectIntAny(serial, {"rxPin", "rx_pin"}).value_or(config.tp_uart.rx_pin); config.tp_uart.baudrate = static_cast(std::max( 1200, ObjectIntAny(serial, {"baudrate", "baud"}).value_or(config.tp_uart.baudrate))); config.tp_uart.rx_buffer_size = static_cast(std::max( 128, ObjectIntAny(serial, {"rxBufferSize", "rx_buffer_size"}) .value_or(static_cast(config.tp_uart.rx_buffer_size)))); config.tp_uart.tx_buffer_size = static_cast(std::max( 128, ObjectIntAny(serial, {"txBufferSize", "tx_buffer_size"}) .value_or(static_cast(config.tp_uart.tx_buffer_size)))); config.tp_uart.startup_timeout_ms = static_cast(std::max( 0, ObjectIntAny(serial, {"startupTimeoutMs", "startup_timeout_ms"}) .value_or(static_cast(config.tp_uart.startup_timeout_ms)))); config.tp_uart.read_timeout_ms = static_cast(std::max( 1, ObjectIntAny(serial, {"readTimeoutMs", "read_timeout_ms"}) .value_or(static_cast(config.tp_uart.read_timeout_ms)))); config.tp_uart.nine_bit_mode = ObjectBoolAny( serial, {"nineBitMode", "nine_bit_mode", "use9BitMode", "use_9_bit_mode"}) .value_or(config.tp_uart.nine_bit_mode); } return config; } DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config) { DaliValue::Object out; out["daliRouterEnabled"] = config.dali_router_enabled; out["ipRouterEnabled"] = config.ip_router_enabled; out["tunnelEnabled"] = config.tunnel_enabled; out["multicastEnabled"] = config.multicast_enabled; out["etsDatabaseEnabled"] = config.ets_database_enabled; out["mappingMode"] = GatewayKnxMappingModeToString(config.mapping_mode); out["mainGroup"] = static_cast(config.main_group); out["daliBusId"] = static_cast(config.dali_bus_id); out["udpPort"] = static_cast(config.udp_port); out["multicastAddress"] = config.multicast_address; out["ipInterfaceIndividualAddress"] = static_cast(config.ip_interface_individual_address); out["individualAddress"] = static_cast(config.individual_address); out["programmingButtonGpio"] = config.programming_button_gpio; out["programmingButtonActiveLow"] = config.programming_button_active_low; out["programmingLedGpio"] = config.programming_led_gpio; out["programmingLedActiveHigh"] = config.programming_led_active_high; DaliValue::Object serial; serial["uartPort"] = config.tp_uart.uart_port; serial["txPin"] = config.tp_uart.tx_pin; serial["rxPin"] = config.tp_uart.rx_pin; serial["baudrate"] = static_cast(config.tp_uart.baudrate); serial["rxBufferSize"] = static_cast(config.tp_uart.rx_buffer_size); serial["txBufferSize"] = static_cast(config.tp_uart.tx_buffer_size); serial["startupTimeoutMs"] = static_cast(config.tp_uart.startup_timeout_ms); serial["readTimeoutMs"] = static_cast(config.tp_uart.read_timeout_ms); serial["nineBitMode"] = config.tp_uart.nine_bit_mode; out["tpUart"] = std::move(serial); DaliValue::Array ets_associations; ets_associations.reserve(config.ets_associations.size()); for (const auto& association : config.ets_associations) { DaliValue::Object entry; entry["groupAddress"] = static_cast(association.group_address); entry["groupObjectNumber"] = static_cast(association.group_object_number); ets_associations.emplace_back(std::move(entry)); } out["etsAssociations"] = std::move(ets_associations); return DaliValue(std::move(out)); } bool GatewayKnxConfigUsesTpUart(const GatewayKnxConfig& config) { return config.ip_router_enabled && config.tp_uart.uart_port >= 0; } const char* GatewayKnxMappingModeToString(GatewayKnxMappingMode mode) { switch (mode) { case GatewayKnxMappingMode::kEtsDatabase: return "ets_database"; case GatewayKnxMappingMode::kGwReg1Direct: return "gw_reg1_direct"; case GatewayKnxMappingMode::kManual: return "manual"; case GatewayKnxMappingMode::kFormula: default: return "formula"; } } GatewayKnxMappingMode GatewayKnxMappingModeFromString(const std::string& value) { const std::string normalized = NormalizeModeString(value); if (normalized == "gwreg1direct" || normalized == "gwreg1" || normalized == "gwreg1channel" || normalized == "channelindex") { return GatewayKnxMappingMode::kGwReg1Direct; } if (normalized == "manual" || normalized == "database" || normalized == "db") { return GatewayKnxMappingMode::kManual; } if (normalized == "etsdatabase" || normalized == "ets" || normalized == "openknx") { return GatewayKnxMappingMode::kEtsDatabase; } return GatewayKnxMappingMode::kFormula; } const char* GatewayKnxDataTypeToString(GatewayKnxDaliDataType data_type) { switch (data_type) { case GatewayKnxDaliDataType::kSwitch: return "switch"; case GatewayKnxDaliDataType::kBrightness: return "brightness"; case GatewayKnxDaliDataType::kBrightnessRelative: return "brightness_relative"; case GatewayKnxDaliDataType::kColorTemperature: return "color_temperature"; case GatewayKnxDaliDataType::kRgb: return "rgb"; case GatewayKnxDaliDataType::kScene: return "scene"; case GatewayKnxDaliDataType::kUnknown: default: return "unknown"; } } const char* GatewayKnxTargetKindToString(GatewayKnxDaliTargetKind kind) { switch (kind) { case GatewayKnxDaliTargetKind::kBroadcast: return "broadcast"; case GatewayKnxDaliTargetKind::kShortAddress: return "short_address"; case GatewayKnxDaliTargetKind::kGroup: return "group"; case GatewayKnxDaliTargetKind::kNone: default: return "none"; } } std::optional GatewayKnxDaliDataTypeForMiddleGroup( uint8_t middle_group) { switch (middle_group) { case 1: return GatewayKnxDaliDataType::kSwitch; case 2: return GatewayKnxDaliDataType::kBrightness; case 3: return GatewayKnxDaliDataType::kColorTemperature; case 4: return GatewayKnxDaliDataType::kRgb; default: return std::nullopt; } } std::optional GatewayKnxDaliTargetForSubgroup(uint8_t sub_group) { if (sub_group == 0) { return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kBroadcast, 127}; } if (sub_group >= 1 && sub_group <= 64) { return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kShortAddress, static_cast(sub_group - 1)}; } if (sub_group >= 65 && sub_group <= 80) { return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kGroup, static_cast(sub_group - 65)}; } return std::nullopt; } uint16_t GatewayKnxGroupAddress(uint8_t main_group, uint8_t middle_group, uint8_t sub_group) { return static_cast(((main_group & 0x1f) << 11) | ((middle_group & 0x07) << 8) | sub_group); } std::string GatewayKnxGroupAddressString(uint16_t group_address) { const int main = (group_address >> 11) & 0x1f; const int middle = (group_address >> 8) & 0x07; const int sub = group_address & 0xff; return std::to_string(main) + "/" + std::to_string(middle) + "/" + std::to_string(sub); } namespace { uint16_t GwReg1GroupAddressForObject(uint8_t main_group, uint16_t object_number) { return GatewayKnxGroupAddress(main_group, static_cast(object_number >> 8), static_cast(object_number & 0xff)); } GatewayKnxDaliBinding MakeGwReg1Binding(uint8_t main_group, uint16_t object_number, int channel_index, const char* object_role, GatewayKnxDaliDataType data_type, GatewayKnxDaliTarget target) { GatewayKnxDaliBinding binding; binding.mapping_mode = GatewayKnxMappingMode::kGwReg1Direct; binding.group_object_number = static_cast(object_number); binding.channel_index = channel_index; binding.object_role = object_role; binding.main_group = main_group; binding.middle_group = static_cast((object_number >> 8) & 0x07); binding.sub_group = static_cast(object_number & 0xff); binding.group_address = GwReg1GroupAddressForObject(main_group, object_number); binding.address = GatewayKnxGroupAddressString(binding.group_address); binding.data_type = data_type; binding.target = target; binding.datapoint_type = DataTypeDpt(data_type); binding.name = std::string("GW-REG1 ") + TargetName(target) + " - " + DataTypeName(data_type); return binding; } std::optional GwReg1BindingForObject(uint8_t main_group, uint16_t object_number) { if (object_number == kGwReg1AppKoBroadcastSwitch) { return MakeGwReg1Binding( main_group, object_number, -1, "broadcast_switch", GatewayKnxDaliDataType::kSwitch, GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kBroadcast, 127}); } if (object_number == kGwReg1AppKoBroadcastDimm) { return MakeGwReg1Binding( main_group, object_number, -1, "broadcast_dimm_absolute", GatewayKnxDaliDataType::kBrightness, GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kBroadcast, 127}); } if (object_number == kGwReg1AppKoScene) { return MakeGwReg1Binding(main_group, object_number, -1, "scene", GatewayKnxDaliDataType::kScene, GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kNone, -1}); } const int adr_relative = static_cast(object_number) - kGwReg1AdrKoOffset; if (adr_relative >= 0 && adr_relative < kGwReg1AdrKoBlockSize * 64) { const int channel = adr_relative / kGwReg1AdrKoBlockSize; const int slot = adr_relative % kGwReg1AdrKoBlockSize; const GatewayKnxDaliTarget target{GatewayKnxDaliTargetKind::kShortAddress, channel}; if (slot == kGwReg1KoSwitch) { return MakeGwReg1Binding(main_group, object_number, channel, "switch", GatewayKnxDaliDataType::kSwitch, target); } if (slot == kGwReg1KoDimmRelative) { return MakeGwReg1Binding(main_group, object_number, channel, "dimm_relative", GatewayKnxDaliDataType::kBrightnessRelative, target); } if (slot == kGwReg1KoDimmAbsolute) { return MakeGwReg1Binding(main_group, object_number, channel, "dimm_absolute", GatewayKnxDaliDataType::kBrightness, target); } if (slot == kGwReg1KoColor) { return MakeGwReg1Binding(main_group, object_number, channel, "color", GatewayKnxDaliDataType::kRgb, target); } } const int group_relative = static_cast(object_number) - kGwReg1GrpKoOffset; if (group_relative >= 0 && group_relative < kGwReg1GrpKoBlockSize * 16) { const int group = group_relative / kGwReg1GrpKoBlockSize; const int slot = group_relative % kGwReg1GrpKoBlockSize; const GatewayKnxDaliTarget target{GatewayKnxDaliTargetKind::kGroup, group}; if (slot == kGwReg1KoSwitch) { return MakeGwReg1Binding(main_group, object_number, group, "switch", GatewayKnxDaliDataType::kSwitch, target); } if (slot == kGwReg1KoDimmRelative) { return MakeGwReg1Binding(main_group, object_number, group, "dimm_relative", GatewayKnxDaliDataType::kBrightnessRelative, target); } if (slot == kGwReg1KoDimmAbsolute) { return MakeGwReg1Binding(main_group, object_number, group, "dimm_absolute", GatewayKnxDaliDataType::kBrightness, target); } if (slot == kGwReg1KoColor) { return MakeGwReg1Binding(main_group, object_number, group, "color", GatewayKnxDaliDataType::kRgb, target); } } return std::nullopt; } std::optional EtsBindingForAssociation(uint8_t main_group, const GatewayKnxEtsAssociation& association) { auto binding = GwReg1BindingForObject(main_group, association.group_object_number); if (!binding.has_value()) { return std::nullopt; } binding->mapping_mode = GatewayKnxMappingMode::kEtsDatabase; binding->group_address = association.group_address; binding->address = GatewayKnxGroupAddressString(association.group_address); binding->name = std::string("ETS ") + binding->name; return binding; } } // namespace GatewayKnxBridge::GatewayKnxBridge(DaliBridgeEngine& engine) : engine_(engine) {} void GatewayKnxBridge::setConfig(const GatewayKnxConfig& config) { config_ = config; rebuildEtsBindings(); } void GatewayKnxBridge::setRuntimeContext(const openknx::EtsDeviceRuntime* runtime) { runtime_ = runtime; } const GatewayKnxConfig& GatewayKnxBridge::config() const { return config_; } size_t GatewayKnxBridge::etsBindingCount() const { size_t count = 0; for (const auto& entry : ets_bindings_by_group_address_) { count += entry.second.size(); } return count; } std::vector GatewayKnxBridge::describeDaliBindings() const { std::vector bindings; std::set ets_group_addresses; if (config_.ets_database_enabled) { for (const auto& entry : ets_bindings_by_group_address_) { ets_group_addresses.insert(entry.first); bindings.insert(bindings.end(), entry.second.begin(), entry.second.end()); } } if (config_.mapping_mode == GatewayKnxMappingMode::kGwReg1Direct) { bindings.reserve(2 + (64 * 4) + (16 * 4)); if (const auto binding = GwReg1BindingForObject(config_.main_group, kGwReg1AppKoBroadcastSwitch)) { if (ets_group_addresses.count(binding->group_address) == 0) { bindings.push_back(binding.value()); } } if (const auto binding = GwReg1BindingForObject(config_.main_group, kGwReg1AppKoBroadcastDimm)) { if (ets_group_addresses.count(binding->group_address) == 0) { bindings.push_back(binding.value()); } } if (const auto binding = GwReg1BindingForObject(config_.main_group, kGwReg1AppKoScene)) { if (ets_group_addresses.count(binding->group_address) == 0) { bindings.push_back(binding.value()); } } for (int address = 0; address < 64; ++address) { const uint16_t base = static_cast(kGwReg1AdrKoOffset + (address * kGwReg1AdrKoBlockSize)); for (const uint8_t slot : {kGwReg1KoSwitch, kGwReg1KoDimmRelative, kGwReg1KoDimmAbsolute, kGwReg1KoColor}) { if (const auto binding = GwReg1BindingForObject(config_.main_group, base + slot)) { if (ets_group_addresses.count(binding->group_address) == 0) { bindings.push_back(binding.value()); } } } } for (int group = 0; group < 16; ++group) { const uint16_t base = static_cast(kGwReg1GrpKoOffset + (group * kGwReg1GrpKoBlockSize)); for (const uint8_t slot : {kGwReg1KoSwitch, kGwReg1KoDimmRelative, kGwReg1KoDimmAbsolute, kGwReg1KoColor}) { if (const auto binding = GwReg1BindingForObject(config_.main_group, base + slot)) { if (ets_group_addresses.count(binding->group_address) == 0) { bindings.push_back(binding.value()); } } } } return bindings; } bindings.reserve(4 * 81); for (uint8_t middle = 1; middle <= 4; ++middle) { const auto data_type = GatewayKnxDaliDataTypeForMiddleGroup(middle); if (!data_type.has_value()) { continue; } for (uint8_t sub = 0; sub <= 80; ++sub) { const auto target = GatewayKnxDaliTargetForSubgroup(sub); if (!target.has_value()) { continue; } GatewayKnxDaliBinding binding; binding.mapping_mode = GatewayKnxMappingMode::kFormula; binding.main_group = config_.main_group; binding.middle_group = middle; binding.sub_group = sub; binding.group_address = GatewayKnxGroupAddress(config_.main_group, middle, sub); binding.address = GatewayKnxGroupAddressString(binding.group_address); binding.data_type = data_type.value(); binding.target = target.value(); if (ets_group_addresses.count(binding.group_address) != 0) { continue; } binding.object_role = GatewayKnxDataTypeToString(data_type.value()); binding.datapoint_type = DataTypeDpt(data_type.value()); binding.name = TargetName(target.value()) + " - " + DataTypeName(data_type.value()); bindings.push_back(std::move(binding)); } } return bindings; } bool GatewayKnxBridge::matchesGroupAddress(uint16_t group_address) const { if (!config_.dali_router_enabled) { return false; } if (config_.ets_database_enabled && ets_bindings_by_group_address_.find(group_address) != ets_bindings_by_group_address_.end()) { return true; } const uint8_t main = static_cast((group_address >> 11) & 0x1f); const uint8_t middle = static_cast((group_address >> 8) & 0x07); const uint8_t sub = static_cast(group_address & 0xff); if (main != config_.main_group) { return false; } if (config_.mapping_mode == GatewayKnxMappingMode::kGwReg1Direct) { const uint16_t object_number = static_cast((middle << 8) | sub); return GwReg1BindingForObject(config_.main_group, object_number).has_value(); } if (config_.mapping_mode == GatewayKnxMappingMode::kManual) { return false; } return GatewayKnxDaliDataTypeForMiddleGroup(middle).has_value() && GatewayKnxDaliTargetForSubgroup(sub).has_value(); } DaliBridgeResult GatewayKnxBridge::handleGroupWrite(uint16_t group_address, const uint8_t* data, size_t len) { if (!config_.dali_router_enabled) { return ErrorResult(group_address, "KNX to DALI router disabled"); } if (config_.ets_database_enabled) { const auto ets_bindings = ets_bindings_by_group_address_.find(group_address); if (ets_bindings != ets_bindings_by_group_address_.end()) { return executeEtsBindings(group_address, ets_bindings->second, data, len); } } const uint8_t main = static_cast((group_address >> 11) & 0x1f); const uint8_t middle = static_cast((group_address >> 8) & 0x07); const uint8_t sub = static_cast(group_address & 0xff); if (main != config_.main_group) { return ErrorResult(group_address, "KNX main group does not match gateway config"); } if (config_.mapping_mode == GatewayKnxMappingMode::kGwReg1Direct) { const uint16_t object_number = static_cast((middle << 8) | sub); const auto binding = GwReg1BindingForObject(config_.main_group, object_number); if (!binding.has_value()) { return ErrorResult(group_address, "unmapped GW-REG1 KNX object address"); } return executeForDecodedWrite(group_address, binding->data_type, binding->target, data, len); } if (config_.mapping_mode == GatewayKnxMappingMode::kManual) { return ErrorResult(group_address, "manual KNX mapping dataset is not configured"); } const auto data_type = GatewayKnxDaliDataTypeForMiddleGroup(middle); const auto target = GatewayKnxDaliTargetForSubgroup(sub); if (!data_type.has_value() || !target.has_value()) { return ErrorResult(group_address, "unmapped KNX group address"); } return executeForDecodedWrite(group_address, data_type.value(), target.value(), data, len); } DaliBridgeResult GatewayKnxBridge::handleGroupObjectWrite(uint16_t group_object_number, const uint8_t* data, size_t len) { const uint16_t group_address = GwReg1GroupAddressForObject(config_.main_group, group_object_number); const std::string payload = HexBytes(data, len); ESP_LOGI(kTag, "OpenKNX KO write ko=%u derivedGa=%s len=%u payload=%s", static_cast(group_object_number), GatewayKnxGroupAddressString(group_address).c_str(), static_cast(len), payload.c_str()); if (!config_.dali_router_enabled) { return ErrorResult(group_address, "KNX to DALI router disabled"); } const auto binding = GwReg1BindingForObject(config_.main_group, group_object_number); if (!binding.has_value()) { ESP_LOGW(kTag, "OpenKNX KO write ignored ko=%u: unsupported GW-REG1 object", static_cast(group_object_number)); return IgnoredResult(group_address, group_object_number, "unsupported GW-REG1 group object"); } DaliBridgeResult result = executeForDecodedWrite(binding->group_address, binding->data_type, binding->target, data, len); result.metadata["source"] = "openknx_group_object"; result.metadata["groupObjectNumber"] = static_cast(group_object_number); result.metadata["objectRole"] = binding->object_role; if (result.ok) { ESP_LOGI(kTag, "OpenKNX KO write routed ko=%u role=%s target=%s", static_cast(group_object_number), binding->object_role.c_str(), TargetName(binding->target).c_str()); } else { ESP_LOGW(kTag, "OpenKNX KO write failed ko=%u role=%s error=%s", static_cast(group_object_number), binding->object_role.c_str(), result.error.c_str()); } return result; } bool GatewayKnxBridge::handleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len, std::vector* response) { if (object_index != kReg1DaliFunctionObjectIndex || property_id != kReg1DaliFunctionPropertyId || data == nullptr || len == 0 || response == nullptr) { return false; } switch (data[0]) { case kReg1FunctionType: return handleReg1TypeCommand(data, len, response); case kReg1FunctionScan: return handleReg1ScanCommand(data, len, response); case kReg1FunctionAssign: return handleReg1AssignCommand(data, len, response); case kReg1FunctionEvgWrite: return handleReg1EvgWriteCommand(data, len, response); case kReg1FunctionEvgRead: return handleReg1EvgReadCommand(data, len, response); case kReg1FunctionSetScene: return handleReg1SetSceneCommand(data, len, response); case kReg1FunctionGetScene: return handleReg1GetSceneCommand(data, len, response); case kReg1FunctionIdentify: return handleReg1IdentifyCommand(data, len, response); default: return false; } } bool GatewayKnxBridge::handleFunctionPropertyState(uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len, std::vector* response) { if (object_index != kReg1DaliFunctionObjectIndex || property_id != kReg1DaliFunctionPropertyId || data == nullptr || len == 0 || response == nullptr) { return false; } switch (data[0]) { case kReg1FunctionScan: case 5: return handleReg1ScanState(data, len, response); case kReg1FunctionAssign: return handleReg1AssignState(data, len, response); case 7: return handleReg1FoundEvgsState(data, len, response); default: return false; } } bool GatewayKnxBridge::handleReg1TypeCommand(const uint8_t* data, size_t len, std::vector* response) { if (len < 2 || response == nullptr) { return false; } const uint8_t short_address = data[1]; const auto type_response = QueryShort(engine_, short_address, DALI_CMD_QUERY_DEVICE_TYPE, "knx-function-type"); if (!type_response.has_value()) { *response = {0x01}; return true; } uint8_t device_type = static_cast(type_response.value()); if (device_type == kDaliDeviceTypeMultiple) { for (int index = 0; index < 16; ++index) { const auto next_type = QueryShort(engine_, short_address, DALI_CMD_QUERY_NEXT_DEVICE_TYPE, "knx-function-next-device-type"); if (!next_type.has_value()) { *response = {0x01}; return true; } if (next_type.value() == kDaliDeviceTypeNone) { break; } if (next_type.value() < 20) { device_type = static_cast(next_type.value()); } } } *response = {0x00, device_type}; if (device_type == kReg1DeviceTypeDt8) { if (!SendRaw(engine_, DALI_CMD_SPECIAL_DT_SELECT, kReg1DeviceTypeDt8, "knx-function-dt8-select")) { *response = {0x02}; return true; } const auto color_features = QueryShort(engine_, short_address, DALI_CMD_QUERY_COLOR_TYPE, "knx-function-color-type"); if (!color_features.has_value()) { *response = {0x02}; return true; } response->push_back(static_cast(color_features.value())); } return true; } bool GatewayKnxBridge::handleReg1ScanCommand(const uint8_t* data, size_t len, std::vector* response) { if (len < 5 || response == nullptr) { return false; } commissioning_scan_done_ = false; commissioning_found_ballasts_.clear(); const bool only_new = data[1] == 1; const bool randomize = data[2] == 1; const bool delete_all = data[3] == 1; const bool assign = data[4] == 1; ESP_LOGI(kTag, "REG1-Dali scan start onlyNew=%d randomize=%d deleteAll=%d assign=%d", only_new, randomize, delete_all, assign); std::array used_addresses{}; if (assign && !delete_all) { used_addresses = QueryUsedShortAddresses(engine_); } const bool initialized = SendRaw(engine_, DALI_CMD_SPECIAL_TERMINATE, DALI_CMD_OFF, "knx-function-scan-terminate-prev") && SendRawExt(engine_, DALI_CMD_SPECIAL_INITIALIZE, only_new ? DALI_CMD_STOP_FADE : DALI_CMD_OFF, "knx-function-scan-init") && SendRawExt(engine_, DALI_CMD_SPECIAL_INITIALIZE, only_new ? DALI_CMD_STOP_FADE : DALI_CMD_OFF, "knx-function-scan-init-repeat"); if (!initialized) { ESP_LOGW(kTag, "REG1-Dali scan failed during initialize"); commissioning_scan_done_ = true; response->clear(); return true; } if (delete_all) { const bool removed = SendRaw(engine_, DALI_CMD_SPECIAL_SET_DTR0, 0xff, "knx-function-scan-clear-short-dtr") && SendRawExt(engine_, 0xff, DALI_CMD_STORE_DTR_AS_SHORT_ADDRESS, "knx-function-scan-clear-short"); if (!removed) { ESP_LOGW(kTag, "REG1-Dali scan failed while clearing short addresses"); commissioning_scan_done_ = true; response->clear(); return true; } } if (randomize) { const bool randomized = SendRawExt(engine_, DALI_CMD_SPECIAL_RANDOMIZE, DALI_CMD_OFF, "knx-function-scan-randomize") && SendRawExt(engine_, DALI_CMD_SPECIAL_RANDOMIZE, DALI_CMD_OFF, "knx-function-scan-randomize-repeat"); if (!randomized) { ESP_LOGW(kTag, "REG1-Dali scan failed while randomizing addresses"); commissioning_scan_done_ = true; response->clear(); return true; } } while (true) { const auto random_address = FindLowestSelectedRandomAddress(engine_); if (!random_address.has_value()) { break; } GatewayKnxCommissioningBallast ballast; ballast.high = static_cast((random_address.value() >> 16) & 0xff); ballast.middle = static_cast((random_address.value() >> 8) & 0xff); ballast.low = static_cast(random_address.value() & 0xff); ballast.short_address = 0xff; if (assign) { const auto next_address = NextFreeShortAddress(used_addresses); if (!next_address.has_value()) { ESP_LOGW(kTag, "REG1-Dali scan has no free short address left for 0x%06x", static_cast(random_address.value())); break; } if (!SendRaw(engine_, DALI_CMD_SPECIAL_PROGRAM_SHORT_ADDRESS, DaliComm::toCmdAddr(next_address.value()), "knx-function-scan-program-short") || !VerifyShortAddress(engine_, next_address.value())) { ESP_LOGW(kTag, "REG1-Dali scan failed to program short address %u", static_cast(next_address.value())); break; } used_addresses[next_address.value()] = true; ballast.short_address = next_address.value(); } else { ballast.short_address = QuerySelectedShortAddress(engine_).value_or(0xff); } commissioning_found_ballasts_.push_back(ballast); ESP_LOGI(kTag, "REG1-Dali scan found random=0x%02X%02X%02X short=%u", ballast.high, ballast.middle, ballast.low, static_cast(ballast.short_address)); if (!SendRaw(engine_, DALI_CMD_SPECIAL_WITHDRAW, DALI_CMD_OFF, "knx-function-scan-withdraw")) { ESP_LOGW(kTag, "REG1-Dali scan failed while withdrawing matched device"); break; } } SendRaw(engine_, DALI_CMD_SPECIAL_TERMINATE, DALI_CMD_OFF, "knx-function-scan-terminate"); commissioning_scan_done_ = true; ESP_LOGI(kTag, "REG1-Dali scan completed count=%u", static_cast(commissioning_found_ballasts_.size())); response->clear(); return true; } bool GatewayKnxBridge::handleReg1AssignCommand(const uint8_t* data, size_t len, std::vector* response) { if (len < 5 || response == nullptr) { return false; } commissioning_assign_done_ = false; const uint8_t short_address = data[1] == 99 ? 0xff : data[1]; const bool ok = SendRawExt(engine_, DALI_CMD_SPECIAL_INITIALIZE, 0x00, "knx-function-assign-init") && SendRaw(engine_, DALI_CMD_SPECIAL_SEARCHADDRH, data[2], "knx-function-assign-search-h") && SendRaw(engine_, DALI_CMD_SPECIAL_SEARCHADDRM, data[3], "knx-function-assign-search-m") && SendRaw(engine_, DALI_CMD_SPECIAL_SEARCHADDRL, data[4], "knx-function-assign-search-l") && SendRaw(engine_, DALI_CMD_SPECIAL_PROGRAM_SHORT_ADDRESS, short_address == 0xff ? 0xff : DaliComm::toCmdAddr(short_address), "knx-function-assign-program") && SendRaw(engine_, DALI_CMD_SPECIAL_TERMINATE, 0x00, "knx-function-assign-terminate"); commissioning_assign_done_ = true; if (!ok) { ESP_LOGW(kTag, "REG1-Dali assign command failed while programming short address %u", short_address); } response->clear(); return true; } bool GatewayKnxBridge::handleReg1EvgWriteCommand(const uint8_t* data, size_t len, std::vector* response) { if (len < 10 || response == nullptr) { return false; } const uint8_t short_address = data[1]; DaliBridgeRequest settings = FunctionRequest("knx-function-evg-write-settings", BridgeOperation::setAddressSettings); settings.shortAddress = short_address; settings.value = DaliValue::Object{ {"minLevel", Reg1PercentToArc(data[2])}, {"maxLevel", Reg1PercentToArc(data[3])}, {"powerOnLevel", Reg1PercentToArc(data[4])}, {"systemFailureLevel", Reg1PercentToArc(data[5])}, {"fadeTime", static_cast((data[6] >> 4) & 0x0f)}, {"fadeRate", static_cast(data[6] & 0x0f)}, }; const bool settings_ok = engine_.execute(settings).ok; DaliBridgeRequest groups = FunctionRequest("knx-function-evg-write-groups", BridgeOperation::setGroupMask); groups.shortAddress = short_address; groups.value = static_cast(static_cast(data[8]) | (static_cast(data[9]) << 8)); const bool groups_ok = engine_.execute(groups).ok; if (!settings_ok || !groups_ok) { ESP_LOGW(kTag, "REG1-Dali EVG write command failed for short address %u", short_address); } response->clear(); return true; } bool GatewayKnxBridge::handleReg1EvgReadCommand(const uint8_t* data, size_t len, std::vector* response) { if (len < 2 || response == nullptr) { return false; } const uint8_t short_address = data[1]; response->assign(12, 0x00); (*response)[0] = 0x00; uint8_t error_byte = 0; DaliBridgeRequest settings = FunctionRequest("knx-function-evg-read-settings", BridgeOperation::getAddressSettings); settings.shortAddress = short_address; const auto settings_result = engine_.execute(settings); const auto set_level = [&](size_t index, const char* key, uint8_t error_mask) { const auto value = MetadataInt(settings_result, key); if (!settings_result.ok || !value.has_value()) { error_byte |= error_mask; (*response)[index] = 0xff; return; } (*response)[index] = Reg1ArcToPercent(static_cast(std::clamp(value.value(), 0, 255))); }; set_level(1, "minLevel", 0b00000001); set_level(2, "maxLevel", 0b00000010); set_level(3, "powerOnLevel", 0b00000100); set_level(4, "systemFailureLevel", 0b00001000); const auto fade_time = MetadataInt(settings_result, "fadeTime"); const auto fade_rate = MetadataInt(settings_result, "fadeRate"); if (!settings_result.ok || !fade_time.has_value() || !fade_rate.has_value()) { error_byte |= 0b00010000; (*response)[5] = 0xff; } else { (*response)[5] = static_cast(((fade_rate.value() & 0x0f) << 4) | (fade_time.value() & 0x0f)); } DaliBridgeRequest groups = FunctionRequest("knx-function-evg-read-groups", BridgeOperation::getGroupMask); groups.shortAddress = short_address; const auto groups_result = engine_.execute(groups); if (!groups_result.ok || !groups_result.data.has_value()) { error_byte |= 0b11000000; } else { const uint16_t mask = static_cast(groups_result.data.value()); (*response)[7] = static_cast(mask & 0xff); (*response)[8] = static_cast((mask >> 8) & 0xff); } (*response)[9] = error_byte; return true; } bool GatewayKnxBridge::handleReg1SetSceneCommand(const uint8_t* data, size_t len, std::vector* response) { if (len < 10 || response == nullptr) { return false; } const GatewayKnxDaliTarget target = Reg1SceneTarget(data[1]); const uint8_t scene = data[2] & 0x0f; const bool enabled = data[3] != 0; DaliBridgeRequest request = FunctionRequest( enabled ? "knx-function-set-scene" : "knx-function-remove-scene", enabled ? (data[4] == kReg1DeviceTypeDt8 ? BridgeOperation::storeDt8SceneSnapshot : BridgeOperation::setSceneLevel) : BridgeOperation::removeSceneLevel); ApplyTargetToRequest(target, &request); DaliValue::Object value{{"scene", static_cast(scene)}}; if (enabled) { value["brightness"] = static_cast(Reg1PercentToArc(data[6])); if (data[4] == kReg1DeviceTypeDt8) { if (data[5] == kReg1ColorTypeTw) { const uint16_t kelvin = ReadBe16(data + 7); value["colorMode"] = "color_temperature"; value["colorTemperature"] = static_cast(kelvin); } else { value["colorMode"] = "rgb"; value["r"] = static_cast(data[7]); value["g"] = static_cast(data[8]); value["b"] = static_cast(data[9]); } } } request.value = std::move(value); const auto result = engine_.execute(request); if (!result.ok) { ESP_LOGW(kTag, "REG1-Dali set scene command failed for scene %u", scene); } response->clear(); return true; } bool GatewayKnxBridge::handleReg1GetSceneCommand(const uint8_t* data, size_t len, std::vector* response) { if (len < 5 || response == nullptr) { return false; } const uint8_t short_address = data[1]; const uint8_t scene = data[2] & 0x0f; DaliBridgeRequest request = FunctionRequest("knx-function-get-scene", BridgeOperation::getSceneLevel); request.shortAddress = short_address; request.value = DaliValue::Object{{"scene", static_cast(scene)}}; const auto result = engine_.execute(request); if (!result.ok || !result.data.has_value()) { *response = {0xff}; return true; } const uint8_t raw_level = static_cast(std::clamp(result.data.value(), 0, 255)); *response = {static_cast(raw_level == 0xff ? 0xff : Reg1ArcToPercent(raw_level))}; if (raw_level != 0xff && data[3] == kReg1DeviceTypeDt8) { if (data[4] == kReg1ColorTypeTw) { response->resize(3, 0); SendRaw(engine_, DALI_CMD_SPECIAL_SET_DTR0, 0xe2, "knx-function-get-scene-ct-selector"); SendRaw(engine_, DALI_CMD_SPECIAL_DT_SELECT, kReg1DeviceTypeDt8, "knx-function-get-scene-ct-dt-select"); const uint16_t mirek = static_cast( (QueryShort(engine_, short_address, DALI_CMD_QUERY_COLOR_VALUE, "knx-function-get-scene-mirek-h") .value_or(0) << 8) | QueryShort(engine_, short_address, DALI_CMD_QUERY_CONTENT_DTR, "knx-function-get-scene-mirek-l") .value_or(0)); const uint16_t kelvin = mirek == 0 ? 0 : static_cast(1000000U / mirek); (*response)[1] = static_cast((kelvin >> 8) & 0xff); (*response)[2] = static_cast(kelvin & 0xff); } else { response->resize(4, 0); const std::array selectors{0xe9, 0xea, 0xeb}; for (size_t index = 0; index < selectors.size(); ++index) { SendRaw(engine_, DALI_CMD_SPECIAL_SET_DTR0, selectors[index], "knx-function-get-scene-rgb-selector"); SendRaw(engine_, DALI_CMD_SPECIAL_DT_SELECT, kReg1DeviceTypeDt8, "knx-function-get-scene-rgb-dt-select"); (*response)[index + 1] = static_cast( QueryShort(engine_, short_address, DALI_CMD_QUERY_COLOR_VALUE, "knx-function-get-scene-rgb-value") .value_or(0)); } } } return true; } bool GatewayKnxBridge::handleReg1IdentifyCommand(const uint8_t* data, size_t len, std::vector* response) { if (len < 2 || response == nullptr) { return false; } DaliBridgeRequest off = FunctionRequest("knx-function-identify-broadcast-off", BridgeOperation::off); off.metadata["broadcast"] = true; engine_.execute(off); DaliBridgeRequest identify = FunctionRequest("knx-function-identify-recall-max", BridgeOperation::recallMaxLevel); identify.shortAddress = data[1]; engine_.execute(identify); response->clear(); return true; } bool GatewayKnxBridge::handleReg1ScanState(const uint8_t* data, size_t len, std::vector* response) { if (len < 1 || response == nullptr) { return false; } response->clear(); response->push_back(commissioning_scan_done_ ? 1 : 0); if (data[0] == kReg1FunctionScan) { response->push_back(static_cast( std::min(commissioning_found_ballasts_.size(), 0xff))); } return true; } bool GatewayKnxBridge::handleReg1AssignState(const uint8_t* data, size_t len, std::vector* response) { if (len < 1 || response == nullptr) { return false; } *response = {static_cast(commissioning_assign_done_ ? 1 : 0)}; return true; } bool GatewayKnxBridge::handleReg1FoundEvgsState(const uint8_t* data, size_t len, std::vector* response) { if (len < 2 || response == nullptr) { return false; } if (data[1] == 254) { commissioning_found_ballasts_.clear(); response->clear(); return true; } const size_t index = data[1]; response->clear(); response->push_back(index < commissioning_found_ballasts_.size() ? 1 : 0); if (index < commissioning_found_ballasts_.size()) { const auto& ballast = commissioning_found_ballasts_[index]; response->push_back(ballast.high); response->push_back(ballast.middle); response->push_back(ballast.low); response->push_back(ballast.short_address); } return true; } DaliBridgeResult GatewayKnxBridge::executeEtsBindings( uint16_t group_address, const std::vector& bindings, const uint8_t* data, size_t len) { if (bindings.empty()) { return ErrorResult(group_address, "unmapped ETS KNX group address"); } DaliBridgeResult result; result.ok = true; result.metadata["source"] = "ets_database"; result.metadata["groupAddress"] = GatewayKnxGroupAddressString(group_address); result.metadata["bindingCount"] = static_cast(bindings.size()); for (const auto& binding : bindings) { DaliBridgeResult child = executeForDecodedWrite(group_address, binding.data_type, binding.target, data, len); result.ok = result.ok && child.ok; result.results.emplace_back(child.toJson()); } result.data = static_cast(result.results.size()); if (!result.ok) { result.error = "one or more ETS KNX bindings failed"; } return result; } DaliBridgeResult GatewayKnxBridge::executeReg1SceneWrite(uint16_t group_address, const uint8_t* data, size_t len) { if (runtime_ == nullptr || !runtime_->configured()) { return ErrorResult(group_address, "REG1 scene parameters are unavailable"); } if (data == nullptr || len < 1) { return ErrorResult(group_address, "missing KNX scene payload"); } const uint8_t knx_scene = data[0] & kReg1SceneTelegramNumberMask; const bool store_scene = (data[0] & kReg1SceneTelegramStoreMask) != 0; DaliBridgeResult result; result.ok = true; result.sequence = "knx-" + GatewayKnxGroupAddressString(group_address); result.metadata["sourceProtocol"] = "knx"; result.metadata["knxGroupAddress"] = GatewayKnxGroupAddressString(group_address); result.metadata["sceneNumber"] = static_cast(knx_scene); result.metadata["sceneAction"] = std::string(store_scene ? "store" : "recall"); size_t matched_entries = 0; for (size_t index = 0; index < kReg1SceneEntryCount; ++index) { if (Reg1SceneTypeForEntry(*runtime_, index) == kReg1SceneTypeNone) { continue; } const uint8_t configured_knx_scene = Reg1KnxSceneNumberForEntry(*runtime_, index); if (configured_knx_scene == 0 || knx_scene != static_cast(configured_knx_scene - 1)) { continue; } if (store_scene && !Reg1SceneSaveAllowedForEntry(*runtime_, index)) { continue; } const auto target = Reg1SceneTargetForEntry(*runtime_, index); if (!target.has_value()) { continue; } ++matched_entries; const uint8_t dali_scene = Reg1DaliSceneNumberForEntry(*runtime_, index); if (store_scene) { DaliBridgeResult copy_result = SendRawExtForTarget(engine_, group_address, target.value(), DALI_CMD_STORE_ACTUAL_LEVEL_IN_THE_DTR); copy_result.metadata["sceneTableIndex"] = static_cast(index); copy_result.metadata["sceneNumber"] = static_cast(dali_scene); result.results.emplace_back(copy_result.toJson()); result.ok = result.ok && copy_result.ok; DaliBridgeResult store_result = SendRawExtForTarget(engine_, group_address, target.value(), DALI_CMD_SET_SCENE(dali_scene)); store_result.metadata["sceneTableIndex"] = static_cast(index); store_result.metadata["sceneNumber"] = static_cast(dali_scene); result.results.emplace_back(store_result.toJson()); result.ok = result.ok && store_result.ok; } else { DaliBridgeResult recall_result = SendRawForTarget(engine_, group_address, target.value(), DALI_CMD_GO_TO_SCENE(dali_scene)); recall_result.metadata["sceneTableIndex"] = static_cast(index); recall_result.metadata["sceneNumber"] = static_cast(dali_scene); result.results.emplace_back(recall_result.toJson()); result.ok = result.ok && recall_result.ok; } } if (matched_entries == 0) { result.ok = false; result.error = "no configured REG1 scene mapping matched KNX scene"; return result; } result.data = static_cast(matched_entries); result.metadata["matchedSceneEntries"] = static_cast(matched_entries); if (!result.ok) { result.error = "one or more REG1 scene operations failed"; } return result; } void GatewayKnxBridge::rebuildEtsBindings() { ets_bindings_by_group_address_.clear(); for (const auto& association : config_.ets_associations) { const auto binding = EtsBindingForAssociation(config_.main_group, association); if (!binding.has_value()) { continue; } ets_bindings_by_group_address_[association.group_address].push_back(binding.value()); } } DaliBridgeResult GatewayKnxBridge::executeForDecodedWrite(uint16_t group_address, GatewayKnxDaliDataType data_type, GatewayKnxDaliTarget target, const uint8_t* data, size_t len) { if (target.kind == GatewayKnxDaliTargetKind::kNone && data_type != GatewayKnxDaliDataType::kScene) { return ErrorResult(group_address, "missing DALI target"); } switch (data_type) { case GatewayKnxDaliDataType::kSwitch: { if (data == nullptr || len < 1) { return ErrorResult(group_address, "missing DPT1 switch payload"); } DaliBridgeRequest request = RequestForTarget( group_address, target, (data[0] & 0x01) != 0 ? BridgeOperation::on : BridgeOperation::off); return engine_.execute(request); } case GatewayKnxDaliDataType::kBrightness: { if (data == nullptr || len < 1) { return ErrorResult(group_address, "missing DPT5 brightness payload"); } DaliBridgeRequest request = RequestForTarget(group_address, target, BridgeOperation::setBrightnessPercent); request.value = (static_cast(data[0]) * 100.0) / 255.0; return engine_.execute(request); } case GatewayKnxDaliDataType::kBrightnessRelative: { if (data == nullptr || len < 1) { return ErrorResult(group_address, "missing DPT3 relative dimming payload"); } const uint8_t payload = data[0]; const uint8_t step_code = payload & 0x07; const bool dim_up = (payload & 0x10) != 0; const uint8_t cmd = step_code == 0 ? kDaliCmdStopFade : (dim_up ? kDaliCmdOnStepUp : kDaliCmdStepDownOff); DaliBridgeResult result = SendRawForTarget(engine_, group_address, target, cmd); result.metadata["knxRelativeStepCode"] = static_cast(step_code); result.metadata["knxRelativeDirection"] = step_code == 0 ? std::string("stop") : std::string(dim_up ? "up" : "down"); return result; } case GatewayKnxDaliDataType::kColorTemperature: { if (data == nullptr || len < 2) { return ErrorResult(group_address, "missing DPT7 color temperature payload"); } DaliBridgeRequest request = RequestForTarget(group_address, target, BridgeOperation::setColorTemperature); request.value = static_cast(ReadBe16(data)); return engine_.execute(request); } case GatewayKnxDaliDataType::kRgb: { if (data == nullptr || len < 3) { return ErrorResult(group_address, "missing DPT232 RGB payload"); } DaliBridgeRequest request = RequestForTarget(group_address, target, BridgeOperation::setColourRGB); DaliValue::Object rgb; rgb["r"] = static_cast(data[0]); rgb["g"] = static_cast(data[1]); rgb["b"] = static_cast(data[2]); request.value = std::move(rgb); return engine_.execute(request); } case GatewayKnxDaliDataType::kScene: return executeReg1SceneWrite(group_address, data, len); case GatewayKnxDaliDataType::kUnknown: default: return ErrorResult(group_address, "unsupported KNX data type"); } } GatewayKnxTpIpRouter::GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, std::string openknx_namespace) : bridge_(bridge), openknx_namespace_(std::move(openknx_namespace)) { openknx_lock_ = xSemaphoreCreateMutex(); startup_semaphore_ = xSemaphoreCreateBinary(); } GatewayKnxTpIpRouter::~GatewayKnxTpIpRouter() { stop(); if (startup_semaphore_ != nullptr) { vSemaphoreDelete(startup_semaphore_); startup_semaphore_ = nullptr; } if (openknx_lock_ != nullptr) { vSemaphoreDelete(openknx_lock_); openknx_lock_ = nullptr; } } void GatewayKnxTpIpRouter::setConfig(const GatewayKnxConfig& config) { config_ = config; } void GatewayKnxTpIpRouter::setCommissioningOnly(bool enabled) { commissioning_only_ = enabled; } void GatewayKnxTpIpRouter::setGroupWriteHandler(GroupWriteHandler handler) { group_write_handler_ = std::move(handler); } void GatewayKnxTpIpRouter::setGroupObjectWriteHandler(GroupObjectWriteHandler handler) { group_object_write_handler_ = std::move(handler); } const GatewayKnxConfig& GatewayKnxTpIpRouter::config() const { return config_; } bool GatewayKnxTpIpRouter::tpUartOnline() const { return tp_uart_online_; } bool GatewayKnxTpIpRouter::programmingMode() { if (openknx_lock_ == nullptr) { return false; } SemaphoreGuard guard(openknx_lock_); return ets_device_ != nullptr && ets_device_->programmingMode(); } esp_err_t GatewayKnxTpIpRouter::setProgrammingMode(bool enabled) { if (openknx_lock_ == nullptr) { last_error_ = "KNX runtime lock is unavailable"; return ESP_ERR_INVALID_STATE; } SemaphoreGuard guard(openknx_lock_); if (ets_device_ == nullptr) { last_error_ = "KNX OpenKNX runtime is unavailable"; return ESP_ERR_INVALID_STATE; } ets_device_->setProgrammingMode(enabled); setProgrammingLed(enabled); ESP_LOGI(kTag, "KNX programming mode %s namespace=%s", enabled ? "enabled" : "disabled", openknx_namespace_.c_str()); return ESP_OK; } esp_err_t GatewayKnxTpIpRouter::toggleProgrammingMode() { return setProgrammingMode(!programmingMode()); } esp_err_t GatewayKnxTpIpRouter::start(uint32_t task_stack_size, UBaseType_t task_priority) { if (started_ || task_handle_ != nullptr) { return ESP_OK; } if (openknx_lock_ == nullptr || startup_semaphore_ == nullptr) { last_error_ = "failed to allocate KNX runtime synchronization primitives"; return ESP_ERR_NO_MEM; } if (!config_.ip_router_enabled) { last_error_ = "KNXnet/IP router is disabled in config"; return ESP_ERR_NOT_SUPPORTED; } stop_requested_ = false; last_error_.clear(); int log_tp_uart_tx_pin = -1; int log_tp_uart_rx_pin = -1; if (config_.tp_uart.uart_port >= 0 && config_.tp_uart.uart_port < SOC_UART_NUM) { const uart_port_t log_uart_port = static_cast(config_.tp_uart.uart_port); ResolveUartIoPin(log_uart_port, config_.tp_uart.tx_pin, SOC_UART_TX_PIN_IDX, &log_tp_uart_tx_pin); ResolveUartIoPin(log_uart_port, config_.tp_uart.rx_pin, SOC_UART_RX_PIN_IDX, &log_tp_uart_rx_pin); } ESP_LOGI(kTag, "starting KNXnet/IP router namespace=%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(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(), UartPinDescription(config_.tp_uart.rx_pin, log_tp_uart_rx_pin).c_str(), config_.tp_uart.nine_bit_mode, commissioning_only_); if (!configureSocket()) { return ESP_FAIL; } while (xSemaphoreTake(startup_semaphore_, 0) == pdTRUE) { } startup_result_ = ESP_ERR_TIMEOUT; const BaseType_t created = xTaskCreate(&GatewayKnxTpIpRouter::TaskEntry, "gw_knx_ip", task_stack_size, this, task_priority, &task_handle_); if (created != pdPASS) { task_handle_ = nullptr; closeSockets(); return ESP_ERR_NO_MEM; } if (xSemaphoreTake(startup_semaphore_, pdMS_TO_TICKS(10000)) != pdTRUE) { last_error_ = "timed out starting KNXnet/IP OpenKNX runtime"; stop_requested_ = true; closeSockets(); return ESP_ERR_TIMEOUT; } return startup_result_; } esp_err_t GatewayKnxTpIpRouter::stop() { stop_requested_ = true; closeSockets(); const TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); for (int attempt = 0; task_handle_ != nullptr && task_handle_ != current_task && attempt < 50; ++attempt) { vTaskDelay(pdMS_TO_TICKS(10)); } return ESP_OK; } bool GatewayKnxTpIpRouter::started() const { return started_; } const std::string& GatewayKnxTpIpRouter::lastError() const { return last_error_; } bool GatewayKnxTpIpRouter::publishDaliStatus(const GatewayKnxDaliTarget& target, uint8_t actual_level) { if (!started_ || !config_.ip_router_enabled || !shouldRouteDaliApplicationFrames()) { return false; } uint16_t switch_object = 0; uint16_t dimm_object = 0; if (target.kind == GatewayKnxDaliTargetKind::kShortAddress) { if (target.address < 0 || target.address > 63) { return false; } const uint16_t base = kGwReg1AdrKoOffset + kGwReg1AdrKoBlockSize * static_cast(target.address); switch_object = base + kGwReg1KoSwitchState; dimm_object = base + kGwReg1KoDimmState; } else if (target.kind == GatewayKnxDaliTargetKind::kGroup) { if (target.address < 0 || target.address > 15) { return false; } const uint16_t base = kGwReg1GrpKoOffset + kGwReg1GrpKoBlockSize * static_cast(target.address); switch_object = base + kGwReg1KoSwitchState; dimm_object = base + kGwReg1KoDimmState; } else if (target.kind == GatewayKnxDaliTargetKind::kBroadcast) { switch_object = kGwReg1AppKoBroadcastSwitch; dimm_object = kGwReg1AppKoBroadcastDimm; } else { return false; } const uint8_t switch_value = actual_level > 0 ? 1 : 0; const uint8_t dimm_value = DaliArcLevelToDpt5(actual_level); bool emitted = emitOpenKnxGroupValue(switch_object, &switch_value, 1); emitted = emitOpenKnxGroupValue(dimm_object, &dimm_value, 1) || emitted; return emitted; } void GatewayKnxTpIpRouter::TaskEntry(void* arg) { static_cast(arg)->taskLoop(); } esp_err_t GatewayKnxTpIpRouter::initializeRuntime() { { SemaphoreGuard guard(openknx_lock_); auto tp_uart_interface = createOpenKnxTpUartInterface(); if (GatewayKnxConfigUsesTpUart(config_) && tp_uart_interface == nullptr && !last_error_.empty()) { return ESP_FAIL; } ets_device_ = std::make_unique(openknx_namespace_, config_.individual_address, effectiveTunnelAddress(), std::move(tp_uart_interface)); bridge_.setRuntimeContext(ets_device_.get()); knx_ip_parameters_ = std::make_unique( ets_device_->deviceObject(), ets_device_->platform()); openknx_configured_.store(ets_device_->configured()); ESP_LOGI(kTag, "OpenKNX runtime namespace=%s configured=%d ipInterface=0x%04x " "device=0x%04x tunnelClient=0x%04x commissioningOnly=%d", openknx_namespace_.c_str(), ets_device_->configured(), effectiveIpInterfaceIndividualAddress(), ets_device_->individualAddress(), ets_device_->tunnelClientAddress(), commissioning_only_); ets_device_->setFunctionPropertyHandlers( [this](uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len, std::vector* response) { if (!shouldRouteDaliApplicationFrames()) { return false; } return bridge_.handleFunctionPropertyCommand(object_index, property_id, data, len, response); }, [this](uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len, std::vector* response) { if (!shouldRouteDaliApplicationFrames()) { return false; } return bridge_.handleFunctionPropertyState(object_index, property_id, data, len, response); }); ets_device_->setGroupWriteHandler( [this](uint16_t group_address, const uint8_t* data, size_t len) { if (!shouldRouteDaliApplicationFrames()) { return; } const DaliBridgeResult result = group_write_handler_ ? group_write_handler_(group_address, data, len) : bridge_.handleGroupWrite(group_address, data, len); if (!result.ok && !result.error.empty()) { ESP_LOGD(kTag, "secure KNX group write not routed to DALI: %s", result.error.c_str()); } }); ets_device_->setGroupObjectWriteHandler( [this](uint16_t group_object_number, const uint8_t* data, size_t len) { if (!shouldRouteDaliApplicationFrames()) { return IgnoredResult( GwReg1GroupAddressForObject(config_.main_group, group_object_number), group_object_number, "routing blocked by commissioning-only state"); } const DaliBridgeResult result = group_object_write_handler_ ? group_object_write_handler_(group_object_number, data, len) : bridge_.handleGroupObjectWrite(group_object_number, data, len); const bool ignored = getObjectBool(result.metadata, "ignored").value_or(false); if (ignored) { const auto reason = getObjectString(result.metadata, "reason").value_or("ignored"); ESP_LOGW(kTag, "OpenKNX group object %u accepted by ETS but ignored: %s", static_cast(group_object_number), reason.c_str()); } else if (!result.ok && !result.error.empty()) { ESP_LOGW(kTag, "OpenKNX group object %u not routed to DALI: %s", static_cast(group_object_number), result.error.c_str()); } return result; }); ets_device_->setBusFrameSender([this](const uint8_t* data, size_t len) { sendTunnelIndication(data, len); sendRoutingIndication(data, len); }); syncOpenKnxConfigFromDevice(); } if (!configureTpUart()) { last_error_ = last_error_.empty() ? "failed to configure KNX TP-UART" : last_error_; return ESP_FAIL; } if (!configureProgrammingGpio()) { last_error_ = last_error_.empty() ? "failed to configure KNX programming GPIO" : last_error_; return ESP_FAIL; } return ESP_OK; } void GatewayKnxTpIpRouter::taskLoop() { startup_result_ = initializeRuntime(); if (startup_result_ == ESP_OK) { started_ = true; } if (startup_semaphore_ != nullptr) { xSemaphoreGive(startup_semaphore_); } if (startup_result_ != ESP_OK || stop_requested_) { finishTask(); return; } std::array buffer{}; auto run_maintenance = [this]() { { SemaphoreGuard guard(openknx_lock_); if (ets_device_ != nullptr) { pollProgrammingButton(); ets_device_->loop(); tp_uart_online_ = ets_device_->tpUartOnline(); updateProgrammingLed(); } } }; while (!stop_requested_) { const TickType_t now = xTaskGetTickCount(); if (network_refresh_tick_ == 0 || now - network_refresh_tick_ >= pdMS_TO_TICKS(1000)) { refreshNetworkInterfaces(false); pruneStaleTunnelClients(); network_refresh_tick_ = now; } fd_set read_fds; FD_ZERO(&read_fds); int max_fd = -1; if (udp_sock_ >= 0) { FD_SET(udp_sock_, &read_fds); max_fd = std::max(max_fd, udp_sock_); } if (tcp_sock_ >= 0) { FD_SET(tcp_sock_, &read_fds); max_fd = std::max(max_fd, tcp_sock_); } for (const auto& client : tcp_clients_) { if (client.sock >= 0) { FD_SET(client.sock, &read_fds); max_fd = std::max(max_fd, client.sock); } } timeval timeout{}; timeout.tv_sec = 0; timeout.tv_usec = 20000; const int selected = max_fd >= 0 ? select(max_fd + 1, &read_fds, nullptr, nullptr, &timeout) : 0; if (selected < 0) { ESP_LOGW(kTag, "KNXnet/IP socket select failed: errno=%d (%s)", errno, std::strerror(errno)); run_maintenance(); vTaskDelay(pdMS_TO_TICKS(10)); continue; } if (selected == 0) { run_maintenance(); continue; } if (tcp_sock_ >= 0 && FD_ISSET(tcp_sock_, &read_fds)) { handleTcpAccept(); } for (auto& client : tcp_clients_) { if (client.sock >= 0 && FD_ISSET(client.sock, &read_fds)) { handleTcpClient(client); } } sockaddr_in remote{}; socklen_t remote_len = sizeof(remote); if (udp_sock_ >= 0 && FD_ISSET(udp_sock_, &read_fds)) { const int received = recvfrom(udp_sock_, buffer.data(), buffer.size(), 0, reinterpret_cast(&remote), &remote_len); if (received > 0) { handleUdpDatagram(buffer.data(), static_cast(received), remote); } } run_maintenance(); } finishTask(); } void GatewayKnxTpIpRouter::finishTask() { closeSockets(); { SemaphoreGuard guard(openknx_lock_); setProgrammingLed(false); knx_ip_parameters_.reset(); bridge_.setRuntimeContext(nullptr); ets_device_.reset(); openknx_configured_.store(false); } started_ = false; task_handle_ = nullptr; vTaskDelete(nullptr); } void GatewayKnxTpIpRouter::pollProgrammingButton() { if (config_.programming_button_gpio < 0 || ets_device_ == nullptr) { return; } const int level = gpio_get_level(static_cast(config_.programming_button_gpio)); const bool pressed = config_.programming_button_active_low ? level == 0 : level != 0; const TickType_t now = xTaskGetTickCount(); if (pressed && !programming_button_last_pressed_ && now - programming_button_last_toggle_tick_ >= pdMS_TO_TICKS(200)) { ets_device_->toggleProgrammingMode(); ESP_LOGI(kTag, "KNX programming mode %s namespace=%s", ets_device_->programmingMode() ? "enabled" : "disabled", openknx_namespace_.c_str()); programming_button_last_toggle_tick_ = now; } programming_button_last_pressed_ = pressed; } void GatewayKnxTpIpRouter::updateProgrammingLed() { if (config_.programming_led_gpio < 0 || ets_device_ == nullptr) { return; } const bool programming_mode = ets_device_->programmingMode(); if (programming_mode == programming_led_state_) { return; } setProgrammingLed(programming_mode); } void GatewayKnxTpIpRouter::setProgrammingLed(bool on) { if (config_.programming_led_gpio < 0) { programming_led_state_ = on; return; } const bool level = config_.programming_led_active_high ? on : !on; gpio_set_level(static_cast(config_.programming_led_gpio), level ? 1 : 0); programming_led_state_ = on; } void GatewayKnxTpIpRouter::closeSockets() { if (udp_sock_ >= 0) { shutdown(udp_sock_, SHUT_RDWR); close(udp_sock_); udp_sock_ = -1; } if (tcp_sock_ >= 0) { shutdown(tcp_sock_, SHUT_RDWR); close(tcp_sock_); tcp_sock_ = -1; } for (auto& client : tcp_clients_) { closeTcpClient(client); } active_tcp_sock_ = -1; multicast_joined_interfaces_.clear(); network_refresh_tick_ = 0; for (auto& client : tunnel_clients_) { resetTunnelClient(client); } last_tunnel_channel_id_ = 0; } bool GatewayKnxTpIpRouter::configureSocket() { udp_sock_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (udp_sock_ < 0) { last_error_ = ErrnoDetail("failed to create KNXnet/IP UDP socket", errno); ESP_LOGE(kTag, "%s", last_error_.c_str()); return false; } int broadcast = 1; if (setsockopt(udp_sock_, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast)) < 0) { ESP_LOGW(kTag, "failed to enable broadcast for KNX UDP port %u: errno=%d (%s)", static_cast(config_.udp_port), errno, std::strerror(errno)); } sockaddr_in bind_addr{}; bind_addr.sin_family = AF_INET; bind_addr.sin_addr.s_addr = htonl(INADDR_ANY); bind_addr.sin_port = htons(config_.udp_port); if (bind(udp_sock_, reinterpret_cast(&bind_addr), sizeof(bind_addr)) < 0) { const int saved_errno = errno; last_error_ = ErrnoDetail("failed to bind KNXnet/IP UDP socket on port " + std::to_string(config_.udp_port), saved_errno); ESP_LOGE(kTag, "%s", last_error_.c_str()); closeSockets(); return false; } timeval timeout{}; timeout.tv_sec = 0; timeout.tv_usec = 20000; if (setsockopt(udp_sock_, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) { ESP_LOGW(kTag, "failed to set KNX UDP receive timeout on port %u: errno=%d (%s)", static_cast(config_.udp_port), errno, std::strerror(errno)); } if (config_.multicast_enabled) { uint8_t multicast_loop = 0; if (setsockopt(udp_sock_, IPPROTO_IP, IP_MULTICAST_LOOP, &multicast_loop, sizeof(multicast_loop)) < 0) { ESP_LOGW(kTag, "failed to disable KNX multicast loopback for %s: errno=%d (%s)", config_.multicast_address.c_str(), errno, std::strerror(errno)); } refreshNetworkInterfaces(true); } tcp_sock_ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (tcp_sock_ < 0) { last_error_ = ErrnoDetail("failed to create KNXnet/IP TCP socket", errno); ESP_LOGE(kTag, "%s", last_error_.c_str()); closeSockets(); return false; } int reuse = 1; if (setsockopt(tcp_sock_, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) { ESP_LOGW(kTag, "failed to enable TCP reuse for KNX port %u: errno=%d (%s)", static_cast(config_.udp_port), errno, std::strerror(errno)); } sockaddr_in tcp_bind_addr{}; tcp_bind_addr.sin_family = AF_INET; tcp_bind_addr.sin_addr.s_addr = htonl(INADDR_ANY); tcp_bind_addr.sin_port = htons(config_.udp_port); if (bind(tcp_sock_, reinterpret_cast(&tcp_bind_addr), sizeof(tcp_bind_addr)) < 0) { const int saved_errno = errno; last_error_ = ErrnoDetail("failed to bind KNXnet/IP TCP socket on port " + std::to_string(config_.udp_port), saved_errno); ESP_LOGE(kTag, "%s", last_error_.c_str()); closeSockets(); return false; } if (listen(tcp_sock_, static_cast(kMaxTcpClients)) < 0) { const int saved_errno = errno; last_error_ = ErrnoDetail("failed to listen on KNXnet/IP TCP port " + std::to_string(config_.udp_port), saved_errno); ESP_LOGE(kTag, "%s", last_error_.c_str()); closeSockets(); return false; } ESP_LOGI(kTag, "KNXnet/IP listening on UDP/TCP port %u", static_cast(config_.udp_port)); return true; } void GatewayKnxTpIpRouter::handleTcpAccept() { sockaddr_in remote{}; socklen_t remote_len = sizeof(remote); const int client_sock = accept(tcp_sock_, reinterpret_cast(&remote), &remote_len); if (client_sock < 0) { ESP_LOGW(kTag, "failed to accept KNXnet/IP TCP client: errno=%d (%s)", errno, std::strerror(errno)); return; } TcpClient* slot = nullptr; for (auto& client : tcp_clients_) { if (client.sock < 0) { slot = &client; break; } } if (slot == nullptr) { ESP_LOGW(kTag, "reject KNXnet/IP TCP client from %s: no free TCP slots", EndpointString(remote).c_str()); close(client_sock); return; } slot->sock = client_sock; slot->remote = remote; slot->rx_buffer.clear(); slot->last_activity_tick = xTaskGetTickCount(); ESP_LOGI(kTag, "accepted KNXnet/IP TCP client from %s", EndpointString(remote).c_str()); } void GatewayKnxTpIpRouter::handleTcpClient(TcpClient& client) { if (client.sock < 0) { return; } std::array buffer{}; const int received = recv(client.sock, buffer.data(), buffer.size(), 0); if (received <= 0) { ESP_LOGI(kTag, "closed KNXnet/IP TCP client from %s", EndpointString(client.remote).c_str()); closeTcpClient(client); return; } client.last_activity_tick = xTaskGetTickCount(); client.rx_buffer.insert(client.rx_buffer.end(), buffer.begin(), buffer.begin() + received); while (client.rx_buffer.size() >= 6) { uint16_t service = 0; uint16_t total_len = 0; if (!ParseKnxNetIpHeader(client.rx_buffer.data(), client.rx_buffer.size(), &service, &total_len)) { ESP_LOGW(kTag, "invalid KNXnet/IP TCP packet from %s; closing stream", EndpointString(client.remote).c_str()); closeTcpClient(client); return; } if (client.rx_buffer.size() < total_len) { return; } std::vector packet(client.rx_buffer.begin(), client.rx_buffer.begin() + total_len); client.rx_buffer.erase(client.rx_buffer.begin(), client.rx_buffer.begin() + total_len); active_tcp_sock_ = client.sock; handleUdpDatagram(packet.data(), packet.size(), client.remote); active_tcp_sock_ = -1; if (client.sock < 0) { return; } } } void GatewayKnxTpIpRouter::closeTcpClient(TcpClient& client) { if (client.sock < 0) { client.rx_buffer.clear(); return; } const int sock = client.sock; for (auto& tunnel : tunnel_clients_) { if (tunnel.connected && tunnel.tcp_sock == sock) { resetTunnelClient(tunnel); } } if (active_tcp_sock_ == sock) { active_tcp_sock_ = -1; } shutdown(sock, SHUT_RDWR); close(sock); client.sock = -1; client.rx_buffer.clear(); client.last_activity_tick = 0; } void GatewayKnxTpIpRouter::refreshNetworkInterfaces(bool force_log) { if (!config_.multicast_enabled || udp_sock_ < 0) { return; } const auto netifs = ActiveKnxNetifs(); if (netifs.empty()) { SemaphoreGuard guard(openknx_lock_); if (ets_device_ != nullptr) { ets_device_->setNetworkInterface(nullptr); } if (force_log) { ESP_LOGW(kTag, "KNX multicast group %s not joined yet: no IPv4 interface is up", config_.multicast_address.c_str()); } return; } { SemaphoreGuard guard(openknx_lock_); if (ets_device_ != nullptr) { ets_device_->setNetworkInterface(netifs.front().netif); } } const uint32_t multicast_address = inet_addr(config_.multicast_address.c_str()); for (const auto& netif : netifs) { if (std::find(multicast_joined_interfaces_.begin(), multicast_joined_interfaces_.end(), netif.address) != multicast_joined_interfaces_.end()) { continue; } ip_mreq mreq{}; mreq.imr_multiaddr.s_addr = multicast_address; mreq.imr_interface.s_addr = netif.address; if (setsockopt(udp_sock_, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) { ESP_LOGW(kTag, "failed to join KNX multicast group %s on %s %s UDP port %u: errno=%d (%s)", config_.multicast_address.c_str(), netif.key, Ipv4String(netif.address).c_str(), static_cast(config_.udp_port), errno, std::strerror(errno)); continue; } multicast_joined_interfaces_.push_back(netif.address); ESP_LOGI(kTag, "joined KNX multicast group %s on %s %s UDP port %u", config_.multicast_address.c_str(), netif.key, Ipv4String(netif.address).c_str(), static_cast(config_.udp_port)); } } std::unique_ptr GatewayKnxTpIpRouter::createOpenKnxTpUartInterface() { if (!GatewayKnxConfigUsesTpUart(config_)) { tp_uart_port_ = -1; tp_uart_tx_pin_ = -1; tp_uart_rx_pin_ = -1; tp_uart_online_ = false; ESP_LOGI(kTag, "KNX TP-UART disabled by UART port; KNXnet/IP uses IP-only runtime"); return nullptr; } const auto& serial = config_.tp_uart; if (serial.uart_port < 0 || serial.uart_port > 2) { last_error_ = "invalid KNX TP-UART port " + std::to_string(serial.uart_port); ESP_LOGE(kTag, "%s", last_error_.c_str()); return nullptr; } const uart_port_t uart_port = static_cast(serial.uart_port); int tx_pin = UART_PIN_NO_CHANGE; int rx_pin = UART_PIN_NO_CHANGE; const bool tx_pin_ok = ResolveUartIoPin(uart_port, serial.tx_pin, SOC_UART_TX_PIN_IDX, &tx_pin); const bool rx_pin_ok = ResolveUartIoPin(uart_port, serial.rx_pin, SOC_UART_RX_PIN_IDX, &rx_pin); if (!tx_pin_ok || !rx_pin_ok) { last_error_ = "KNX TP-UART UART" + std::to_string(serial.uart_port) + " has no ESP-IDF default " + (!tx_pin_ok ? std::string("TX") : std::string("")) + (!tx_pin_ok && !rx_pin_ok ? "/" : "") + (!rx_pin_ok ? std::string("RX") : std::string("")) + " pin; configure explicit txPin/rxPin values"; ESP_LOGE(kTag, "%s", last_error_.c_str()); return nullptr; } tp_uart_port_ = serial.uart_port; tp_uart_tx_pin_ = tx_pin; tp_uart_rx_pin_ = rx_pin; return std::make_unique( uart_port, serial.tx_pin, serial.rx_pin, serial.rx_buffer_size, serial.tx_buffer_size, serial.nine_bit_mode); } bool GatewayKnxTpIpRouter::configureTpUart() { if (tp_uart_port_ < 0) { return true; } if (ets_device_ == nullptr || !ets_device_->hasTpUart()) { last_error_ = "KNX TP-UART interface is unavailable in OpenKNX runtime"; ESP_LOGE(kTag, "%s", last_error_.c_str()); return false; } const TickType_t startup_timeout_ticks = pdMS_TO_TICKS(config_.tp_uart.startup_timeout_ms); const TickType_t retry_poll_ticks = std::max(1, pdMS_TO_TICKS(20)); const TickType_t startup_begin_tick = xTaskGetTickCount(); tp_uart_online_ = ets_device_->enableTpUart(true); while (!tp_uart_online_ && startup_timeout_ticks > 0 && (xTaskGetTickCount() - startup_begin_tick) < startup_timeout_ticks) { vTaskDelay(retry_poll_ticks); ets_device_->loop(); tp_uart_online_ = ets_device_->tpUartOnline(); } if (!tp_uart_online_) { last_error_ = "OpenKNX failed to initialize KNX TP-UART uart=" + std::to_string(config_.tp_uart.uart_port) + " tx=" + UartPinDescription(config_.tp_uart.tx_pin, tp_uart_tx_pin_) + " rx=" + UartPinDescription(config_.tp_uart.rx_pin, tp_uart_rx_pin_); const bool configured = ets_device_ != nullptr && ets_device_->configured(); ESP_LOGW(kTag, "%s; continuing KNXnet/IP in %s IP mode while TP-UART stays offline", last_error_.c_str(), configured ? "configured" : "commissioning-only"); tp_uart_port_ = -1; tp_uart_online_ = false; return true; } const TickType_t startup_elapsed_ticks = xTaskGetTickCount() - startup_begin_tick; if (startup_elapsed_ticks > 0) { ESP_LOGI(kTag, "KNX TP-UART startup settled after %lu ms", static_cast(pdTICKS_TO_MS(startup_elapsed_ticks))); } ESP_LOGI(kTag, "KNX TP-UART online uart=%d tx=%s rx=%s baud=%u nineBit=%d", config_.tp_uart.uart_port, UartPinDescription(config_.tp_uart.tx_pin, tp_uart_tx_pin_).c_str(), UartPinDescription(config_.tp_uart.rx_pin, tp_uart_rx_pin_).c_str(), static_cast(config_.tp_uart.baudrate), config_.tp_uart.nine_bit_mode); return true; } bool GatewayKnxTpIpRouter::configureProgrammingGpio() { programming_button_last_pressed_ = false; programming_button_last_toggle_tick_ = 0; programming_led_state_ = false; if (config_.programming_button_gpio >= 0) { gpio_config_t button_config{}; button_config.pin_bit_mask = 1ULL << static_cast(config_.programming_button_gpio); button_config.mode = GPIO_MODE_INPUT; button_config.pull_up_en = config_.programming_button_active_low ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE; button_config.pull_down_en = config_.programming_button_active_low ? GPIO_PULLDOWN_DISABLE : GPIO_PULLDOWN_ENABLE; button_config.intr_type = GPIO_INTR_DISABLE; const esp_err_t err = gpio_config(&button_config); if (err != ESP_OK) { last_error_ = EspErrDetail("failed to configure KNX programming button GPIO" + std::to_string(config_.programming_button_gpio), err); ESP_LOGE(kTag, "%s", last_error_.c_str()); return false; } } if (config_.programming_led_gpio >= 0) { gpio_config_t led_config{}; led_config.pin_bit_mask = 1ULL << static_cast(config_.programming_led_gpio); led_config.mode = GPIO_MODE_OUTPUT; led_config.pull_up_en = GPIO_PULLUP_DISABLE; led_config.pull_down_en = GPIO_PULLDOWN_DISABLE; led_config.intr_type = GPIO_INTR_DISABLE; const esp_err_t err = gpio_config(&led_config); if (err != ESP_OK) { last_error_ = EspErrDetail("failed to configure KNX programming LED GPIO" + std::to_string(config_.programming_led_gpio), err); ESP_LOGE(kTag, "%s", last_error_.c_str()); return false; } setProgrammingLed(false); } return true; } void GatewayKnxTpIpRouter::handleUdpDatagram(const uint8_t* data, size_t len, const sockaddr_in& remote) { uint16_t service = 0; uint16_t total_len = 0; if (!ParseKnxNetIpHeader(data, len, &service, &total_len)) { return; } const uint8_t* body = data + 6; const size_t body_len = total_len - 6; if (IsKnxNetIpSecureService(service)) { handleSecureService(service, body, body_len, remote); return; } switch (service) { case kServiceSearchRequest: case kServiceSearchRequestExt: handleSearchRequest(service, data, total_len, remote); break; case kServiceDescriptionRequest: handleDescriptionRequest(data, total_len, remote); break; case kServiceDeviceConfigurationRequest: handleDeviceConfigurationRequest(data, total_len, remote); break; case kServiceDeviceConfigurationAck: case kServiceTunnellingAck: if (body_len >= 4) { ESP_LOGD(kTag, "rx KNXnet/IP ack service=0x%04x channel=%u seq=%u status=0x%02x from %s", static_cast(service), static_cast(body[1]), static_cast(body[2]), static_cast(body[3]), EndpointString(remote).c_str()); TunnelClient* client = findTunnelClient(body[1]); if (client != nullptr) { client->last_activity_tick = xTaskGetTickCount(); if (service == kServiceTunnellingAck && body[3] == kKnxNoError && !client->last_tunnel_confirmation_packet.empty() && client->last_tunnel_confirmation_sequence == body[2]) { client->last_tunnel_confirmation_packet.clear(); } } } break; case kServiceRoutingIndication: if (config_.multicast_enabled) { handleRoutingIndication(data, total_len); } break; case kServiceTunnellingRequest: if (config_.tunnel_enabled) { handleTunnellingRequest(data, total_len, remote); } break; case kServiceConnectRequest: if (config_.tunnel_enabled) { handleConnectRequest(data, total_len, remote); } break; case kServiceConnectionStateRequest: handleConnectionStateRequest(data, total_len, remote); break; case kServiceDisconnectRequest: handleDisconnectRequest(data, total_len, remote); break; default: ESP_LOGD(kTag, "ignore KNXnet/IP service=0x%04x len=%u from %s", static_cast(service), static_cast(body_len), EndpointString(remote).c_str()); break; } } void GatewayKnxTpIpRouter::handleSearchRequest(uint16_t service, const uint8_t* packet_data, size_t len, const sockaddr_in& remote) { if (packet_data == nullptr || len < kKnxNetIpHeaderSize + LEN_IPHPAI) { ESP_LOGW(kTag, "invalid KNXnet/IP search request from %s len=%u", EndpointString(remote).c_str(), static_cast(len)); return; } std::vector request_packet(packet_data, packet_data + len); KnxIpSearchRequest request(request_packet.data(), static_cast(request_packet.size())); auto& request_hpai = request.hpai(); if (OpenKnxHpaiUsesUnsupportedProtocol(request_hpai, currentTransportAllowsTcpHpai())) { ESP_LOGW(kTag, "ignore KNXnet/IP search request from %s: unsupported HPAI protocol", EndpointString(remote).c_str()); return; } sockaddr_in response_remote = EndpointFromOpenKnxHpai(request_hpai, remote); selectOpenKnxNetworkInterface(response_remote); const auto hpai = localHpaiForRemote(response_remote, currentTransportAllowsTcpHpai()); if (!hpai.has_value()) { ESP_LOGW(kTag, "cannot send KNXnet/IP search response to %s: no active IPv4 interface", EndpointString(response_remote).c_str()); return; } std::vector body_resp; body_resp.insert(body_resp.end(), hpai->begin(), hpai->end()); auto dev_dib = buildDeviceInfoDib(response_remote); body_resp.insert(body_resp.end(), dev_dib.begin(), dev_dib.end()); auto svc_dib = buildSupportedServiceDib(); body_resp.insert(body_resp.end(), svc_dib.begin(), svc_dib.end()); if (service == kServiceSearchRequestExt) { auto ext_dib = buildExtendedDeviceInfoDib(); body_resp.insert(body_resp.end(), ext_dib.begin(), ext_dib.end()); auto ip_dib = buildIpConfigDib(response_remote, false); body_resp.insert(body_resp.end(), ip_dib.begin(), ip_dib.end()); auto current_ip_dib = buildIpConfigDib(response_remote, true); body_resp.insert(body_resp.end(), current_ip_dib.begin(), current_ip_dib.end()); auto addresses_dib = buildKnxAddressesDib(); body_resp.insert(body_resp.end(), addresses_dib.begin(), addresses_dib.end()); auto tunneling_dib = buildTunnelingInfoDib(); body_resp.insert(body_resp.end(), tunneling_dib.begin(), tunneling_dib.end()); } const auto response_packet = OpenKnxIpPacket( 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(service), openknx_namespace_.c_str(), static_cast(config_.main_group), Ipv4String(response_remote.sin_addr.s_addr).c_str(), static_cast(ntohs(response_remote.sin_port)), static_cast((*hpai)[2]), static_cast((*hpai)[3]), static_cast((*hpai)[4]), static_cast((*hpai)[5]), static_cast(config_.udp_port)); } void GatewayKnxTpIpRouter::handleDescriptionRequest(const uint8_t* packet_data, size_t len, const sockaddr_in& remote) { if (packet_data == nullptr || len < kKnxNetIpHeaderSize + LEN_IPHPAI) { ESP_LOGW(kTag, "invalid KNXnet/IP description request from %s len=%u", EndpointString(remote).c_str(), static_cast(len)); return; } std::vector request_packet(packet_data, packet_data + len); KnxIpDescriptionRequest request(request_packet.data(), static_cast(request_packet.size())); auto& hpai = request.hpaiCtrl(); if (OpenKnxHpaiUsesUnsupportedProtocol(hpai, currentTransportAllowsTcpHpai())) { ESP_LOGW(kTag, "ignore KNXnet/IP description request from %s: unsupported HPAI protocol", EndpointString(remote).c_str()); return; } const sockaddr_in response_remote = EndpointFromOpenKnxHpai(hpai, remote); selectOpenKnxNetworkInterface(response_remote); auto device = buildDeviceInfoDib(response_remote); auto services = buildSupportedServiceDib(); std::vector body_resp; auto extended = buildExtendedDeviceInfoDib(); auto ip_config = buildIpConfigDib(response_remote, false); auto current_ip_config = buildIpConfigDib(response_remote, true); auto addresses = buildKnxAddressesDib(); auto tunneling = buildTunnelingInfoDib(); body_resp.reserve(device.size() + services.size() + extended.size() + ip_config.size() + current_ip_config.size() + addresses.size() + tunneling.size()); body_resp.insert(body_resp.end(), device.begin(), device.end()); body_resp.insert(body_resp.end(), services.begin(), services.end()); body_resp.insert(body_resp.end(), extended.begin(), extended.end()); body_resp.insert(body_resp.end(), ip_config.begin(), ip_config.end()); body_resp.insert(body_resp.end(), current_ip_config.begin(), current_ip_config.end()); body_resp.insert(body_resp.end(), addresses.begin(), addresses.end()); 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(advertisedMedium()), tp_uart_online_, Ipv4String(response_remote.sin_addr.s_addr).c_str(), static_cast(ntohs(response_remote.sin_port))); } void GatewayKnxTpIpRouter::handleRoutingIndication(const uint8_t* packet_data, size_t len) { if (packet_data == nullptr || len < kKnxNetIpHeaderSize + 2) { return; } std::vector packet(packet_data, packet_data + len); KnxIpRoutingIndication routing(packet.data(), static_cast(packet.size())); CemiFrame& frame = routing.frame(); if (!frame.valid()) { ESP_LOGW(kTag, "invalid OpenKNX routing cEMI len=%u", static_cast(len)); return; } const uint8_t* cemi = frame.data(); const size_t cemi_len = frame.dataLength(); bool consumed_by_local_application = false; if (ets_device_ != nullptr && MatchesOpenKnxLocalIndividualAddress(frame, *ets_device_)) { std::vector local_tunnel_frame; if (BuildLocalRoutingTunnelFrame(cemi, cemi_len, &local_tunnel_frame)) { consumed_by_local_application = handleOpenKnxTunnelFrame( local_tunnel_frame.data(), local_tunnel_frame.size(), nullptr, kServiceRoutingIndication, frame.messageCode() == L_data_ind ? cemi : nullptr, frame.messageCode() == L_data_ind ? cemi_len : 0); } } if (consumed_by_local_application) { return; } const bool consumed_by_openknx = handleOpenKnxBusFrame(cemi, cemi_len); const bool routed_to_dali = routeOpenKnxGroupWrite(cemi, cemi_len, "KNX routing indication"); const bool sent_to_tp = transmitOpenKnxTpFrame(cemi, cemi_len); if (!consumed_by_openknx && !routed_to_dali && !sent_to_tp) { ESP_LOGD(kTag, "KNX routing indication ignored: no OpenKNX/DALI handler matched"); } } GatewayKnxTpIpRouter::TunnelClient* GatewayKnxTpIpRouter::findTunnelClient( uint8_t channel_id) { if (channel_id == 0) { return nullptr; } for (auto& client : tunnel_clients_) { if (client.connected && client.channel_id == channel_id) { return &client; } } return nullptr; } const GatewayKnxTpIpRouter::TunnelClient* GatewayKnxTpIpRouter::findTunnelClient( uint8_t channel_id) const { if (channel_id == 0) { return nullptr; } for (const auto& client : tunnel_clients_) { if (client.connected && client.channel_id == channel_id) { return &client; } } return nullptr; } void GatewayKnxTpIpRouter::resetTunnelClient(TunnelClient& client) { if (client.connected) { ESP_LOGI(kTag, "closed KNXnet/IP tunnel channel=%u type=0x%02x data=%s", static_cast(client.channel_id), static_cast(client.connection_type), EndpointString(client.data_remote).c_str()); } client = TunnelClient{}; } uint8_t GatewayKnxTpIpRouter::nextTunnelChannelId() const { uint8_t candidate = last_tunnel_channel_id_; for (int attempts = 0; attempts < 255; ++attempts) { candidate = static_cast(candidate + 1); if (candidate == 0) { candidate = 1; } if (findTunnelClient(candidate) == nullptr) { return candidate; } } return 0; } uint16_t GatewayKnxTpIpRouter::effectiveTunnelAddressForSlot(size_t slot) const { const uint16_t first = effectiveTunnelAddress(); const uint16_t line = first & 0xff00; uint16_t device = static_cast((first & 0x00ff) + slot); if (device == 0 || device > 0xff) { device = static_cast(1 + slot); } return static_cast(line | (device & 0x00ff)); } GatewayKnxTpIpRouter::TunnelClient* GatewayKnxTpIpRouter::allocateTunnelClient( const sockaddr_in& control_remote, const sockaddr_in& data_remote, uint8_t connection_type) { TunnelClient* free_client = nullptr; size_t free_index = 0; for (size_t index = 0; index < tunnel_clients_.size(); ++index) { auto& client = tunnel_clients_[index]; const bool same_tcp_stream = active_tcp_sock_ >= 0 && client.tcp_sock == active_tcp_sock_; const bool same_udp_endpoints = EndpointEquals(client.control_remote, control_remote) && EndpointEquals(client.data_remote, data_remote); if (client.connected && client.connection_type == connection_type && (same_tcp_stream || same_udp_endpoints)) { ESP_LOGW(kTag, "replacing existing KNXnet/IP tunnel channel=%u for endpoint %s", static_cast(client.channel_id), EndpointString(data_remote).c_str()); resetTunnelClient(client); free_client = &client; free_index = index; break; } if (!client.connected && free_client == nullptr) { free_client = &client; free_index = index; } } if (free_client == nullptr) { return nullptr; } const uint8_t channel_id = nextTunnelChannelId(); if (channel_id == 0) { return nullptr; } free_client->connected = true; free_client->channel_id = channel_id; free_client->connection_type = connection_type; free_client->received_sequence = 255; free_client->send_sequence = 0; free_client->individual_address = effectiveTunnelAddressForSlot(free_index); free_client->last_activity_tick = xTaskGetTickCount(); free_client->control_remote = control_remote; free_client->data_remote = data_remote; free_client->tcp_sock = active_tcp_sock_; last_tunnel_channel_id_ = channel_id; return free_client; } void GatewayKnxTpIpRouter::pruneStaleTunnelClients() { const TickType_t now = xTaskGetTickCount(); const TickType_t timeout = pdMS_TO_TICKS(120000); for (auto& client : tunnel_clients_) { if (!client.connected || client.last_activity_tick == 0) { continue; } if (now - client.last_activity_tick > timeout) { ESP_LOGW(kTag, "closing stale KNXnet/IP tunnel channel=%u after heartbeat timeout", static_cast(client.channel_id)); resetTunnelClient(client); } } } void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* packet_data, size_t len, const sockaddr_in& remote) { if (packet_data == nullptr || len < kKnxNetIpHeaderSize + LEN_CH + 2) { ESP_LOGW(kTag, "invalid KNXnet/IP tunnelling request from %s len=%u", EndpointString(remote).c_str(), static_cast(len)); return; } std::vector packet(packet_data, packet_data + len); KnxIpTunnelingRequest tunneling(packet.data(), static_cast(packet.size())); auto& header = tunneling.connectionHeader(); if (header.length() != LEN_CH) { ESP_LOGW(kTag, "invalid KNXnet/IP tunnelling header from %s len=%u chLen=%u", EndpointString(remote).c_str(), static_cast(len), static_cast(header.length())); return; } const uint8_t channel_id = header.channelId(); const uint8_t sequence = header.sequenceCounter(); TunnelClient* client = findTunnelClient(channel_id); if (client == nullptr) { ESP_LOGW(kTag, "reject KNXnet/IP tunnelling request channel=%u seq=%u from %s: no connection", static_cast(channel_id), static_cast(sequence), EndpointString(remote).c_str()); sendTunnellingAck(channel_id, sequence, kKnxErrorConnectionId, remote); return; } const bool same_tcp_stream = client->tcp_sock >= 0 && active_tcp_sock_ == client->tcp_sock; if (!same_tcp_stream && !EndpointEquals(remote, client->data_remote)) { ESP_LOGW(kTag, "reject KNXnet/IP tunnelling request channel=%u seq=%u from %s: expected data endpoint %s", static_cast(channel_id), static_cast(sequence), EndpointString(remote).c_str(), EndpointString(client->data_remote).c_str()); sendTunnellingAck(channel_id, sequence, kKnxErrorConnectionId, remote); return; } CemiFrame& frame = tunneling.frame(); if (!frame.valid()) { ESP_LOGW(kTag, "invalid OpenKNX tunnel cEMI channel=%u seq=%u from %s", static_cast(channel_id), static_cast(sequence), EndpointString(remote).c_str()); return; } if (frame.messageCode() == L_data_req && frame.sourceAddress() == 0) { frame.sourceAddress(client->individual_address); } const uint8_t* cemi = frame.data(); const size_t cemi_len = frame.dataLength(); const std::vector current_cemi(cemi, cemi + cemi_len); const bool duplicate_sequence = sequence == client->received_sequence; const bool duplicate_payload = duplicate_sequence && client->last_received_cemi == current_cemi; if (duplicate_payload) { ESP_LOGD(kTag, "duplicate KNXnet/IP tunnelling request channel=%u seq=%u", static_cast(channel_id), static_cast(sequence)); sendTunnellingAck(channel_id, sequence, kKnxNoError, client->data_remote); if (!client->last_tunnel_confirmation_packet.empty()) { if (sendPacketToTunnelClient(*client, client->last_tunnel_confirmation_packet)) { ESP_LOGI(kTag, "resent cached KNXnet/IP tunnel confirmation channel=%u confirmSeq=%u after duplicate req seq=%u to %s", static_cast(channel_id), static_cast(client->last_tunnel_confirmation_sequence), static_cast(sequence), EndpointString(client->data_remote).c_str()); } else { ESP_LOGW(kTag, "failed to resend cached KNXnet/IP tunnel confirmation channel=%u confirmSeq=%u to %s", static_cast(channel_id), static_cast(client->last_tunnel_confirmation_sequence), EndpointString(client->data_remote).c_str()); } } return; } if (duplicate_sequence) { ESP_LOGW(kTag, "accept KNXnet/IP tunnelling request channel=%u with repeated seq=%u because cEMI payload changed", static_cast(channel_id), static_cast(sequence)); } else if (static_cast(sequence - 1) != client->received_sequence) { ESP_LOGW(kTag, "reject KNXnet/IP tunnelling request channel=%u seq=%u expected=%u from %s", static_cast(channel_id), static_cast(sequence), static_cast(static_cast(client->received_sequence + 1)), EndpointString(remote).c_str()); sendTunnellingAck(channel_id, sequence, kKnxErrorSequenceNumber, remote); return; } client->received_sequence = sequence; client->last_received_cemi = current_cemi; client->last_activity_tick = xTaskGetTickCount(); sendTunnellingAck(channel_id, sequence, kKnxNoError, client->data_remote); ESP_LOGI(kTag, "rx KNXnet/IP tunnelling request channel=%u seq=%u cemiLen=%u from %s", static_cast(channel_id), static_cast(sequence), static_cast(cemi_len), EndpointString(remote).c_str()); const bool consumed_by_openknx = handleOpenKnxTunnelFrame( cemi, cemi_len, client, kServiceTunnellingRequest); const bool routed_to_dali = routeOpenKnxGroupWrite(cemi, cemi_len, "KNX tunnel frame"); if (!consumed_by_openknx && routed_to_dali) { std::vector tunnel_confirmation; if (BuildTunnelConfirmationFrame(cemi, cemi_len, &tunnel_confirmation)) { sendCemiFrameToClient(*client, kServiceTunnellingRequest, tunnel_confirmation.data(), tunnel_confirmation.size()); } } if (consumed_by_openknx || routed_to_dali) { return; } } void GatewayKnxTpIpRouter::handleDeviceConfigurationRequest(const uint8_t* packet_data, size_t len, const sockaddr_in& remote) { if (packet_data == nullptr || len < kKnxNetIpHeaderSize + LEN_CH + 2) { ESP_LOGW(kTag, "invalid KNXnet/IP device-configuration request from %s len=%u", EndpointString(remote).c_str(), static_cast(len)); return; } std::vector packet(packet_data, packet_data + len); KnxIpConfigRequest config_request(packet.data(), static_cast(packet.size())); auto& header = config_request.connectionHeader(); if (header.length() != LEN_CH) { ESP_LOGW(kTag, "invalid KNXnet/IP device-configuration header from %s len=%u chLen=%u", EndpointString(remote).c_str(), static_cast(len), static_cast(header.length())); return; } const uint8_t channel_id = header.channelId(); const uint8_t sequence = header.sequenceCounter(); TunnelClient* client = findTunnelClient(channel_id); if (client == nullptr) { ESP_LOGW(kTag, "reject KNXnet/IP device-configuration request channel=%u seq=%u from %s: no connection", static_cast(channel_id), static_cast(sequence), EndpointString(remote).c_str()); sendDeviceConfigurationAck(channel_id, sequence, kKnxErrorConnectionId, remote); return; } const bool same_tcp_stream = client->tcp_sock >= 0 && active_tcp_sock_ == client->tcp_sock; if (!same_tcp_stream && !EndpointEquals(remote, client->data_remote)) { ESP_LOGW(kTag, "reject KNXnet/IP device-configuration request channel=%u seq=%u from %s: expected data endpoint %s", static_cast(channel_id), static_cast(sequence), EndpointString(remote).c_str(), EndpointString(client->data_remote).c_str()); sendDeviceConfigurationAck(channel_id, sequence, kKnxErrorConnectionId, remote); return; } client->last_activity_tick = xTaskGetTickCount(); sendDeviceConfigurationAck(channel_id, sequence, kKnxNoError, client->data_remote); CemiFrame& frame = config_request.frame(); if (!frame.valid()) { ESP_LOGW(kTag, "invalid OpenKNX device-configuration cEMI channel=%u seq=%u from %s", static_cast(channel_id), static_cast(sequence), EndpointString(remote).c_str()); return; } const uint8_t* cemi = frame.data(); const size_t cemi_len = frame.dataLength(); ESP_LOGI(kTag, "rx KNXnet/IP device-configuration request channel=%u seq=%u cemiLen=%u from %s", static_cast(channel_id), static_cast(sequence), static_cast(cemi_len), EndpointString(remote).c_str()); if (!handleOpenKnxTunnelFrame(cemi, cemi_len, client, kServiceDeviceConfigurationRequest)) { ESP_LOGW(kTag, "KNXnet/IP device-configuration cEMI was not consumed by OpenKNX cemiLen=%u", static_cast(cemi_len)); } } void GatewayKnxTpIpRouter::handleConnectRequest(const uint8_t* packet_data, size_t len, const sockaddr_in& remote) { if (packet_data == nullptr || len < kKnxNetIpHeaderSize + 2 * LEN_IPHPAI + 2) { ESP_LOGW(kTag, "invalid KNXnet/IP connect request from %s len=%u", EndpointString(remote).c_str(), static_cast(len)); return; } std::vector packet(packet_data, packet_data + len); KnxIpConnectRequest request(packet.data(), static_cast(packet.size())); auto& control_hpai = request.hpaiCtrl(); auto& data_hpai = request.hpaiData(); if (OpenKnxHpaiUsesUnsupportedProtocol(control_hpai, currentTransportAllowsTcpHpai()) || OpenKnxHpaiUsesUnsupportedProtocol(data_hpai, currentTransportAllowsTcpHpai())) { ESP_LOGW(kTag, "reject KNXnet/IP connect from %s: unsupported HPAI protocol", EndpointString(remote).c_str()); sendConnectResponse(0, kKnxErrorConnectionType, remote, kKnxConnectionTypeTunnel, 0); return; } sockaddr_in control_remote = EndpointFromOpenKnxHpai(control_hpai, remote); sockaddr_in data_remote = EndpointFromOpenKnxHpai(data_hpai, remote); selectOpenKnxNetworkInterface(control_remote); auto& cri = request.cri(); const uint8_t cri_length = cri.length(); const uint8_t connection_type = static_cast(cri.type()); if (cri_length < 2 || kKnxNetIpHeaderSize + 2 * LEN_IPHPAI + cri_length > len) { ESP_LOGW(kTag, "invalid KNXnet/IP connect CRI from %s len=%u criLen=%u", EndpointString(remote).c_str(), static_cast(len), static_cast(cri_length)); sendConnectResponse(0, kKnxErrorConnectionType, control_remote, kKnxConnectionTypeTunnel, 0); return; } if (connection_type != kKnxConnectionTypeTunnel && connection_type != kKnxConnectionTypeDeviceManagement) { ESP_LOGW(kTag, "reject KNXnet/IP connect from %s unsupported type=0x%02x", EndpointString(remote).c_str(), static_cast(connection_type)); sendConnectResponse(0, kKnxErrorConnectionType, control_remote, connection_type, 0); return; } if (connection_type == kKnxConnectionTypeTunnel && (cri_length < 4 || cri.layer() != kKnxTunnelLayerLink)) { ESP_LOGW(kTag, "reject KNXnet/IP tunnel connect from %s unsupported layer=0x%02x", EndpointString(remote).c_str(), static_cast(cri_length >= 3 ? cri.layer() : 0)); sendConnectResponse(0, kKnxErrorTunnellingLayer, control_remote, connection_type, 0); return; } if (!SelectKnxNetifForRemote(control_remote).has_value()) { ESP_LOGW(kTag, "reject KNXnet/IP connect from %s: no active IPv4 interface for response", EndpointString(remote).c_str()); sendConnectResponse(0, kKnxErrorConnectionType, control_remote, connection_type, 0); return; } TunnelClient* client = allocateTunnelClient(control_remote, data_remote, connection_type); if (client == nullptr) { ESP_LOGW(kTag, "reject KNXnet/IP connect from %s: no free tunnel client slots", EndpointString(remote).c_str()); sendConnectResponse(0, kKnxErrorNoMoreConnections, control_remote, connection_type, 0); 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(client->channel_id), static_cast(connection_type), static_cast(client->individual_address), EndpointString(control_remote).c_str(), EndpointString(data_remote).c_str(), EndpointString(remote).c_str(), static_cast(std::count_if(tunnel_clients_.begin(), tunnel_clients_.end(), [](const TunnelClient& item) { return item.connected; })), static_cast(tunnel_clients_.size())); sendConnectResponse(client->channel_id, kKnxNoError, control_remote, connection_type, client->individual_address); } void GatewayKnxTpIpRouter::handleConnectionStateRequest(const uint8_t* packet_data, size_t len, const sockaddr_in& remote) { if (packet_data == nullptr || len < kKnxNetIpHeaderSize + 2 + LEN_IPHPAI) { return; } std::vector packet(packet_data, packet_data + len); KnxIpStateRequest request(packet.data(), static_cast(packet.size())); auto& control_hpai = request.hpaiCtrl(); if (OpenKnxHpaiUsesUnsupportedProtocol(control_hpai, currentTransportAllowsTcpHpai())) { ESP_LOGW(kTag, "reject KNXnet/IP connection-state request from %s: unsupported HPAI protocol", EndpointString(remote).c_str()); return; } const uint8_t channel_id = request.channelId(); const sockaddr_in control_remote = EndpointFromOpenKnxHpai(control_hpai, remote); TunnelClient* client = findTunnelClient(channel_id); const bool endpoint_matches = client != nullptr && ((client->tcp_sock >= 0 && active_tcp_sock_ == client->tcp_sock) || EndpointEquals(control_remote, client->control_remote)); const uint8_t status = endpoint_matches ? kKnxNoError : kKnxErrorConnectionId; if (client != nullptr) { if (endpoint_matches) { client->last_activity_tick = xTaskGetTickCount(); } } ESP_LOGI(kTag, "rx KNXnet/IP connection-state request channel=%u status=0x%02x from %s ctrl=%s expected=%s", static_cast(channel_id), static_cast(status), EndpointString(remote).c_str(), EndpointString(control_remote).c_str(), client == nullptr ? "none" : EndpointString(client->control_remote).c_str()); sendConnectionStateResponse( channel_id, status, control_remote); } void GatewayKnxTpIpRouter::handleDisconnectRequest(const uint8_t* packet_data, size_t len, const sockaddr_in& remote) { if (packet_data == nullptr || len < kKnxNetIpHeaderSize + 2 + LEN_IPHPAI) { return; } std::vector packet(packet_data, packet_data + len); KnxIpDisconnectRequest request(packet.data(), static_cast(packet.size())); auto& control_hpai = request.hpaiCtrl(); if (OpenKnxHpaiUsesUnsupportedProtocol(control_hpai, currentTransportAllowsTcpHpai())) { ESP_LOGW(kTag, "reject KNXnet/IP disconnect request from %s: unsupported HPAI protocol", EndpointString(remote).c_str()); return; } const uint8_t channel_id = request.channelId(); const sockaddr_in control_remote = EndpointFromOpenKnxHpai(control_hpai, remote); TunnelClient* client = findTunnelClient(channel_id); const bool endpoint_matches = client != nullptr && ((client->tcp_sock >= 0 && active_tcp_sock_ == client->tcp_sock) || EndpointEquals(control_remote, client->control_remote)); const uint8_t status = endpoint_matches ? kKnxNoError : kKnxErrorConnectionId; const std::string expected = client == nullptr ? "none" : EndpointString(client->control_remote); if (status == kKnxNoError) { resetTunnelClient(*client); } ESP_LOGI(kTag, "rx KNXnet/IP disconnect request channel=%u status=0x%02x from %s ctrl=%s expected=%s", static_cast(channel_id), static_cast(status), EndpointString(remote).c_str(), EndpointString(control_remote).c_str(), expected.c_str()); sendDisconnectResponse(channel_id, status, control_remote); } void GatewayKnxTpIpRouter::handleSecureService(uint16_t service, const uint8_t* body, size_t len, const sockaddr_in& remote) { #if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED) switch (service) { case kServiceSecureSessionRequest: case kServiceSecureSessionAuth: ESP_LOGW(kTag, "KNXnet/IP Secure service 0x%04x rejected: secure sessions are not provisioned", service); sendSecureSessionStatus(kKnxSecureStatusAuthFailed, remote); break; case kServiceSecureWrapper: ESP_LOGW(kTag, "KNXnet/IP Secure wrapper rejected: no authenticated secure session"); sendSecureSessionStatus(kKnxSecureStatusUnauthenticated, remote); break; case kServiceSecureGroupSync: ESP_LOGD(kTag, "KNXnet/IP Secure group sync ignored until secure routing is provisioned"); break; default: ESP_LOGD(kTag, "KNXnet/IP Secure service 0x%04x ignored", service); break; } #else (void)service; (void)body; (void)len; (void)remote; #endif } void GatewayKnxTpIpRouter::sendTunnellingAck(uint8_t channel_id, uint8_t sequence, uint8_t status, const sockaddr_in& remote) { sendConnectionHeaderAck(kServiceTunnellingAck, channel_id, sequence, status, remote); } void GatewayKnxTpIpRouter::sendDeviceConfigurationAck(uint8_t channel_id, uint8_t sequence, uint8_t status, const sockaddr_in& remote) { sendConnectionHeaderAck(kServiceDeviceConfigurationAck, channel_id, sequence, status, remote); } void GatewayKnxTpIpRouter::sendConnectionHeaderAck(uint16_t service, uint8_t channel_id, uint8_t sequence, uint8_t status, const sockaddr_in& remote) { KnxIpTunnelingAck ack; ack.serviceTypeIdentifier(service); ack.connectionHeader().length(LEN_CH); ack.connectionHeader().channelId(channel_id); ack.connectionHeader().sequenceCounter(sequence); ack.connectionHeader().status(status); const std::vector packet(ack.data(), ack.data() + ack.totalLength()); sendPacket(packet, remote); } void GatewayKnxTpIpRouter::sendSecureSessionStatus(uint8_t status, const sockaddr_in& remote) { const std::vector body{status, 0x00}; const auto packet = OpenKnxIpPacket(kServiceSecureSessionStatus, body); sendPacket(packet, remote); } bool GatewayKnxTpIpRouter::sendPacket(const std::vector& packet, const sockaddr_in& remote) const { if (packet.empty()) { return false; } if (active_tcp_sock_ >= 0) { return SendStream(active_tcp_sock_, packet.data(), packet.size()); } return udp_sock_ >= 0 && SendAll(udp_sock_, packet.data(), packet.size(), remote); } bool GatewayKnxTpIpRouter::sendPacketToTunnelClient( const TunnelClient& client, const std::vector& packet) const { if (packet.empty()) { return false; } if (client.tcp_sock >= 0) { return SendStream(client.tcp_sock, packet.data(), packet.size()); } return udp_sock_ >= 0 && SendAll(udp_sock_, packet.data(), packet.size(), client.data_remote); } bool GatewayKnxTpIpRouter::currentTransportAllowsTcpHpai() const { return active_tcp_sock_ >= 0; } std::optional> GatewayKnxTpIpRouter::localHpaiForRemote( const sockaddr_in& remote, bool tcp) const { std::array hpai{}; hpai[0] = 0x08; hpai[1] = tcp ? kKnxHpaiIpv4Tcp : kKnxHpaiIpv4Udp; if (tcp) { return hpai; } const auto netif = SelectKnxNetifForRemote(remote); if (!netif.has_value()) { return std::nullopt; } WriteIp(hpai.data() + 2, netif->address); WriteBe16(hpai.data() + 6, config_.udp_port); return hpai; } std::vector GatewayKnxTpIpRouter::buildOpenKnxSearchResponse( const sockaddr_in& remote) const { // Use OpenKNX's proven DIB construction via KnxIpSearchResponse. // Requires ets_device_ to be initialized (DeviceObject + Platform). if (ets_device_ == nullptr || knx_ip_parameters_ == nullptr) { ESP_LOGW(kTag, "OpenKNX search response unavailable; falling back to hand-rolled DIBs"); return {}; } KnxIpSearchResponse response(*knx_ip_parameters_, ets_device_->deviceObject()); return std::vector(response.data(), response.data() + response.totalLength()); } std::vector GatewayKnxTpIpRouter::buildOpenKnxDescriptionResponse( const sockaddr_in& remote) const { if (ets_device_ == nullptr || knx_ip_parameters_ == nullptr) { ESP_LOGW(kTag, "OpenKNX description response unavailable; falling back to hand-rolled DIBs"); return {}; } KnxIpDescriptionResponse response(*knx_ip_parameters_, ets_device_->deviceObject()); return std::vector(response.data(), response.data() + response.totalLength()); } std::vector GatewayKnxTpIpRouter::buildDeviceInfoDib( const sockaddr_in& remote) const { std::vector dib(54, 0); dib[0] = static_cast(dib.size()); dib[1] = kKnxDibDeviceInfo; dib[2] = advertisedMedium(); dib[3] = 0; WriteBe16(dib.data() + 4, effectiveIpInterfaceIndividualAddress()); WriteBe16(dib.data() + 6, 0); uint8_t mac[6]{}; if (ReadBaseMac(mac)) { dib[8] = static_cast((knx_internal::kReg1DaliManufacturerId >> 8) & 0xff); dib[9] = static_cast(knx_internal::kReg1DaliManufacturerId & 0xff); std::memcpy(dib.data() + 10, mac + 2, 4); 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(config_.main_group), openknx_namespace_.c_str()); std::memcpy(dib.data() + 24, friendly, std::min(30, std::strlen(friendly))); (void)remote; return dib; } std::vector GatewayKnxTpIpRouter::buildExtendedDeviceInfoDib() const { std::vector dib(8, 0); dib[0] = static_cast(dib.size()); dib[1] = kKnxDibExtendedDeviceInfo; dib[2] = 0x01; dib[3] = 0x00; WriteBe16(dib.data() + 4, 254); WriteBe16(dib.data() + 6, advertisedMedium() == kKnxMediumIp ? kKnxIpOnlyDeviceDescriptor : kKnxTpIpInterfaceDeviceDescriptor); return dib; } std::vector GatewayKnxTpIpRouter::buildIpConfigDib(const sockaddr_in& remote, bool current) const { const auto netif = SelectKnxNetifForRemote(remote); const uint32_t address = netif.has_value() ? netif->address : htonl(INADDR_ANY); const uint32_t netmask = netif.has_value() ? netif->netmask : htonl(INADDR_ANY); const uint32_t gateway = netif.has_value() ? netif->gateway : htonl(INADDR_ANY); std::vector dib(current ? 20 : 16, 0); dib[0] = static_cast(dib.size()); dib[1] = current ? kKnxDibCurrentIpConfig : kKnxDibIpConfig; WriteIp(dib.data() + 2, address); WriteIp(dib.data() + 6, netmask); WriteIp(dib.data() + 10, gateway); if (current) { WriteIp(dib.data() + 14, htonl(INADDR_ANY)); dib[18] = kKnxIpAssignmentManual; dib[19] = 0x00; } else { dib[14] = kKnxIpCapabilityManual; dib[15] = kKnxIpAssignmentManual; } return dib; } std::vector GatewayKnxTpIpRouter::buildKnxAddressesDib() const { std::vector dib(4 + kMaxTunnelClients * 2U, 0); dib[0] = static_cast(dib.size()); dib[1] = kKnxDibKnxAddresses; WriteBe16(dib.data() + 2, effectiveIpInterfaceIndividualAddress()); size_t offset = 4; for (size_t slot = 0; slot < kMaxTunnelClients; ++slot) { WriteBe16(dib.data() + offset, effectiveTunnelAddressForSlot(slot)); offset += 2; } return dib; } std::vector GatewayKnxTpIpRouter::buildTunnelingInfoDib() const { std::vector dib(4 + kMaxTunnelClients * 4U, 0); dib[0] = static_cast(dib.size()); dib[1] = kKnxDibTunnellingInfo; WriteBe16(dib.data() + 2, 254); size_t offset = 4; for (size_t slot = 0; slot < kMaxTunnelClients; ++slot) { const uint16_t address = effectiveTunnelAddressForSlot(slot); bool used = false; for (const auto& client : tunnel_clients_) { if (client.connected && client.individual_address == address) { used = true; break; } } uint16_t flags = 0xffff; if (used) { flags = static_cast(flags & ~0x0001U); flags = static_cast(flags & ~0x0004U); } WriteBe16(dib.data() + offset, address); WriteBe16(dib.data() + offset + 2, flags); offset += 4; } return dib; } std::vector GatewayKnxTpIpRouter::buildSupportedServiceDib() const { std::vector> services{ {kKnxServiceFamilyCore, 2}, {kKnxServiceFamilyDeviceManagement, 1}, }; if (config_.tunnel_enabled) { services.emplace_back(kKnxServiceFamilyTunnelling, 1); } if (config_.multicast_enabled) { services.emplace_back(kKnxServiceFamilyRouting, 1); } std::vector dib(2 + services.size() * 2U, 0); dib[0] = static_cast(dib.size()); dib[1] = kKnxDibSupportedServices; size_t offset = 2; for (const auto& service : services) { dib[offset++] = service.first; dib[offset++] = service.second; } return dib; } void GatewayKnxTpIpRouter::sendTunnelIndication(const uint8_t* data, size_t len) { if (data == nullptr || len == 0) { return; } for (auto& client : tunnel_clients_) { if (client.connected) { sendTunnelIndicationToClient(client, data, len); } } } void GatewayKnxTpIpRouter::sendTunnelIndicationToClient(TunnelClient& client, const uint8_t* data, size_t len) { sendCemiFrameToClient(client, kServiceTunnellingRequest, data, len); } bool GatewayKnxTpIpRouter::sendCemiFrameToClient(TunnelClient& client, uint16_t service, const uint8_t* data, size_t len) { if (!client.connected || data == nullptr || len == 0) { return false; } std::vector frame_data(data, data + len); CemiFrame frame(frame_data.data(), static_cast(frame_data.size())); if (!frame.valid()) { ESP_LOGW(kTag, "not sending invalid OpenKNX cEMI service=0x%04x len=%u to %s", static_cast(service), static_cast(len), EndpointString(client.data_remote).c_str()); return false; } KnxIpTunnelingRequest request(frame); request.serviceTypeIdentifier(service); request.connectionHeader().length(LEN_CH); request.connectionHeader().channelId(client.channel_id); const auto message_code = CemiMessageCode(data, len); const uint8_t send_sequence = client.send_sequence++; request.connectionHeader().sequenceCounter(send_sequence); request.connectionHeader().status(kKnxNoError); const std::vector packet(request.data(), request.data() + request.totalLength()); if (!sendPacketToTunnelClient(client, packet)) { ESP_LOGW(kTag, "failed to send KNXnet/IP cEMI service=0x%04x channel=%u seq=%u to %s", static_cast(service), static_cast(client.channel_id), static_cast(request.connectionHeader().sequenceCounter()), EndpointString(client.data_remote).c_str()); return false; } if (service == kServiceTunnellingRequest && message_code.has_value() && message_code.value() == L_data_con) { client.last_tunnel_confirmation_sequence = send_sequence; client.last_tunnel_confirmation_packet = packet; } ESP_LOGI(kTag, "sent KNXnet/IP cEMI service=0x%04x channel=%u seq=%u cemi=0x%02x len=%u to %s", static_cast(service), static_cast(client.channel_id), static_cast(request.connectionHeader().sequenceCounter()), static_cast(data[0]), static_cast(len), EndpointString(client.data_remote).c_str()); return true; } void GatewayKnxTpIpRouter::sendConnectionStateResponse(uint8_t channel_id, uint8_t status, const sockaddr_in& remote) { KnxIpStateResponse response(channel_id, status); const std::vector packet(response.data(), response.data() + response.totalLength()); sendPacket(packet, remote); } void GatewayKnxTpIpRouter::sendDisconnectResponse(uint8_t channel_id, uint8_t status, const sockaddr_in& remote) { KnxIpDisconnectResponse response(channel_id, status); const std::vector packet(response.data(), response.data() + response.totalLength()); sendPacket(packet, remote); } void GatewayKnxTpIpRouter::sendConnectResponse(uint8_t channel_id, uint8_t status, const sockaddr_in& remote, uint8_t connection_type, uint16_t tunnel_address) { if (status != kKnxNoError) { KnxIpConnectResponse response(channel_id, status); const std::vector packet(response.data(), response.data() + response.totalLength()); sendPacket(packet, remote); ESP_LOGI(kTag, "sent KNXnet/IP connect error channel=%u status=0x%02x to %s", static_cast(channel_id), static_cast(status), EndpointString(remote).c_str()); return; } const auto netif = SelectKnxNetifForRemote(remote); if (!netif.has_value() || knx_ip_parameters_ == nullptr) { ESP_LOGW(kTag, "cannot accept KNXnet/IP connect from %s: no active IPv4 interface", EndpointString(remote).c_str()); KnxIpConnectResponse response(channel_id, kKnxErrorConnectionType); const std::vector packet(response.data(), response.data() + response.totalLength()); sendPacket(packet, remote); return; } KnxIpConnectResponse response(*knx_ip_parameters_, tunnel_address, config_.udp_port, channel_id, connection_type); const bool tcp = currentTransportAllowsTcpHpai(); const uint32_t endpoint_address = ntohl(netif->address); response.controlEndpoint().code(tcp ? IPV4_TCP : IPV4_UDP); response.controlEndpoint().ipAddress(tcp ? 0 : endpoint_address); response.controlEndpoint().ipPortNumber(tcp ? 0 : config_.udp_port); const std::vector packet(response.data(), response.data() + response.totalLength()); sendPacket(packet, remote); std::string endpoint_string; if (tcp) { endpoint_string = "0.0.0.0:0 (TCP HPAI)"; } else { sockaddr_in local_endpoint{}; local_endpoint.sin_family = AF_INET; local_endpoint.sin_port = htons(config_.udp_port); local_endpoint.sin_addr.s_addr = netif->address; endpoint_string = EndpointString(local_endpoint); } ESP_LOGI(kTag, "sent KNXnet/IP connect response channel=%u type=0x%02x to %s endpoint=%s", static_cast(channel_id), static_cast(connection_type), EndpointString(remote).c_str(), endpoint_string.c_str()); } void GatewayKnxTpIpRouter::sendRoutingIndication(const uint8_t* data, size_t len) { if (!config_.multicast_enabled || udp_sock_ < 0 || data == nullptr || len == 0) { return; } sockaddr_in remote{}; remote.sin_family = AF_INET; remote.sin_port = htons(config_.udp_port); remote.sin_addr.s_addr = inet_addr(config_.multicast_address.c_str()); std::vector frame_data(data, data + len); CemiFrame frame(frame_data.data(), static_cast(frame_data.size())); if (!frame.valid()) { ESP_LOGW(kTag, "not sending invalid OpenKNX routing cEMI len=%u", static_cast(len)); return; } KnxIpRoutingIndication routing(frame); const std::vector packet(routing.data(), routing.data() + routing.totalLength()); const auto netifs = ActiveKnxNetifs(); if (netifs.empty()) { SendAll(udp_sock_, packet.data(), packet.size(), remote); return; } for (const auto& netif : netifs) { in_addr multicast_interface{}; multicast_interface.s_addr = netif.address; if (setsockopt(udp_sock_, IPPROTO_IP, IP_MULTICAST_IF, &multicast_interface, sizeof(multicast_interface)) < 0) { ESP_LOGW(kTag, "failed to select KNX multicast interface %s %s: errno=%d (%s)", netif.key, Ipv4String(netif.address).c_str(), errno, std::strerror(errno)); continue; } SendAll(udp_sock_, packet.data(), packet.size(), remote); } } 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); } } 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) { 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; } 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; } 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::transmitOpenKnxTpFrame(const uint8_t* data, size_t len) { SemaphoreGuard guard(openknx_lock_); if (ets_device_ == nullptr) { return false; } const bool sent = ets_device_->transmitTpFrame(data, len); tp_uart_online_ = ets_device_->tpUartOnline(); return sent; } bool GatewayKnxTpIpRouter::handleOpenKnxBusFrame(const uint8_t* data, size_t len) { SemaphoreGuard guard(openknx_lock_); if (ets_device_ == nullptr) { return false; } const bool consumed = ets_device_->handleBusFrame(data, len); syncOpenKnxConfigFromDevice(); return consumed; } 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::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; } 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 (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_.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 { 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