#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 "openknx_idf/ets_device_runtime.h" #include "soc/uart_periph.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace gateway { namespace { constexpr const char* kTag = "gateway_knx"; constexpr uint8_t kCemiLDataReq = 0x11; constexpr uint8_t kCemiLDataInd = 0x29; constexpr uint8_t kCemiLDataCon = 0x2e; 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 kKnxManufacturerId = 0x00a4; constexpr uint16_t kKnxIpOnlyDeviceDescriptor = 0x57b0; constexpr uint16_t kKnxTpIpInterfaceDeviceDescriptor = 0x091a; constexpr uint8_t kKnxIpAssignmentManual = 0x01; constexpr uint8_t kKnxIpCapabilityManual = 0x01; constexpr uint8_t kTpUartResetRequest = 0x01; constexpr uint8_t kTpUartResetIndication = 0x03; constexpr uint8_t kTpUartStateRequest = 0x02; constexpr uint8_t kTpUartStateIndicationMask = 0x07; constexpr uint8_t kTpUartSetAddressRequest = 0x28; constexpr uint8_t kTpUartAckInfo = 0x10; constexpr uint8_t kTpUartLDataConfirmPositive = 0x8b; constexpr uint8_t kTpUartLDataConfirmNegative = 0x0b; constexpr uint8_t kTpUartLDataStart = 0x80; constexpr uint8_t kTpUartLDataEnd = 0x40; constexpr uint8_t kTpUartBusy = 0xc0; 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 uint8_t kGwReg1KoSwitch = 0; constexpr uint8_t kGwReg1KoDimmAbsolute = 3; constexpr uint8_t kGwReg1KoColor = 6; constexpr uint8_t kGwReg1KoSwitchState = 1; constexpr uint8_t kGwReg1KoDimmState = 4; 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 EndpointFromHpaiAt(const uint8_t* body, size_t len, size_t offset, const sockaddr_in& fallback) { sockaddr_in out = fallback; if (body == nullptr || offset + 8 > len || body[offset] != 0x08 || (body[offset + 1] != kKnxHpaiIpv4Udp && body[offset + 1] != kKnxHpaiIpv4Tcp)) { return out; } uint32_t address = 0; std::memcpy(&address, body + offset + 2, sizeof(address)); const uint16_t port = ReadBe16(body + offset + 6); if (address != 0) { out.sin_addr.s_addr = address; } if (port != 0) { out.sin_port = htons(port); } return out; } sockaddr_in ResponseEndpointFromHpai(const uint8_t* body, size_t len, const sockaddr_in& fallback) { return EndpointFromHpaiAt(body, len, 0, fallback); } bool HasUnsupportedHpaiProtocolAt(const uint8_t* body, size_t len, size_t offset, bool allow_tcp) { if (body == nullptr || offset + 2 > len || body[offset] != 0x08) { return false; } const uint8_t protocol = body[offset + 1]; return protocol != kKnxHpaiIpv4Udp && !(allow_tcp && protocol == kKnxHpaiIpv4Tcp); } void WriteBe16(uint8_t* data, uint16_t value) { data[0] = static_cast((value >> 8) & 0xff); data[1] = static_cast(value & 0xff); } uint16_t TunnelServiceForCemi(const uint8_t* data, size_t len) { if (data == nullptr || len == 0) { return kServiceTunnellingRequest; } return (data[0] == kCemiLDataReq || data[0] == kCemiLDataCon || data[0] == kCemiLDataInd) ? kServiceTunnellingRequest : kServiceDeviceConfigurationRequest; } std::vector CemiWithTunnelSourceAddress(const uint8_t* data, size_t len, uint16_t source_address) { std::vector frame(data, data + len); if (len < 8 || frame[0] != kCemiLDataReq) { return frame; } const size_t additional_info_len = frame[1]; const size_t source_offset = 2 + additional_info_len + 2; if (source_offset + 1 >= frame.size()) { return frame; } if (ReadBe16(frame.data() + source_offset) == 0) { WriteBe16(frame.data() + source_offset, source_address); } return frame; } 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::kColorTemperature: return "Color Temperature"; case GatewayKnxDaliDataType::kRgb: return "RGB"; 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::kColorTemperature: return "DPST-7-600"; case GatewayKnxDaliDataType::kRgb: return "DPST-232-600"; case GatewayKnxDaliDataType::kUnknown: default: return ""; } } std::optional DecodeCemiGroupWrite(const uint8_t* data, size_t len) { if (data == nullptr || len < 10) { return std::nullopt; } const uint8_t message_code = data[0]; if (message_code != kCemiLDataReq && message_code != kCemiLDataInd && message_code != kCemiLDataCon) { return std::nullopt; } const size_t base = 2U + data[1]; if (len < base + 8U) { return std::nullopt; } const uint8_t control2 = data[base + 1]; if ((control2 & 0x80) == 0) { return std::nullopt; } const uint16_t destination = ReadBe16(data + base + 4); const size_t tpdu_len = static_cast(data[base + 6]) + 1U; if (tpdu_len < 2U || len < base + 7U + tpdu_len) { return std::nullopt; } const uint8_t* tpdu = data + base + 7; const uint16_t apci = static_cast(((tpdu[0] & 0x03) << 8) | (tpdu[1] & 0xc0)); if (apci != 0x80) { return std::nullopt; } DecodedGroupWrite out; out.group_address = destination; if (tpdu_len == 2U) { out.data.push_back(tpdu[1] & 0x3f); } else { out.data.assign(tpdu + 2, tpdu + tpdu_len); } return out; } bool IsCemiGroupFrame(const uint8_t* data, size_t len) { if (data == nullptr || len < 10) { return false; } const uint8_t message_code = data[0]; if (message_code != kCemiLDataReq && message_code != kCemiLDataInd && message_code != kCemiLDataCon) { return false; } const size_t base = 2U + data[1]; if (len < base + 8U) { return false; } return (data[base + 1] & 0x80) != 0; } 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 MetadataInt(const DaliBridgeResult& result, const std::string& key) { return getObjectInt(result.metadata, key); } 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; } 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 KnxNetIpPacket(uint16_t service, const std::vector& body) { std::vector packet(6 + body.size()); packet[0] = kKnxNetIpHeaderSize; packet[1] = kKnxNetIpVersion10; WriteBe16(packet.data() + 2, service); WriteBe16(packet.data() + 4, static_cast(packet.size())); if (!body.empty()) { std::memcpy(packet.data() + 6, body.data(), body.size()); } return packet; } 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; } } bool IsExtendedTpFrame(const uint8_t* data, size_t len) { return len > 0 && (data[0] & 0xD3) == 0x10; } size_t ExpectedTpFrameSize(const uint8_t* data, size_t len) { if (data == nullptr || len < 6) { return 0; } if (IsExtendedTpFrame(data, len)) { return 9U + data[6]; } return 8U + (data[5] & 0x0F); } bool ValidateTpChecksum(const uint8_t* data, size_t len) { if (data == nullptr || len < 2) { return false; } uint8_t crc = 0xFF; for (size_t index = 0; index + 1 < len; ++index) { crc ^= data[index]; } return data[len - 1] == crc; } bool IsTpUartControlByte(uint8_t byte) { return byte == kTpUartResetIndication || byte == kTpUartLDataConfirmPositive || byte == kTpUartLDataConfirmNegative || byte == kTpUartBusy || (byte & kTpUartStateIndicationMask) == kTpUartStateIndicationMask; } bool IsTpUartFrameStart(uint8_t byte, bool* extended) { if (extended == nullptr) { return false; } *extended = (byte & 0x80) == 0; return (byte & 0x50) == 0x10; } std::vector WrapTpUartTelegram(const std::vector& telegram) { std::vector wrapped; wrapped.reserve(telegram.size() * 2U); for (size_t index = 0; index < telegram.size(); ++index) { const uint8_t control = static_cast( (index + 1U == telegram.size() ? kTpUartLDataEnd : kTpUartLDataStart) | (index & 0x3fU)); wrapped.push_back(control); wrapped.push_back(telegram[index]); } return wrapped; } bool TpTelegramEqualsIgnoringRepeatBit(const std::vector& left, const std::vector& right) { if (left.size() != right.size() || left.empty()) { return false; } if ((left[0] & static_cast(~0x20U)) != (right[0] & static_cast(~0x20U))) { return false; } return std::equal(left.begin() + 1, left.end(), right.begin() + 1); } std::optional> CemiToTpTelegram(const uint8_t* data, size_t len) { if (data == nullptr || len < 10 || data[1] != 0) { return std::nullopt; } const uint8_t* ctrl = data + 2; const bool standard = (ctrl[0] & 0x80) != 0; const size_t tp_len = standard ? len - 2U : len - 1U; if (tp_len < 8) { return std::nullopt; } std::vector telegram(tp_len, 0); if (standard) { telegram[0] = ctrl[0]; std::memcpy(telegram.data() + 1, ctrl + 2, 4); telegram[5] = static_cast((ctrl[1] & 0xF0) | (ctrl[6] & 0x0F)); if (tp_len > 7U) { std::memcpy(telegram.data() + 6, ctrl + 7, tp_len - 7U); } } else { std::memcpy(telegram.data(), ctrl, tp_len - 1U); } uint8_t crc = 0xFF; for (size_t index = 0; index + 1 < telegram.size(); ++index) { crc ^= telegram[index]; } telegram.back() = crc; return telegram; } std::optional> TpTelegramToCemi(const uint8_t* data, size_t len) { if (data == nullptr || len < 8 || !ValidateTpChecksum(data, len)) { return std::nullopt; } const bool extended = IsExtendedTpFrame(data, len); const size_t cemi_len = len + (extended ? 2U : 3U) - 1U; std::vector cemi(cemi_len, 0); cemi[0] = kCemiLDataInd; cemi[1] = 0x00; cemi[2] = data[0]; if (extended) { std::memcpy(cemi.data() + 2, data, len - 1U); } else { cemi[3] = data[5] & 0xF0; std::memcpy(cemi.data() + 4, data + 1, 4); cemi[8] = data[5] & 0x0F; const size_t copy_len = static_cast(cemi[8]) + 1U; if (9U + copy_len > cemi.size() || 6U + copy_len > len) { return std::nullopt; } std::memcpy(cemi.data() + 9, data + 6, copy_len); } return cemi; } } // 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.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.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["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["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::kColorTemperature: return "color_temperature"; case GatewayKnxDaliDataType::kRgb: return "rgb"; 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}); } 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 == 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 == 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(); } 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 * 3) + (16 * 3)); 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()); } } for (int address = 0; address < 64; ++address) { const uint16_t base = static_cast(kGwReg1AdrKoOffset + (address * kGwReg1AdrKoBlockSize)); for (const uint8_t slot : {kGwReg1KoSwitch, 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, 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::matchesCemiFrame(const uint8_t* data, size_t len) const { const auto decoded = DecodeCemiGroupWrite(data, len); return decoded.has_value() && matchesGroupAddress(decoded->group_address); } 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::handleCemiFrame(const uint8_t* data, size_t len) { const auto decoded = DecodeCemiGroupWrite(data, len); if (!decoded.has_value()) { return ErrorResult(0, "unsupported or non group-write cEMI frame"); } return handleGroupWrite(decoded->group_address, decoded->data.data(), decoded->data.size()); } 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); } 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 delete_all = data[3] == 1; const bool assign = data[4] == 1; if (assign || delete_all) { DaliBridgeRequest allocate = FunctionRequest( "knx-function-scan-allocate", delete_all ? BridgeOperation::resetAndAllocateShortAddresses : BridgeOperation::allocateAllShortAddresses); allocate.value = DaliValue::Object{{"start", 0}, {"removeAddrFirst", delete_all}}; engine_.execute(allocate); } DaliBridgeRequest search = FunctionRequest("knx-function-scan-search", BridgeOperation::searchAddressRange); search.value = DaliValue::Object{{"start", 0}, {"end", 63}}; const auto search_result = engine_.execute(search); if (search_result.ok) { if (const auto* addresses_value = getObjectValue(search_result.metadata, "addresses")) { if (const auto* addresses = addresses_value->asArray()) { for (const auto& address_value : *addresses) { const auto short_address = address_value.asInt(); if (!short_address.has_value() || short_address.value() < 0 || short_address.value() > 63) { continue; } GatewayKnxCommissioningBallast ballast; ballast.short_address = static_cast(short_address.value()); ballast.high = static_cast( QueryShort(engine_, ballast.short_address, DALI_CMD_QUERY_RANDOM_ADDRESS_H, "knx-function-scan-rand-h") .value_or(0)); ballast.middle = static_cast( QueryShort(engine_, ballast.short_address, DALI_CMD_QUERY_RANDOM_ADDRESS_M, "knx-function-scan-rand-m") .value_or(0)); ballast.low = static_cast( QueryShort(engine_, ballast.short_address, DALI_CMD_QUERY_RANDOM_ADDRESS_L, "knx-function-scan-rand-l") .value_or(0)); commissioning_found_ballasts_.push_back(ballast); } } } } commissioning_scan_done_ = true; 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; } 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) { 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::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::kUnknown: default: return ErrorResult(group_address, "unsupported KNX data type"); } } GatewayKnxTpIpRouter::GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHandler handler, std::string openknx_namespace) : bridge_(bridge), handler_(std::move(handler)), 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); } 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 { 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_); ets_device_ = std::make_unique(openknx_namespace_, config_.individual_address, effectiveTunnelAddress()); 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()); } }); 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]() { pollTpUart(); { SemaphoreGuard guard(openknx_lock_); if (ets_device_ != nullptr) { pollProgrammingButton(); ets_device_->loop(); 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); 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; if (tp_uart_port_ >= 0) { uart_driver_delete(static_cast(tp_uart_port_)); tp_uart_port_ = -1; } } 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)); } if (bind(tcp_sock_, reinterpret_cast(&bind_addr), sizeof(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)); } } bool GatewayKnxTpIpRouter::configureTpUart() { if (!GatewayKnxConfigUsesTpUart(config_)) { tp_uart_port_ = -1; tp_uart_online_ = false; ESP_LOGI(kTag, "KNX TP-UART disabled by UART port; KNXnet/IP uses IP-only runtime"); return true; } 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 false; } uart_config_t uart_config{}; uart_config.baud_rate = static_cast(serial.baudrate); uart_config.data_bits = UART_DATA_8_BITS; uart_config.parity = serial.nine_bit_mode ? UART_PARITY_EVEN : UART_PARITY_DISABLE; uart_config.stop_bits = UART_STOP_BITS_1; uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; uart_config.source_clk = UART_SCLK_DEFAULT; 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 false; } esp_err_t err = uart_param_config(uart_port, &uart_config); if (err != ESP_OK) { last_error_ = EspErrDetail("failed to configure KNX TP-UART parameters on UART" + std::to_string(serial.uart_port), err); ESP_LOGE(kTag, "%s", last_error_.c_str()); return false; } err = uart_set_pin(uart_port, tx_pin, rx_pin, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); if (err != ESP_OK) { last_error_ = EspErrDetail("failed to configure KNX TP-UART pins uart=" + std::to_string(serial.uart_port) + " tx=" + UartPinDescription(serial.tx_pin, tx_pin) + " rx=" + UartPinDescription(serial.rx_pin, rx_pin), err); ESP_LOGE(kTag, "%s", last_error_.c_str()); return false; } err = uart_driver_install(uart_port, serial.rx_buffer_size, serial.tx_buffer_size, 0, nullptr, 0); if (err != ESP_OK) { last_error_ = EspErrDetail("failed to install KNX TP-UART driver on UART" + std::to_string(serial.uart_port), err); ESP_LOGE(kTag, "%s", last_error_.c_str()); return false; } tp_uart_port_ = serial.uart_port; tp_uart_tx_pin_ = tx_pin; tp_uart_rx_pin_ = rx_pin; if (!initializeTpUart()) { if (ets_device_ != nullptr && !ets_device_->configured()) { ESP_LOGW(kTag, "%s; continuing KNXnet/IP in commissioning-only IP mode so ETS can program the " "device", last_error_.c_str()); uart_driver_delete(uart_port); tp_uart_port_ = -1; tp_uart_online_ = false; return true; } ESP_LOGE(kTag, "%s", last_error_.c_str()); return false; } ESP_LOGI(kTag, "KNX TP-UART online uart=%d tx=%s rx=%s baud=%u nineBit=%d", serial.uart_port, UartPinDescription(serial.tx_pin, tp_uart_tx_pin_).c_str(), UartPinDescription(serial.rx_pin, tp_uart_rx_pin_).c_str(), static_cast(serial.baudrate), serial.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; } bool GatewayKnxTpIpRouter::initializeTpUart() { if (tp_uart_port_ < 0) { return false; } const uart_port_t uart_port = static_cast(tp_uart_port_); tp_rx_frame_.clear(); tp_last_sent_telegram_.clear(); tp_uart_last_byte_tick_ = 0; tp_uart_extended_frame_ = false; tp_uart_online_ = false; uart_flush_input(uart_port); const uint8_t reset_request = kTpUartResetRequest; if (uart_write_bytes(uart_port, &reset_request, 1) != 1) { last_error_ = "failed to send KNX TP-UART reset request uart=" + std::to_string(tp_uart_port_); return false; } const TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(1500); bool saw_reset = false; std::array buffer{}; while (xTaskGetTickCount() < deadline) { const int read = uart_read_bytes(uart_port, buffer.data(), buffer.size(), pdMS_TO_TICKS(config_.tp_uart.read_timeout_ms)); if (read <= 0) { continue; } for (int index = 0; index < read; ++index) { const uint8_t byte = buffer[static_cast(index)]; if (!saw_reset) { if (byte == kTpUartResetIndication) { saw_reset = true; const std::array set_address{ kTpUartSetAddressRequest, static_cast((effectiveIpInterfaceIndividualAddress() >> 8) & 0xff), static_cast(effectiveIpInterfaceIndividualAddress() & 0xff), }; uart_write_bytes(uart_port, set_address.data(), set_address.size()); const uint8_t state_request = kTpUartStateRequest; uart_write_bytes(uart_port, &state_request, 1); } continue; } if ((byte & kTpUartStateIndicationMask) == kTpUartStateIndicationMask) { tp_uart_online_ = true; return true; } } } last_error_ = (saw_reset ? "timed out waiting for KNX TP-UART state indication" : "timed out waiting for KNX TP-UART reset indication") + std::string(" 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_) + " timeoutMs=1500"; return false; } 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, body, body_len, remote); break; case kServiceDescriptionRequest: handleDescriptionRequest(body, body_len, remote); break; case kServiceDeviceConfigurationRequest: handleDeviceConfigurationRequest(body, body_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()); } break; case kServiceRoutingIndication: if (config_.multicast_enabled) { handleRoutingIndication(body, body_len); } break; case kServiceTunnellingRequest: if (config_.tunnel_enabled) { handleTunnellingRequest(body, body_len, remote); } break; case kServiceConnectRequest: if (config_.tunnel_enabled) { handleConnectRequest(body, body_len, remote); } break; case kServiceConnectionStateRequest: handleConnectionStateRequest(body, body_len, remote); break; case kServiceDisconnectRequest: handleDisconnectRequest(body, body_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* body, size_t len, const sockaddr_in& remote) { if (HasUnsupportedHpaiProtocolAt(body, len, 0, currentTransportAllowsTcpHpai())) { ESP_LOGW(kTag, "ignore KNXnet/IP search request from %s: unsupported HPAI protocol", EndpointString(remote).c_str()); return; } sockaddr_in response_remote = ResponseEndpointFromHpai(body, len, remote); selectOpenKnxNetworkInterface(response_remote); std::set requested_dibs; if (service == kServiceSearchRequestExt && body != nullptr && len > 8) { size_t offset = 8; while (offset + 2 <= len) { const uint8_t srp_len = body[offset]; if (srp_len < 2 || offset + srp_len > len) { break; } const uint8_t srp_type = body[offset + 1]; if (srp_type == 0x01) { // The programming button belongs to the logical KNX-DALI device behind the tunnel. return; } else if (srp_type == 0x02 && srp_len >= 8) { uint8_t mac[6]{}; if (!ReadBaseMac(mac) || std::memcmp(mac, body + offset + 2, 6) != 0) { return; } } else if (srp_type == 0x03) { for (size_t service_offset = offset + 2; service_offset + 1 < offset + srp_len; service_offset += 2) { const uint8_t family = body[service_offset]; const uint8_t version = body[service_offset + 1]; if ((family == kKnxServiceFamilyCore && version > 2) || (family == kKnxServiceFamilyDeviceManagement && version > 1) || (family == kKnxServiceFamilyTunnelling && (!config_.tunnel_enabled || version > 1)) || (family == kKnxServiceFamilyRouting && (!config_.multicast_enabled || version > 1))) { return; } } } else if (srp_type == 0x04) { for (size_t dib_offset = offset + 2; dib_offset < offset + srp_len; ++dib_offset) { requested_dibs.insert(body[dib_offset]); } } offset += srp_len; } } sendSearchResponse(service == kServiceSearchRequestExt ? kServiceSearchResponseExt : kServiceSearchResponse, response_remote, requested_dibs); } void GatewayKnxTpIpRouter::handleDescriptionRequest(const uint8_t* body, size_t len, const sockaddr_in& remote) { if (HasUnsupportedHpaiProtocolAt(body, len, 0, currentTransportAllowsTcpHpai())) { ESP_LOGW(kTag, "ignore KNXnet/IP description request from %s: unsupported HPAI protocol", EndpointString(remote).c_str()); return; } const sockaddr_in response_remote = ResponseEndpointFromHpai(body, len, remote); selectOpenKnxNetworkInterface(response_remote); sendDescriptionResponse(response_remote); } void GatewayKnxTpIpRouter::handleRoutingIndication(const uint8_t* body, size_t len) { if (body == nullptr || len == 0) { return; } const bool consumed_by_openknx = handleOpenKnxBusFrame(body, len); if (!consumed_by_openknx && shouldRouteDaliApplicationFrames()) { const DaliBridgeResult result = handler_(body, len); if (!result.ok && !result.error.empty()) { ESP_LOGD(kTag, "KNX routing indication ignored: %s", result.error.c_str()); } } forwardCemiToTp(body, len); } 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* body, size_t len, const sockaddr_in& remote) { if (body == nullptr || len < 5 || body[0] != 0x04) { ESP_LOGW(kTag, "invalid KNXnet/IP tunnelling request from %s len=%u", EndpointString(remote).c_str(), static_cast(len)); return; } const uint8_t channel_id = body[1]; const uint8_t sequence = body[2]; 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; } if (sequence == client->received_sequence) { 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); return; } 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_activity_tick = xTaskGetTickCount(); sendTunnellingAck(channel_id, sequence, kKnxNoError, client->data_remote); const auto cemi_frame = CemiWithTunnelSourceAddress(body + 4, len - 4, client->individual_address); const uint8_t* cemi = cemi_frame.data(); const size_t cemi_len = cemi_frame.size(); 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 group_frame = IsCemiGroupFrame(cemi, cemi_len); const bool consumed_by_openknx = handleOpenKnxTunnelFrame(cemi, cemi_len, client); if (consumed_by_openknx) { if (group_frame) { forwardCemiToTp(cemi, cemi_len); } return; } if (shouldRouteDaliApplicationFrames()) { const DaliBridgeResult result = handler_(cemi, cemi_len); if (!result.ok && !result.error.empty()) { ESP_LOGD(kTag, "KNX tunnel frame not routed to DALI: %s", result.error.c_str()); } } forwardCemiToTp(cemi, cemi_len); } void GatewayKnxTpIpRouter::handleDeviceConfigurationRequest(const uint8_t* body, size_t len, const sockaddr_in& remote) { if (body == nullptr || len < 5 || body[0] != 0x04) { ESP_LOGW(kTag, "invalid KNXnet/IP device-configuration request from %s len=%u", EndpointString(remote).c_str(), static_cast(len)); return; } const uint8_t channel_id = body[1]; const uint8_t sequence = body[2]; 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); const uint8_t* cemi = body + 4; const size_t cemi_len = len - 4; 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)) { 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* body, size_t len, const sockaddr_in& remote) { if (body == nullptr || len < 18) { ESP_LOGW(kTag, "invalid KNXnet/IP connect request from %s len=%u", EndpointString(remote).c_str(), static_cast(len)); return; } if (HasUnsupportedHpaiProtocolAt(body, len, 0, currentTransportAllowsTcpHpai()) || HasUnsupportedHpaiProtocolAt(body, len, 8, 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 = EndpointFromHpaiAt(body, len, 0, remote); sockaddr_in data_remote = EndpointFromHpaiAt(body, len, 8, remote); selectOpenKnxNetworkInterface(control_remote); const size_t cri_offset = 16; const uint8_t cri_length = body[cri_offset]; const uint8_t connection_type = body[cri_offset + 1]; if (cri_length < 2 || cri_offset + 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 || body[cri_offset + 2] != kKnxTunnelLayerLink)) { ESP_LOGW(kTag, "reject KNXnet/IP tunnel connect from %s unsupported layer=0x%02x", EndpointString(remote).c_str(), static_cast(cri_length >= 3 ? body[cri_offset + 2] : 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* body, size_t len, const sockaddr_in& remote) { if (body == nullptr || len < 2) { return; } if (HasUnsupportedHpaiProtocolAt(body, len, 2, 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 = body[0]; const sockaddr_in control_remote = EndpointFromHpaiAt(body, len, 2, 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* body, size_t len, const sockaddr_in& remote) { if (body == nullptr || len < 2) { return; } if (HasUnsupportedHpaiProtocolAt(body, len, 2, currentTransportAllowsTcpHpai())) { ESP_LOGW(kTag, "reject KNXnet/IP disconnect request from %s: unsupported HPAI protocol", EndpointString(remote).c_str()); return; } const uint8_t channel_id = body[0]; const sockaddr_in control_remote = EndpointFromHpaiAt(body, len, 2, 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) { const std::vector body{0x04, channel_id, sequence, status}; const auto packet = KnxNetIpPacket(service, body); sendPacket(packet, remote); } void GatewayKnxTpIpRouter::sendSecureSessionStatus(uint8_t status, const sockaddr_in& remote) { const std::vector body{status, 0x00}; const auto packet = KnxNetIpPacket(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 { const auto netif = SelectKnxNetifForRemote(remote); if (!netif.has_value()) { return std::nullopt; } std::array hpai{}; hpai[0] = 0x08; hpai[1] = tcp ? kKnxHpaiIpv4Tcp : kKnxHpaiIpv4Udp; WriteIp(hpai.data() + 2, netif->address); WriteBe16(hpai.data() + 6, config_.udp_port); return hpai; } 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((kKnxManufacturerId >> 8) & 0xff); dib[9] = static_cast(kKnxManufacturerId & 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::sendSearchResponse(uint16_t service, const sockaddr_in& remote, const std::set& requested_dibs) { if (udp_sock_ < 0 && active_tcp_sock_ < 0) { return; } const auto hpai = localHpaiForRemote(remote, currentTransportAllowsTcpHpai()); if (!hpai.has_value()) { ESP_LOGW(kTag, "cannot send KNXnet/IP search response to %s: no active IPv4 interface", EndpointString(remote).c_str()); return; } std::vector body; body.insert(body.end(), hpai->begin(), hpai->end()); std::set appended_dibs; auto append_dib = [&body, &appended_dibs](const std::vector& dib) { if (dib.size() < 2 || !appended_dibs.insert(dib[1]).second) { return; } body.insert(body.end(), dib.begin(), dib.end()); }; append_dib(buildDeviceInfoDib(remote)); append_dib(buildSupportedServiceDib()); if (service == kServiceSearchResponseExt) { append_dib(buildExtendedDeviceInfoDib()); for (const uint8_t dib_type : requested_dibs) { switch (dib_type) { case kKnxDibDeviceInfo: append_dib(buildDeviceInfoDib(remote)); break; case kKnxDibSupportedServices: append_dib(buildSupportedServiceDib()); break; case kKnxDibIpConfig: append_dib(buildIpConfigDib(remote, false)); break; case kKnxDibCurrentIpConfig: append_dib(buildIpConfigDib(remote, true)); break; case kKnxDibKnxAddresses: append_dib(buildKnxAddressesDib()); break; case kKnxDibTunnellingInfo: if (config_.tunnel_enabled) { append_dib(buildTunnelingInfoDib()); } break; case kKnxDibExtendedDeviceInfo: append_dib(buildExtendedDeviceInfoDib()); break; default: break; } } } const auto packet = KnxNetIpPacket(service, body); sendPacket(packet, remote); ESP_LOGI(kTag, "sent KNXnet/IP search response namespace=%s mainGroup=%u to %s:%u endpoint=%u.%u.%u.%u:%u", openknx_namespace_.c_str(), static_cast(config_.main_group), Ipv4String(remote.sin_addr.s_addr).c_str(), static_cast(ntohs(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::sendDescriptionResponse(const sockaddr_in& remote) { if (udp_sock_ < 0 && active_tcp_sock_ < 0) { return; } auto device = buildDeviceInfoDib(remote); auto services = buildSupportedServiceDib(); std::vector body; body.reserve(device.size() + services.size()); body.insert(body.end(), device.begin(), device.end()); body.insert(body.end(), services.begin(), services.end()); const auto packet = KnxNetIpPacket(kServiceDescriptionResponse, body); sendPacket(packet, 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(remote.sin_addr.s_addr).c_str(), static_cast(ntohs(remote.sin_port))); } 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) { if (!client.connected || data == nullptr || len == 0) { return; } const uint16_t service = TunnelServiceForCemi(data, len); std::vector body; body.reserve(4 + len); body.push_back(0x04); body.push_back(client.channel_id); body.push_back(client.send_sequence++); body.push_back(0x00); body.insert(body.end(), data, data + len); const auto packet = KnxNetIpPacket(service, body); sendPacketToTunnelClient(client, 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(body[2]), static_cast(data[0]), static_cast(len), EndpointString(client.data_remote).c_str()); } void GatewayKnxTpIpRouter::sendConnectionStateResponse(uint8_t channel_id, uint8_t status, const sockaddr_in& remote) { const std::vector body{channel_id, status}; const auto packet = KnxNetIpPacket(kServiceConnectionStateResponse, body); sendPacket(packet, remote); } void GatewayKnxTpIpRouter::sendDisconnectResponse(uint8_t channel_id, uint8_t status, const sockaddr_in& remote) { const std::vector body{channel_id, status}; const auto packet = KnxNetIpPacket(kServiceDisconnectResponse, body); 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) { std::vector body; body.reserve(16); body.push_back(channel_id); body.push_back(status); if (status != kKnxNoError) { const auto packet = KnxNetIpPacket(kServiceConnectResponse, body); 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 data_endpoint = localHpaiForRemote(remote, currentTransportAllowsTcpHpai()); if (!data_endpoint.has_value()) { ESP_LOGW(kTag, "cannot accept KNXnet/IP connect from %s: no active IPv4 interface", EndpointString(remote).c_str()); const auto packet = KnxNetIpPacket(kServiceConnectResponse, std::vector{channel_id, kKnxErrorConnectionType}); sendPacket(packet, remote); return; } body.insert(body.end(), data_endpoint->begin(), data_endpoint->end()); body.push_back(connection_type == kKnxConnectionTypeTunnel ? 0x04 : 0x02); body.push_back(connection_type); if (connection_type == kKnxConnectionTypeTunnel) { body.push_back(static_cast((tunnel_address >> 8) & 0xff)); body.push_back(static_cast(tunnel_address & 0xff)); } const auto packet = KnxNetIpPacket(kServiceConnectResponse, body); sendPacket(packet, remote); ESP_LOGI(kTag, "sent KNXnet/IP connect response channel=%u type=0x%02x to %s endpoint=%u.%u.%u.%u:%u", static_cast(channel_id), static_cast(connection_type), EndpointString(remote).c_str(), static_cast((*data_endpoint)[2]), static_cast((*data_endpoint)[3]), static_cast((*data_endpoint)[4]), static_cast((*data_endpoint)[5]), static_cast(config_.udp_port)); } 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()); const std::vector body(data, data + len); const auto packet = KnxNetIpPacket(kServiceRoutingIndication, body); 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) { SemaphoreGuard guard(openknx_lock_); if (ets_device_ == nullptr) { return false; } const bool consumed = ets_device_->handleTunnelFrame( data, len, [this, response_client](const uint8_t* response, size_t response_len) { if (response_client != nullptr && response_client->connected) { sendTunnelIndicationToClient(*response_client, response, response_len); } else { sendTunnelIndication(response, response_len); } }); syncOpenKnxConfigFromDevice(); return consumed; } 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::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); forwardCemiToTp(frame_data, frame_len); }); 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; } void GatewayKnxTpIpRouter::pollTpUart() { if (tp_uart_port_ < 0) { return; } std::array buffer{}; const int read = uart_read_bytes(static_cast(tp_uart_port_), buffer.data(), buffer.size(), 0); if (read <= 0) { return; } for (int index = 0; index < read; ++index) { const uint8_t byte = buffer[static_cast(index)]; if (tp_rx_frame_.empty()) { if (IsTpUartControlByte(byte)) { handleTpUartControlByte(byte); continue; } if (byte == 0xcb || (byte & 0x17U) == 0x13U) { continue; } } const TickType_t now = xTaskGetTickCount(); if (!tp_rx_frame_.empty() && tp_uart_last_byte_tick_ != 0 && now - tp_uart_last_byte_tick_ > pdMS_TO_TICKS(1000)) { tp_rx_frame_.clear(); } if (tp_rx_frame_.empty()) { if (IsTpUartFrameStart(byte, &tp_uart_extended_frame_)) { tp_rx_frame_.push_back(byte); tp_uart_last_byte_tick_ = now; } continue; } tp_rx_frame_.push_back(byte); tp_uart_last_byte_tick_ = now; const size_t expected = ExpectedTpFrameSize(tp_rx_frame_.data(), tp_rx_frame_.size()); if (expected == 0) { continue; } if (tp_rx_frame_.size() == expected) { const uint8_t ack = kTpUartAckInfo; uart_write_bytes(static_cast(tp_uart_port_), &ack, 1); handleTpTelegram(tp_rx_frame_.data(), tp_rx_frame_.size()); tp_rx_frame_.clear(); } else if (tp_rx_frame_.size() > expected || tp_rx_frame_.size() > 263U) { tp_rx_frame_.clear(); } } } void GatewayKnxTpIpRouter::handleTpUartControlByte(uint8_t byte) { if (byte == kTpUartResetIndication) { ESP_LOGW(kTag, "KNX TP-UART reset indication received; marking link offline"); tp_uart_online_ = false; return; } if (byte == kTpUartBusy) { last_error_ = "KNX TP-UART bus busy"; ESP_LOGW(kTag, "%s", last_error_.c_str()); return; } if (byte == kTpUartLDataConfirmNegative) { last_error_ = "KNX TP-UART negative confirmation"; ESP_LOGW(kTag, "%s", last_error_.c_str()); return; } if (byte == kTpUartLDataConfirmPositive) { return; } if ((byte & kTpUartStateIndicationMask) == kTpUartStateIndicationMask) { tp_uart_online_ = true; } } void GatewayKnxTpIpRouter::handleTpTelegram(const uint8_t* data, size_t len) { if (data == nullptr || len == 0) { return; } const std::vector telegram(data, data + len); if (!tp_last_sent_telegram_.empty() && TpTelegramEqualsIgnoringRepeatBit(telegram, tp_last_sent_telegram_)) { tp_last_sent_telegram_.clear(); return; } const auto cemi = TpTelegramToCemi(data, len); if (!cemi.has_value()) { return; } const bool consumed_by_openknx = handleOpenKnxBusFrame(cemi->data(), cemi->size()); if (!consumed_by_openknx) { const DaliBridgeResult result = handler_(cemi->data(), cemi->size()); if (!result.ok && !result.error.empty()) { ESP_LOGD(kTag, "KNX TP frame not routed to DALI: %s", result.error.c_str()); } } sendTunnelIndication(cemi->data(), cemi->size()); sendRoutingIndication(cemi->data(), cemi->size()); } void GatewayKnxTpIpRouter::forwardCemiToTp(const uint8_t* data, size_t len) { if (tp_uart_port_ < 0 || data == nullptr || len == 0 || !tp_uart_online_) { return; } const auto telegram = CemiToTpTelegram(data, len); if (!telegram.has_value()) { return; } tp_last_sent_telegram_ = *telegram; const auto wrapped = WrapTpUartTelegram(*telegram); uart_write_bytes(static_cast(tp_uart_port_), wrapped.data(), wrapped.size()); } } // namespace gateway