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