#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