#include "gateway_knx.hpp" #include "dali_define.hpp" #include "driver/uart.h" #include "esp_log.h" #include "lwip/inet.h" #include "lwip/sockets.h" #include "openknx_idf/ets_device_runtime.h" #include #include #include #include #include #include #include #include #include #include #include namespace gateway { namespace { constexpr const char* kTag = "gateway_knx"; constexpr uint8_t kCemiLDataReq = 0x11; constexpr uint8_t kCemiLDataInd = 0x29; constexpr uint8_t kCemiLDataCon = 0x2e; constexpr uint16_t 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 kServiceTunnellingRequest = 0x0420; constexpr uint16_t kServiceTunnellingAck = 0x0421; constexpr uint16_t kServiceRoutingIndication = 0x0530; 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 kKnxErrorSequenceNumber = 0x04; constexpr uint8_t kKnxConnectionTypeTunnel = 0x04; constexpr uint8_t kKnxTunnelLayerLink = 0x02; constexpr uint8_t kTpUartResetRequest = 0x01; constexpr uint8_t kTpUartResetIndication = 0x03; constexpr uint8_t kTpUartStateRequest = 0x02; constexpr uint8_t kTpUartStateIndicationMask = 0x07; constexpr uint8_t kTpUartSetAddressRequest = 0x28; constexpr uint8_t kTpUartAckInfo = 0x10; constexpr uint8_t kTpUartLDataConfirmPositive = 0x8b; constexpr uint8_t kTpUartLDataConfirmNegative = 0x0b; constexpr uint8_t kTpUartLDataStart = 0x80; constexpr uint8_t kTpUartLDataEnd = 0x40; constexpr uint8_t kTpUartBusy = 0xc0; constexpr uint16_t kGwReg1AdrKoOffset = 12; constexpr uint16_t kGwReg1AdrKoBlockSize = 18; constexpr uint16_t kGwReg1GrpKoOffset = 1164; constexpr uint16_t kGwReg1GrpKoBlockSize = 17; constexpr uint16_t kGwReg1AppKoBroadcastSwitch = 1; constexpr uint16_t kGwReg1AppKoBroadcastDimm = 2; constexpr uint8_t kGwReg1KoSwitch = 0; constexpr uint8_t kGwReg1KoDimmAbsolute = 3; constexpr uint8_t kGwReg1KoColor = 6; constexpr uint8_t kReg1DaliFunctionObjectIndex = 160; constexpr uint8_t kReg1DaliFunctionPropertyId = 1; constexpr uint8_t kReg1FunctionType = 2; constexpr uint8_t kReg1FunctionScan = 3; constexpr uint8_t kReg1FunctionAssign = 4; constexpr uint8_t kReg1FunctionEvgWrite = 10; constexpr uint8_t kReg1FunctionEvgRead = 11; constexpr uint8_t kReg1FunctionSetScene = 12; constexpr uint8_t kReg1FunctionGetScene = 13; constexpr uint8_t kReg1FunctionIdentify = 14; constexpr uint8_t kReg1DeviceTypeDt8 = 8; constexpr uint8_t kReg1ColorTypeTw = 1; constexpr uint8_t kDaliDeviceTypeNone = 0xfe; constexpr uint8_t kDaliDeviceTypeMultiple = 0xff; struct DecodedGroupWrite { uint16_t group_address{0}; std::vector data; }; uint16_t ReadBe16(const uint8_t* data) { return static_cast((static_cast(data[0]) << 8) | data[1]); } 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::kColorTemperature: return "Color Temperature"; case GatewayKnxDaliDataType::kRgb: return "RGB"; case GatewayKnxDaliDataType::kUnknown: default: return "Unknown"; } } const char* DataTypeDpt(GatewayKnxDaliDataType data_type) { switch (data_type) { case GatewayKnxDaliDataType::kSwitch: return "DPST-1-1"; case GatewayKnxDaliDataType::kBrightness: return "DPST-5-1"; case GatewayKnxDaliDataType::kColorTemperature: return "DPST-7-600"; case GatewayKnxDaliDataType::kRgb: return "DPST-232-600"; case GatewayKnxDaliDataType::kUnknown: default: return ""; } } std::optional DecodeCemiGroupWrite(const uint8_t* data, size_t len) { if (data == nullptr || len < 10) { return std::nullopt; } const uint8_t message_code = data[0]; if (message_code != kCemiLDataReq && message_code != kCemiLDataInd && message_code != kCemiLDataCon) { return std::nullopt; } const size_t base = 2U + data[1]; if (len < base + 8U) { return std::nullopt; } const uint8_t control2 = data[base + 1]; if ((control2 & 0x80) == 0) { return std::nullopt; } const uint16_t destination = ReadBe16(data + base + 4); const size_t tpdu_len = static_cast(data[base + 6]) + 1U; if (tpdu_len < 2U || len < base + 7U + tpdu_len) { return std::nullopt; } const uint8_t* tpdu = data + base + 7; const uint16_t apci = static_cast(((tpdu[0] & 0x03) << 8) | (tpdu[1] & 0xc0)); if (apci != 0x80) { return std::nullopt; } DecodedGroupWrite out; out.group_address = destination; if (tpdu_len == 2U) { out.data.push_back(tpdu[1] & 0x3f); } else { out.data.assign(tpdu + 2, tpdu + tpdu_len); } return out; } uint8_t Reg1PercentToArc(uint8_t value) { if (value == 0 || value == 0xff) { return value; } const double arc = ((253.0 / 3.0) * (std::log10(static_cast(value)) + 1.0)) + 1.0; return static_cast(std::clamp(static_cast(arc + 0.5), 0, 254)); } uint8_t Reg1ArcToPercent(uint8_t value) { if (value == 0 || value == 0xff) { return value; } const double percent = std::pow(10.0, ((static_cast(value) - 1.0) / (253.0 / 3.0)) - 1.0); return static_cast(std::clamp(static_cast(percent + 0.5), 0, 100)); } GatewayKnxDaliTarget Reg1SceneTarget(uint8_t encoded_target) { if ((encoded_target & 0x80) != 0) { return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kGroup, static_cast(encoded_target & 0x0f)}; } return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kShortAddress, static_cast(encoded_target & 0x3f)}; } DaliBridgeRequest FunctionRequest(const char* sequence, BridgeOperation operation) { DaliBridgeRequest request; request.sequence = sequence == nullptr ? "knx-function-property" : sequence; request.operation = operation; return request; } void ApplyTargetToRequest(const GatewayKnxDaliTarget& target, DaliBridgeRequest* request) { if (request == nullptr) { return; } switch (target.kind) { case GatewayKnxDaliTargetKind::kBroadcast: request->metadata["broadcast"] = true; break; case GatewayKnxDaliTargetKind::kShortAddress: request->shortAddress = target.address; break; case GatewayKnxDaliTargetKind::kGroup: request->metadata["group"] = target.address; break; case GatewayKnxDaliTargetKind::kNone: default: break; } } DaliBridgeResult ExecuteRaw(DaliBridgeEngine& engine, BridgeOperation operation, uint8_t addr, uint8_t cmd, const char* sequence) { DaliBridgeRequest request = FunctionRequest(sequence, operation); request.rawAddress = addr; request.rawCommand = cmd; return engine.execute(request); } std::optional QueryShort(DaliBridgeEngine& engine, uint8_t short_address, uint8_t command, const char* sequence) { const auto result = ExecuteRaw(engine, BridgeOperation::query, DaliComm::toCmdAddr(short_address), command, sequence); if (!result.ok || !result.data.has_value()) { return std::nullopt; } return result.data.value(); } bool SendRaw(DaliBridgeEngine& engine, uint8_t addr, uint8_t cmd, const char* sequence) { return ExecuteRaw(engine, BridgeOperation::send, addr, cmd, sequence).ok; } bool SendRawExt(DaliBridgeEngine& engine, uint8_t addr, uint8_t cmd, const char* sequence) { return ExecuteRaw(engine, BridgeOperation::sendExt, addr, cmd, sequence).ok; } std::optional MetadataInt(const DaliBridgeResult& result, const std::string& key) { return getObjectInt(result.metadata, key); } DaliBridgeRequest RequestForTarget(uint16_t group_address, const GatewayKnxDaliTarget& target, BridgeOperation operation) { DaliBridgeRequest request; request.sequence = "knx-" + GatewayKnxGroupAddressString(group_address); request.operation = operation; switch (target.kind) { case GatewayKnxDaliTargetKind::kBroadcast: request.metadata["broadcast"] = true; break; case GatewayKnxDaliTargetKind::kShortAddress: request.shortAddress = target.address; break; case GatewayKnxDaliTargetKind::kGroup: request.metadata["group"] = target.address; break; case GatewayKnxDaliTargetKind::kNone: default: break; } request.metadata["sourceProtocol"] = "knx"; request.metadata["knxGroupAddress"] = GatewayKnxGroupAddressString(group_address); return request; } DaliBridgeResult ErrorResult(uint16_t group_address, const char* message) { DaliBridgeResult result; result.sequence = "knx-" + GatewayKnxGroupAddressString(group_address); result.error = message == nullptr ? "KNX error" : message; return result; } bool SendAll(int sock, const uint8_t* data, size_t len, const sockaddr_in& remote) { return sendto(sock, data, len, 0, reinterpret_cast(&remote), sizeof(remote)) == static_cast(len); } std::vector KnxNetIpPacket(uint16_t service, const std::vector& body) { std::vector packet(6 + body.size()); packet[0] = kKnxNetIpHeaderSize; packet[1] = kKnxNetIpVersion10; WriteBe16(packet.data() + 2, service); WriteBe16(packet.data() + 4, static_cast(packet.size())); if (!body.empty()) { std::memcpy(packet.data() + 6, body.data(), body.size()); } return packet; } std::array HpaiForRemote(const sockaddr_in& remote) { std::array hpai{}; hpai[0] = 0x08; hpai[1] = 0x01; const uint32_t address = ntohl(remote.sin_addr.s_addr); hpai[2] = static_cast((address >> 24) & 0xff); hpai[3] = static_cast((address >> 16) & 0xff); hpai[4] = static_cast((address >> 8) & 0xff); hpai[5] = static_cast(address & 0xff); WriteBe16(hpai.data() + 6, ntohs(remote.sin_port)); return hpai; } 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 IsExtendedTpFrame(const uint8_t* data, size_t len) { return len > 0 && (data[0] & 0xD3) == 0x10; } size_t ExpectedTpFrameSize(const uint8_t* data, size_t len) { if (data == nullptr || len < 6) { return 0; } if (IsExtendedTpFrame(data, len)) { return 9U + data[6]; } return 8U + (data[5] & 0x0F); } bool ValidateTpChecksum(const uint8_t* data, size_t len) { if (data == nullptr || len < 2) { return false; } uint8_t crc = 0xFF; for (size_t index = 0; index + 1 < len; ++index) { crc ^= data[index]; } return data[len - 1] == crc; } bool IsTpUartControlByte(uint8_t byte) { return byte == kTpUartResetIndication || byte == kTpUartLDataConfirmPositive || byte == kTpUartLDataConfirmNegative || byte == kTpUartBusy || (byte & kTpUartStateIndicationMask) == kTpUartStateIndicationMask; } bool IsTpUartFrameStart(uint8_t byte, bool* extended) { if (extended == nullptr) { return false; } *extended = (byte & 0x80) == 0; return (byte & 0x50) == 0x10; } std::vector WrapTpUartTelegram(const std::vector& telegram) { std::vector wrapped; wrapped.reserve(telegram.size() * 2U); for (size_t index = 0; index < telegram.size(); ++index) { const uint8_t control = static_cast( (index + 1U == telegram.size() ? kTpUartLDataEnd : kTpUartLDataStart) | (index & 0x3fU)); wrapped.push_back(control); wrapped.push_back(telegram[index]); } return wrapped; } bool TpTelegramEqualsIgnoringRepeatBit(const std::vector& left, const std::vector& right) { if (left.size() != right.size() || left.empty()) { return false; } if ((left[0] & static_cast(~0x20U)) != (right[0] & static_cast(~0x20U))) { return false; } return std::equal(left.begin() + 1, left.end(), right.begin() + 1); } std::optional> CemiToTpTelegram(const uint8_t* data, size_t len) { if (data == nullptr || len < 10 || data[1] != 0) { return std::nullopt; } const uint8_t* ctrl = data + 2; const bool standard = (ctrl[0] & 0x80) != 0; const size_t tp_len = standard ? len - 2U : len - 1U; if (tp_len < 8) { return std::nullopt; } std::vector telegram(tp_len, 0); if (standard) { telegram[0] = ctrl[0]; std::memcpy(telegram.data() + 1, ctrl + 2, 4); telegram[5] = static_cast((ctrl[1] & 0xF0) | (ctrl[6] & 0x0F)); if (tp_len > 7U) { std::memcpy(telegram.data() + 6, ctrl + 7, tp_len - 7U); } } else { std::memcpy(telegram.data(), ctrl, tp_len - 1U); } uint8_t crc = 0xFF; for (size_t index = 0; index + 1 < telegram.size(); ++index) { crc ^= telegram[index]; } telegram.back() = crc; return telegram; } std::optional> TpTelegramToCemi(const uint8_t* data, size_t len) { if (data == nullptr || len < 8 || !ValidateTpChecksum(data, len)) { return std::nullopt; } const bool extended = IsExtendedTpFrame(data, len); const size_t cemi_len = len + (extended ? 2U : 3U) - 1U; std::vector cemi(cemi_len, 0); cemi[0] = kCemiLDataInd; cemi[1] = 0x00; cemi[2] = data[0]; if (extended) { std::memcpy(cemi.data() + 2, data, len - 1U); } else { cemi[3] = data[5] & 0xF0; std::memcpy(cemi.data() + 4, data + 1, 4); cemi[8] = data[5] & 0x0F; const size_t copy_len = static_cast(cemi[8]) + 1U; if (9U + copy_len > cemi.size() || 6U + copy_len > len) { return std::nullopt; } std::memcpy(cemi.data() + 9, data + 6, copy_len); } return cemi; } } // namespace std::optional GatewayKnxConfigFromValue(const DaliValue* value) { if (value == nullptr || value->asObject() == nullptr) { return std::nullopt; } const auto& object = *value->asObject(); GatewayKnxConfig config; config.dali_router_enabled = ObjectBoolAny(object, {"daliRouterEnabled", "dali_router_enabled"}) .value_or(config.dali_router_enabled); config.ip_router_enabled = ObjectBoolAny(object, {"ipRouterEnabled", "ip_router_enabled"}) .value_or(config.ip_router_enabled); config.tunnel_enabled = ObjectBoolAny(object, {"tunnelEnabled", "tunnel_enabled"}) .value_or(config.tunnel_enabled); config.multicast_enabled = ObjectBoolAny(object, {"multicastEnabled", "multicast_enabled"}) .value_or(config.multicast_enabled); if (const auto mode = ObjectStringAny(object, {"mappingMode", "mapping_mode"})) { config.mapping_mode = GatewayKnxMappingModeFromString(mode.value()); } config.ets_database_enabled = ObjectBoolAny(object, {"etsDatabaseEnabled", "ets_database_enabled"}) .value_or(config.ets_database_enabled); config.ets_associations = ParseEtsAssociations(object); config.main_group = static_cast( std::clamp(ObjectIntAny(object, {"mainGroup", "main_group"}).value_or(config.main_group), 0, 31)); config.udp_port = static_cast(std::clamp( ObjectIntAny(object, {"udpPort", "port", "udp_port"}).value_or(config.udp_port), 1, 65535)); config.multicast_address = ObjectStringAny(object, {"multicastAddress", "multicast_address"}) .value_or(config.multicast_address); config.individual_address = static_cast(std::clamp( ObjectIntAny(object, {"individualAddress", "individual_address"}) .value_or(config.individual_address), 0, 0xffff)); const auto* tp_uart = getObjectValue(object, "tpUart"); if (tp_uart == nullptr) { tp_uart = getObjectValue(object, "tp_uart"); } if (tp_uart != nullptr && tp_uart->asObject() != nullptr) { const auto& serial = *tp_uart->asObject(); config.tp_uart.uart_port = std::clamp( ObjectIntAny(serial, {"uartPort", "uart_port"}).value_or(config.tp_uart.uart_port), 0, 2); config.tp_uart.tx_pin = ObjectIntAny(serial, {"txPin", "tx_pin"}).value_or(config.tp_uart.tx_pin); config.tp_uart.rx_pin = ObjectIntAny(serial, {"rxPin", "rx_pin"}).value_or(config.tp_uart.rx_pin); config.tp_uart.baudrate = static_cast(std::max( 1200, ObjectIntAny(serial, {"baudrate", "baud"}).value_or(config.tp_uart.baudrate))); config.tp_uart.rx_buffer_size = static_cast(std::max( 128, ObjectIntAny(serial, {"rxBufferSize", "rx_buffer_size"}) .value_or(static_cast(config.tp_uart.rx_buffer_size)))); config.tp_uart.tx_buffer_size = static_cast(std::max( 128, ObjectIntAny(serial, {"txBufferSize", "tx_buffer_size"}) .value_or(static_cast(config.tp_uart.tx_buffer_size)))); config.tp_uart.read_timeout_ms = static_cast(std::max( 1, ObjectIntAny(serial, {"readTimeoutMs", "read_timeout_ms"}) .value_or(static_cast(config.tp_uart.read_timeout_ms)))); } return config; } DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config) { DaliValue::Object out; out["daliRouterEnabled"] = config.dali_router_enabled; out["ipRouterEnabled"] = config.ip_router_enabled; out["tunnelEnabled"] = config.tunnel_enabled; out["multicastEnabled"] = config.multicast_enabled; out["etsDatabaseEnabled"] = config.ets_database_enabled; out["mappingMode"] = GatewayKnxMappingModeToString(config.mapping_mode); out["mainGroup"] = static_cast(config.main_group); out["udpPort"] = static_cast(config.udp_port); out["multicastAddress"] = config.multicast_address; out["individualAddress"] = static_cast(config.individual_address); DaliValue::Object serial; serial["uartPort"] = config.tp_uart.uart_port; serial["txPin"] = config.tp_uart.tx_pin; serial["rxPin"] = config.tp_uart.rx_pin; serial["baudrate"] = static_cast(config.tp_uart.baudrate); serial["rxBufferSize"] = static_cast(config.tp_uart.rx_buffer_size); serial["txBufferSize"] = static_cast(config.tp_uart.tx_buffer_size); serial["readTimeoutMs"] = static_cast(config.tp_uart.read_timeout_ms); out["tpUart"] = std::move(serial); DaliValue::Array ets_associations; ets_associations.reserve(config.ets_associations.size()); for (const auto& association : config.ets_associations) { DaliValue::Object entry; entry["groupAddress"] = static_cast(association.group_address); entry["groupObjectNumber"] = static_cast(association.group_object_number); ets_associations.emplace_back(std::move(entry)); } out["etsAssociations"] = std::move(ets_associations); return DaliValue(std::move(out)); } const char* GatewayKnxMappingModeToString(GatewayKnxMappingMode mode) { switch (mode) { case GatewayKnxMappingMode::kEtsDatabase: return "ets_database"; case GatewayKnxMappingMode::kGwReg1Direct: return "gw_reg1_direct"; case GatewayKnxMappingMode::kManual: return "manual"; case GatewayKnxMappingMode::kFormula: default: return "formula"; } } GatewayKnxMappingMode GatewayKnxMappingModeFromString(const std::string& value) { const std::string normalized = NormalizeModeString(value); if (normalized == "gwreg1direct" || normalized == "gwreg1" || normalized == "gwreg1channel" || normalized == "channelindex") { return GatewayKnxMappingMode::kGwReg1Direct; } if (normalized == "manual" || normalized == "database" || normalized == "db") { return GatewayKnxMappingMode::kManual; } if (normalized == "etsdatabase" || normalized == "ets" || normalized == "openknx") { return GatewayKnxMappingMode::kEtsDatabase; } return GatewayKnxMappingMode::kFormula; } const char* GatewayKnxDataTypeToString(GatewayKnxDaliDataType data_type) { switch (data_type) { case GatewayKnxDaliDataType::kSwitch: return "switch"; case GatewayKnxDaliDataType::kBrightness: return "brightness"; case GatewayKnxDaliDataType::kColorTemperature: return "color_temperature"; case GatewayKnxDaliDataType::kRgb: return "rgb"; case GatewayKnxDaliDataType::kUnknown: default: return "unknown"; } } const char* GatewayKnxTargetKindToString(GatewayKnxDaliTargetKind kind) { switch (kind) { case GatewayKnxDaliTargetKind::kBroadcast: return "broadcast"; case GatewayKnxDaliTargetKind::kShortAddress: return "short_address"; case GatewayKnxDaliTargetKind::kGroup: return "group"; case GatewayKnxDaliTargetKind::kNone: default: return "none"; } } std::optional GatewayKnxDaliDataTypeForMiddleGroup( uint8_t middle_group) { switch (middle_group) { case 1: return GatewayKnxDaliDataType::kSwitch; case 2: return GatewayKnxDaliDataType::kBrightness; case 3: return GatewayKnxDaliDataType::kColorTemperature; case 4: return GatewayKnxDaliDataType::kRgb; default: return std::nullopt; } } std::optional GatewayKnxDaliTargetForSubgroup(uint8_t sub_group) { if (sub_group == 0) { return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kBroadcast, 127}; } if (sub_group >= 1 && sub_group <= 64) { return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kShortAddress, static_cast(sub_group - 1)}; } if (sub_group >= 65 && sub_group <= 80) { return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kGroup, static_cast(sub_group - 65)}; } return std::nullopt; } uint16_t GatewayKnxGroupAddress(uint8_t main_group, uint8_t middle_group, uint8_t sub_group) { return static_cast(((main_group & 0x1f) << 11) | ((middle_group & 0x07) << 8) | sub_group); } std::string GatewayKnxGroupAddressString(uint16_t group_address) { const int main = (group_address >> 11) & 0x1f; const int middle = (group_address >> 8) & 0x07; const int sub = group_address & 0xff; return std::to_string(main) + "/" + std::to_string(middle) + "/" + std::to_string(sub); } namespace { uint16_t GwReg1GroupAddressForObject(uint8_t main_group, uint16_t object_number) { return GatewayKnxGroupAddress(main_group, static_cast(object_number >> 8), static_cast(object_number & 0xff)); } GatewayKnxDaliBinding MakeGwReg1Binding(uint8_t main_group, uint16_t object_number, int channel_index, const char* object_role, GatewayKnxDaliDataType data_type, GatewayKnxDaliTarget target) { GatewayKnxDaliBinding binding; binding.mapping_mode = GatewayKnxMappingMode::kGwReg1Direct; binding.group_object_number = static_cast(object_number); binding.channel_index = channel_index; binding.object_role = object_role; binding.main_group = main_group; binding.middle_group = static_cast((object_number >> 8) & 0x07); binding.sub_group = static_cast(object_number & 0xff); binding.group_address = GwReg1GroupAddressForObject(main_group, object_number); binding.address = GatewayKnxGroupAddressString(binding.group_address); binding.data_type = data_type; binding.target = target; binding.datapoint_type = DataTypeDpt(data_type); binding.name = std::string("GW-REG1 ") + TargetName(target) + " - " + DataTypeName(data_type); return binding; } std::optional GwReg1BindingForObject(uint8_t main_group, uint16_t object_number) { if (object_number == kGwReg1AppKoBroadcastSwitch) { return MakeGwReg1Binding( main_group, object_number, -1, "broadcast_switch", GatewayKnxDaliDataType::kSwitch, GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kBroadcast, 127}); } if (object_number == kGwReg1AppKoBroadcastDimm) { return MakeGwReg1Binding( main_group, object_number, -1, "broadcast_dimm_absolute", GatewayKnxDaliDataType::kBrightness, GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kBroadcast, 127}); } const int adr_relative = static_cast(object_number) - kGwReg1AdrKoOffset; if (adr_relative >= 0 && adr_relative < kGwReg1AdrKoBlockSize * 64) { const int channel = adr_relative / kGwReg1AdrKoBlockSize; const int slot = adr_relative % kGwReg1AdrKoBlockSize; const GatewayKnxDaliTarget target{GatewayKnxDaliTargetKind::kShortAddress, channel}; if (slot == kGwReg1KoSwitch) { return MakeGwReg1Binding(main_group, object_number, channel, "switch", GatewayKnxDaliDataType::kSwitch, target); } if (slot == kGwReg1KoDimmAbsolute) { return MakeGwReg1Binding(main_group, object_number, channel, "dimm_absolute", GatewayKnxDaliDataType::kBrightness, target); } if (slot == kGwReg1KoColor) { return MakeGwReg1Binding(main_group, object_number, channel, "color", GatewayKnxDaliDataType::kRgb, target); } } const int group_relative = static_cast(object_number) - kGwReg1GrpKoOffset; if (group_relative >= 0 && group_relative < kGwReg1GrpKoBlockSize * 16) { const int group = group_relative / kGwReg1GrpKoBlockSize; const int slot = group_relative % kGwReg1GrpKoBlockSize; const GatewayKnxDaliTarget target{GatewayKnxDaliTargetKind::kGroup, group}; if (slot == kGwReg1KoSwitch) { return MakeGwReg1Binding(main_group, object_number, group, "switch", GatewayKnxDaliDataType::kSwitch, target); } if (slot == kGwReg1KoDimmAbsolute) { return MakeGwReg1Binding(main_group, object_number, group, "dimm_absolute", GatewayKnxDaliDataType::kBrightness, target); } if (slot == kGwReg1KoColor) { return MakeGwReg1Binding(main_group, object_number, group, "color", GatewayKnxDaliDataType::kRgb, target); } } return std::nullopt; } std::optional EtsBindingForAssociation(uint8_t main_group, const GatewayKnxEtsAssociation& association) { auto binding = GwReg1BindingForObject(main_group, association.group_object_number); if (!binding.has_value()) { return std::nullopt; } binding->mapping_mode = GatewayKnxMappingMode::kEtsDatabase; binding->group_address = association.group_address; binding->address = GatewayKnxGroupAddressString(association.group_address); binding->name = std::string("ETS ") + binding->name; return binding; } } // namespace GatewayKnxBridge::GatewayKnxBridge(DaliBridgeEngine& engine) : engine_(engine) {} void GatewayKnxBridge::setConfig(const GatewayKnxConfig& config) { config_ = config; rebuildEtsBindings(); } const GatewayKnxConfig& GatewayKnxBridge::config() const { return config_; } size_t GatewayKnxBridge::etsBindingCount() const { size_t count = 0; for (const auto& entry : ets_bindings_by_group_address_) { count += entry.second.size(); } return count; } std::vector GatewayKnxBridge::describeDaliBindings() const { std::vector bindings; std::set ets_group_addresses; if (config_.ets_database_enabled) { for (const auto& entry : ets_bindings_by_group_address_) { ets_group_addresses.insert(entry.first); bindings.insert(bindings.end(), entry.second.begin(), entry.second.end()); } } if (config_.mapping_mode == GatewayKnxMappingMode::kGwReg1Direct) { bindings.reserve(2 + (64 * 3) + (16 * 3)); if (const auto binding = GwReg1BindingForObject(config_.main_group, kGwReg1AppKoBroadcastSwitch)) { if (ets_group_addresses.count(binding->group_address) == 0) { bindings.push_back(binding.value()); } } if (const auto binding = GwReg1BindingForObject(config_.main_group, kGwReg1AppKoBroadcastDimm)) { if (ets_group_addresses.count(binding->group_address) == 0) { bindings.push_back(binding.value()); } } for (int address = 0; address < 64; ++address) { const uint16_t base = static_cast(kGwReg1AdrKoOffset + (address * kGwReg1AdrKoBlockSize)); for (const uint8_t slot : {kGwReg1KoSwitch, kGwReg1KoDimmAbsolute, kGwReg1KoColor}) { if (const auto binding = GwReg1BindingForObject(config_.main_group, base + slot)) { if (ets_group_addresses.count(binding->group_address) == 0) { bindings.push_back(binding.value()); } } } } for (int group = 0; group < 16; ++group) { const uint16_t base = static_cast(kGwReg1GrpKoOffset + (group * kGwReg1GrpKoBlockSize)); for (const uint8_t slot : {kGwReg1KoSwitch, kGwReg1KoDimmAbsolute, kGwReg1KoColor}) { if (const auto binding = GwReg1BindingForObject(config_.main_group, base + slot)) { if (ets_group_addresses.count(binding->group_address) == 0) { bindings.push_back(binding.value()); } } } } return bindings; } bindings.reserve(4 * 81); for (uint8_t middle = 1; middle <= 4; ++middle) { const auto data_type = GatewayKnxDaliDataTypeForMiddleGroup(middle); if (!data_type.has_value()) { continue; } for (uint8_t sub = 0; sub <= 80; ++sub) { const auto target = GatewayKnxDaliTargetForSubgroup(sub); if (!target.has_value()) { continue; } GatewayKnxDaliBinding binding; binding.mapping_mode = GatewayKnxMappingMode::kFormula; binding.main_group = config_.main_group; binding.middle_group = middle; binding.sub_group = sub; binding.group_address = GatewayKnxGroupAddress(config_.main_group, middle, sub); binding.address = GatewayKnxGroupAddressString(binding.group_address); binding.data_type = data_type.value(); binding.target = target.value(); if (ets_group_addresses.count(binding.group_address) != 0) { continue; } binding.object_role = GatewayKnxDataTypeToString(data_type.value()); binding.datapoint_type = DataTypeDpt(data_type.value()); binding.name = TargetName(target.value()) + " - " + DataTypeName(data_type.value()); bindings.push_back(std::move(binding)); } } return bindings; } DaliBridgeResult GatewayKnxBridge::handleCemiFrame(const uint8_t* data, size_t len) { const auto decoded = DecodeCemiGroupWrite(data, len); if (!decoded.has_value()) { return ErrorResult(0, "unsupported or non group-write cEMI frame"); } return handleGroupWrite(decoded->group_address, decoded->data.data(), decoded->data.size()); } DaliBridgeResult GatewayKnxBridge::handleGroupWrite(uint16_t group_address, const uint8_t* data, size_t len) { if (!config_.dali_router_enabled) { return ErrorResult(group_address, "KNX to DALI router disabled"); } if (config_.ets_database_enabled) { const auto ets_bindings = ets_bindings_by_group_address_.find(group_address); if (ets_bindings != ets_bindings_by_group_address_.end()) { return executeEtsBindings(group_address, ets_bindings->second, data, len); } } const uint8_t main = static_cast((group_address >> 11) & 0x1f); const uint8_t middle = static_cast((group_address >> 8) & 0x07); const uint8_t sub = static_cast(group_address & 0xff); if (main != config_.main_group) { return ErrorResult(group_address, "KNX main group does not match gateway config"); } if (config_.mapping_mode == GatewayKnxMappingMode::kGwReg1Direct) { const uint16_t object_number = static_cast((middle << 8) | sub); const auto binding = GwReg1BindingForObject(config_.main_group, object_number); if (!binding.has_value()) { return ErrorResult(group_address, "unmapped GW-REG1 KNX object address"); } return executeForDecodedWrite(group_address, binding->data_type, binding->target, data, len); } if (config_.mapping_mode == GatewayKnxMappingMode::kManual) { return ErrorResult(group_address, "manual KNX mapping dataset is not configured"); } const auto data_type = GatewayKnxDaliDataTypeForMiddleGroup(middle); const auto target = GatewayKnxDaliTargetForSubgroup(sub); if (!data_type.has_value() || !target.has_value()) { return ErrorResult(group_address, "unmapped KNX group address"); } return executeForDecodedWrite(group_address, data_type.value(), target.value(), data, len); } bool GatewayKnxBridge::handleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len, std::vector* response) { if (object_index != kReg1DaliFunctionObjectIndex || property_id != kReg1DaliFunctionPropertyId || data == nullptr || len == 0 || response == nullptr) { return false; } switch (data[0]) { case kReg1FunctionType: return handleReg1TypeCommand(data, len, response); case kReg1FunctionScan: return handleReg1ScanCommand(data, len, response); case kReg1FunctionAssign: return handleReg1AssignCommand(data, len, response); case kReg1FunctionEvgWrite: return handleReg1EvgWriteCommand(data, len, response); case kReg1FunctionEvgRead: return handleReg1EvgReadCommand(data, len, response); case kReg1FunctionSetScene: return handleReg1SetSceneCommand(data, len, response); case kReg1FunctionGetScene: return handleReg1GetSceneCommand(data, len, response); case kReg1FunctionIdentify: return handleReg1IdentifyCommand(data, len, response); default: return false; } } bool GatewayKnxBridge::handleFunctionPropertyState(uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len, std::vector* response) { if (object_index != kReg1DaliFunctionObjectIndex || property_id != kReg1DaliFunctionPropertyId || data == nullptr || len == 0 || response == nullptr) { return false; } switch (data[0]) { case kReg1FunctionScan: case 5: return handleReg1ScanState(data, len, response); case kReg1FunctionAssign: return handleReg1AssignState(data, len, response); case 7: return handleReg1FoundEvgsState(data, len, response); default: return false; } } bool GatewayKnxBridge::handleReg1TypeCommand(const uint8_t* data, size_t len, std::vector* response) { if (len < 2 || response == nullptr) { return false; } const uint8_t short_address = data[1]; const auto type_response = QueryShort(engine_, short_address, DALI_CMD_QUERY_DEVICE_TYPE, "knx-function-type"); if (!type_response.has_value()) { *response = {0x01}; return true; } uint8_t device_type = static_cast(type_response.value()); if (device_type == kDaliDeviceTypeMultiple) { for (int index = 0; index < 16; ++index) { const auto next_type = QueryShort(engine_, short_address, DALI_CMD_QUERY_NEXT_DEVICE_TYPE, "knx-function-next-device-type"); if (!next_type.has_value()) { *response = {0x01}; return true; } if (next_type.value() == kDaliDeviceTypeNone) { break; } if (next_type.value() < 20) { device_type = static_cast(next_type.value()); } } } *response = {0x00, device_type}; if (device_type == kReg1DeviceTypeDt8) { if (!SendRaw(engine_, DALI_CMD_SPECIAL_DT_SELECT, kReg1DeviceTypeDt8, "knx-function-dt8-select")) { *response = {0x02}; return true; } const auto color_features = QueryShort(engine_, short_address, DALI_CMD_QUERY_COLOR_TYPE, "knx-function-color-type"); if (!color_features.has_value()) { *response = {0x02}; return true; } response->push_back(static_cast(color_features.value())); } return true; } bool GatewayKnxBridge::handleReg1ScanCommand(const uint8_t* data, size_t len, std::vector* response) { if (len < 5 || response == nullptr) { return false; } commissioning_scan_done_ = false; commissioning_found_ballasts_.clear(); const bool delete_all = data[3] == 1; const bool assign = data[4] == 1; if (assign || delete_all) { DaliBridgeRequest allocate = FunctionRequest( "knx-function-scan-allocate", delete_all ? BridgeOperation::resetAndAllocateShortAddresses : BridgeOperation::allocateAllShortAddresses); allocate.value = DaliValue::Object{{"start", 0}, {"removeAddrFirst", delete_all}}; engine_.execute(allocate); } DaliBridgeRequest search = FunctionRequest("knx-function-scan-search", BridgeOperation::searchAddressRange); search.value = DaliValue::Object{{"start", 0}, {"end", 63}}; const auto search_result = engine_.execute(search); if (search_result.ok) { if (const auto* addresses_value = getObjectValue(search_result.metadata, "addresses")) { if (const auto* addresses = addresses_value->asArray()) { for (const auto& address_value : *addresses) { const auto short_address = address_value.asInt(); if (!short_address.has_value() || short_address.value() < 0 || short_address.value() > 63) { continue; } GatewayKnxCommissioningBallast ballast; ballast.short_address = static_cast(short_address.value()); ballast.high = static_cast( QueryShort(engine_, ballast.short_address, DALI_CMD_QUERY_RANDOM_ADDRESS_H, "knx-function-scan-rand-h") .value_or(0)); ballast.middle = static_cast( QueryShort(engine_, ballast.short_address, DALI_CMD_QUERY_RANDOM_ADDRESS_M, "knx-function-scan-rand-m") .value_or(0)); ballast.low = static_cast( QueryShort(engine_, ballast.short_address, DALI_CMD_QUERY_RANDOM_ADDRESS_L, "knx-function-scan-rand-l") .value_or(0)); commissioning_found_ballasts_.push_back(ballast); } } } } commissioning_scan_done_ = true; response->clear(); return true; } bool GatewayKnxBridge::handleReg1AssignCommand(const uint8_t* data, size_t len, std::vector* response) { if (len < 5 || response == nullptr) { return false; } commissioning_assign_done_ = false; const uint8_t short_address = data[1] == 99 ? 0xff : data[1]; const bool ok = SendRawExt(engine_, DALI_CMD_SPECIAL_INITIALIZE, 0x00, "knx-function-assign-init") && SendRaw(engine_, DALI_CMD_SPECIAL_SEARCHADDRH, data[2], "knx-function-assign-search-h") && SendRaw(engine_, DALI_CMD_SPECIAL_SEARCHADDRM, data[3], "knx-function-assign-search-m") && SendRaw(engine_, DALI_CMD_SPECIAL_SEARCHADDRL, data[4], "knx-function-assign-search-l") && SendRaw(engine_, DALI_CMD_SPECIAL_PROGRAM_SHORT_ADDRESS, short_address == 0xff ? 0xff : DaliComm::toCmdAddr(short_address), "knx-function-assign-program") && SendRaw(engine_, DALI_CMD_SPECIAL_TERMINATE, 0x00, "knx-function-assign-terminate"); commissioning_assign_done_ = true; if (!ok) { ESP_LOGW(kTag, "REG1-Dali assign command failed while programming short address %u", short_address); } response->clear(); return true; } bool GatewayKnxBridge::handleReg1EvgWriteCommand(const uint8_t* data, size_t len, std::vector* response) { if (len < 10 || response == nullptr) { return false; } const uint8_t short_address = data[1]; DaliBridgeRequest settings = FunctionRequest("knx-function-evg-write-settings", BridgeOperation::setAddressSettings); settings.shortAddress = short_address; settings.value = DaliValue::Object{ {"minLevel", Reg1PercentToArc(data[2])}, {"maxLevel", Reg1PercentToArc(data[3])}, {"powerOnLevel", Reg1PercentToArc(data[4])}, {"systemFailureLevel", Reg1PercentToArc(data[5])}, {"fadeTime", static_cast((data[6] >> 4) & 0x0f)}, {"fadeRate", static_cast(data[6] & 0x0f)}, }; const bool settings_ok = engine_.execute(settings).ok; DaliBridgeRequest groups = FunctionRequest("knx-function-evg-write-groups", BridgeOperation::setGroupMask); groups.shortAddress = short_address; groups.value = static_cast(static_cast(data[8]) | (static_cast(data[9]) << 8)); const bool groups_ok = engine_.execute(groups).ok; if (!settings_ok || !groups_ok) { ESP_LOGW(kTag, "REG1-Dali EVG write command failed for short address %u", short_address); } response->clear(); return true; } bool GatewayKnxBridge::handleReg1EvgReadCommand(const uint8_t* data, size_t len, std::vector* response) { if (len < 2 || response == nullptr) { return false; } const uint8_t short_address = data[1]; response->assign(12, 0x00); (*response)[0] = 0x00; uint8_t error_byte = 0; DaliBridgeRequest settings = FunctionRequest("knx-function-evg-read-settings", BridgeOperation::getAddressSettings); settings.shortAddress = short_address; const auto settings_result = engine_.execute(settings); const auto set_level = [&](size_t index, const char* key, uint8_t error_mask) { const auto value = MetadataInt(settings_result, key); if (!settings_result.ok || !value.has_value()) { error_byte |= error_mask; (*response)[index] = 0xff; return; } (*response)[index] = Reg1ArcToPercent(static_cast(std::clamp(value.value(), 0, 255))); }; set_level(1, "minLevel", 0b00000001); set_level(2, "maxLevel", 0b00000010); set_level(3, "powerOnLevel", 0b00000100); set_level(4, "systemFailureLevel", 0b00001000); const auto fade_time = MetadataInt(settings_result, "fadeTime"); const auto fade_rate = MetadataInt(settings_result, "fadeRate"); if (!settings_result.ok || !fade_time.has_value() || !fade_rate.has_value()) { error_byte |= 0b00010000; (*response)[5] = 0xff; } else { (*response)[5] = static_cast(((fade_rate.value() & 0x0f) << 4) | (fade_time.value() & 0x0f)); } DaliBridgeRequest groups = FunctionRequest("knx-function-evg-read-groups", BridgeOperation::getGroupMask); groups.shortAddress = short_address; const auto groups_result = engine_.execute(groups); if (!groups_result.ok || !groups_result.data.has_value()) { error_byte |= 0b11000000; } else { const uint16_t mask = static_cast(groups_result.data.value()); (*response)[7] = static_cast(mask & 0xff); (*response)[8] = static_cast((mask >> 8) & 0xff); } (*response)[9] = error_byte; return true; } bool GatewayKnxBridge::handleReg1SetSceneCommand(const uint8_t* data, size_t len, std::vector* response) { if (len < 10 || response == nullptr) { return false; } const GatewayKnxDaliTarget target = Reg1SceneTarget(data[1]); const uint8_t scene = data[2] & 0x0f; const bool enabled = data[3] != 0; DaliBridgeRequest request = FunctionRequest( enabled ? "knx-function-set-scene" : "knx-function-remove-scene", enabled ? (data[4] == kReg1DeviceTypeDt8 ? BridgeOperation::storeDt8SceneSnapshot : BridgeOperation::setSceneLevel) : BridgeOperation::removeSceneLevel); ApplyTargetToRequest(target, &request); DaliValue::Object value{{"scene", static_cast(scene)}}; if (enabled) { value["brightness"] = static_cast(Reg1PercentToArc(data[6])); if (data[4] == kReg1DeviceTypeDt8) { if (data[5] == kReg1ColorTypeTw) { const uint16_t kelvin = ReadBe16(data + 7); value["colorMode"] = "color_temperature"; value["colorTemperature"] = static_cast(kelvin); } else { value["colorMode"] = "rgb"; value["r"] = static_cast(data[7]); value["g"] = static_cast(data[8]); value["b"] = static_cast(data[9]); } } } request.value = std::move(value); const auto result = engine_.execute(request); if (!result.ok) { ESP_LOGW(kTag, "REG1-Dali set scene command failed for scene %u", scene); } response->clear(); return true; } bool GatewayKnxBridge::handleReg1GetSceneCommand(const uint8_t* data, size_t len, std::vector* response) { if (len < 5 || response == nullptr) { return false; } const uint8_t short_address = data[1]; const uint8_t scene = data[2] & 0x0f; DaliBridgeRequest request = FunctionRequest("knx-function-get-scene", BridgeOperation::getSceneLevel); request.shortAddress = short_address; request.value = DaliValue::Object{{"scene", static_cast(scene)}}; const auto result = engine_.execute(request); if (!result.ok || !result.data.has_value()) { *response = {0xff}; return true; } const uint8_t raw_level = static_cast(std::clamp(result.data.value(), 0, 255)); *response = {static_cast(raw_level == 0xff ? 0xff : Reg1ArcToPercent(raw_level))}; if (raw_level != 0xff && data[3] == kReg1DeviceTypeDt8) { if (data[4] == kReg1ColorTypeTw) { response->resize(3, 0); SendRaw(engine_, DALI_CMD_SPECIAL_SET_DTR0, 0xe2, "knx-function-get-scene-ct-selector"); SendRaw(engine_, DALI_CMD_SPECIAL_DT_SELECT, kReg1DeviceTypeDt8, "knx-function-get-scene-ct-dt-select"); const uint16_t mirek = static_cast( (QueryShort(engine_, short_address, DALI_CMD_QUERY_COLOR_VALUE, "knx-function-get-scene-mirek-h") .value_or(0) << 8) | QueryShort(engine_, short_address, DALI_CMD_QUERY_CONTENT_DTR, "knx-function-get-scene-mirek-l") .value_or(0)); const uint16_t kelvin = mirek == 0 ? 0 : static_cast(1000000U / mirek); (*response)[1] = static_cast((kelvin >> 8) & 0xff); (*response)[2] = static_cast(kelvin & 0xff); } else { response->resize(4, 0); const std::array selectors{0xe9, 0xea, 0xeb}; for (size_t index = 0; index < selectors.size(); ++index) { SendRaw(engine_, DALI_CMD_SPECIAL_SET_DTR0, selectors[index], "knx-function-get-scene-rgb-selector"); SendRaw(engine_, DALI_CMD_SPECIAL_DT_SELECT, kReg1DeviceTypeDt8, "knx-function-get-scene-rgb-dt-select"); (*response)[index + 1] = static_cast( QueryShort(engine_, short_address, DALI_CMD_QUERY_COLOR_VALUE, "knx-function-get-scene-rgb-value") .value_or(0)); } } } return true; } bool GatewayKnxBridge::handleReg1IdentifyCommand(const uint8_t* data, size_t len, std::vector* response) { if (len < 2 || response == nullptr) { return false; } DaliBridgeRequest off = FunctionRequest("knx-function-identify-broadcast-off", BridgeOperation::off); off.metadata["broadcast"] = true; engine_.execute(off); DaliBridgeRequest identify = FunctionRequest("knx-function-identify-recall-max", BridgeOperation::recallMaxLevel); identify.shortAddress = data[1]; engine_.execute(identify); response->clear(); return true; } bool GatewayKnxBridge::handleReg1ScanState(const uint8_t* data, size_t len, std::vector* response) { if (len < 1 || response == nullptr) { return false; } response->clear(); response->push_back(commissioning_scan_done_ ? 1 : 0); if (data[0] == kReg1FunctionScan) { response->push_back(static_cast( std::min(commissioning_found_ballasts_.size(), 0xff))); } return true; } bool GatewayKnxBridge::handleReg1AssignState(const uint8_t* data, size_t len, std::vector* response) { if (len < 1 || response == nullptr) { return false; } *response = {static_cast(commissioning_assign_done_ ? 1 : 0)}; return true; } bool GatewayKnxBridge::handleReg1FoundEvgsState(const uint8_t* data, size_t len, std::vector* response) { if (len < 2 || response == nullptr) { return false; } if (data[1] == 254) { commissioning_found_ballasts_.clear(); response->clear(); return true; } const size_t index = data[1]; response->clear(); response->push_back(index < commissioning_found_ballasts_.size() ? 1 : 0); if (index < commissioning_found_ballasts_.size()) { const auto& ballast = commissioning_found_ballasts_[index]; response->push_back(ballast.high); response->push_back(ballast.middle); response->push_back(ballast.low); response->push_back(ballast.short_address); } return true; } DaliBridgeResult GatewayKnxBridge::executeEtsBindings( uint16_t group_address, const std::vector& bindings, const uint8_t* data, size_t len) { if (bindings.empty()) { return ErrorResult(group_address, "unmapped ETS KNX group address"); } DaliBridgeResult result; result.ok = true; result.metadata["source"] = "ets_database"; result.metadata["groupAddress"] = GatewayKnxGroupAddressString(group_address); result.metadata["bindingCount"] = static_cast(bindings.size()); for (const auto& binding : bindings) { DaliBridgeResult child = executeForDecodedWrite(group_address, binding.data_type, binding.target, data, len); result.ok = result.ok && child.ok; result.results.emplace_back(child.toJson()); } result.data = static_cast(result.results.size()); if (!result.ok) { result.error = "one or more ETS KNX bindings failed"; } return result; } void GatewayKnxBridge::rebuildEtsBindings() { ets_bindings_by_group_address_.clear(); for (const auto& association : config_.ets_associations) { const auto binding = EtsBindingForAssociation(config_.main_group, association); if (!binding.has_value()) { continue; } ets_bindings_by_group_address_[association.group_address].push_back(binding.value()); } } DaliBridgeResult GatewayKnxBridge::executeForDecodedWrite(uint16_t group_address, GatewayKnxDaliDataType data_type, GatewayKnxDaliTarget target, const uint8_t* data, size_t len) { if (target.kind == GatewayKnxDaliTargetKind::kNone) { return ErrorResult(group_address, "missing DALI target"); } switch (data_type) { case GatewayKnxDaliDataType::kSwitch: { if (data == nullptr || len < 1) { return ErrorResult(group_address, "missing DPT1 switch payload"); } DaliBridgeRequest request = RequestForTarget( group_address, target, (data[0] & 0x01) != 0 ? BridgeOperation::on : BridgeOperation::off); return engine_.execute(request); } case GatewayKnxDaliDataType::kBrightness: { if (data == nullptr || len < 1) { return ErrorResult(group_address, "missing DPT5 brightness payload"); } DaliBridgeRequest request = RequestForTarget(group_address, target, BridgeOperation::setBrightnessPercent); request.value = (static_cast(data[0]) * 100.0) / 255.0; return engine_.execute(request); } case GatewayKnxDaliDataType::kColorTemperature: { if (data == nullptr || len < 2) { return ErrorResult(group_address, "missing DPT7 color temperature payload"); } DaliBridgeRequest request = RequestForTarget(group_address, target, BridgeOperation::setColorTemperature); request.value = static_cast(ReadBe16(data)); return engine_.execute(request); } case GatewayKnxDaliDataType::kRgb: { if (data == nullptr || len < 3) { return ErrorResult(group_address, "missing DPT232 RGB payload"); } DaliBridgeRequest request = RequestForTarget(group_address, target, BridgeOperation::setColourRGB); DaliValue::Object rgb; rgb["r"] = static_cast(data[0]); rgb["g"] = static_cast(data[1]); rgb["b"] = static_cast(data[2]); request.value = std::move(rgb); return engine_.execute(request); } case GatewayKnxDaliDataType::kUnknown: default: return ErrorResult(group_address, "unsupported KNX data type"); } } GatewayKnxTpIpRouter::GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHandler handler, std::string openknx_namespace) : bridge_(bridge), handler_(std::move(handler)), openknx_namespace_(std::move(openknx_namespace)) {} GatewayKnxTpIpRouter::~GatewayKnxTpIpRouter() { stop(); } void GatewayKnxTpIpRouter::setConfig(const GatewayKnxConfig& config) { config_ = config; } const GatewayKnxConfig& GatewayKnxTpIpRouter::config() const { return config_; } esp_err_t GatewayKnxTpIpRouter::start(uint32_t task_stack_size, UBaseType_t task_priority) { if (started_ || task_handle_ != nullptr) { return ESP_OK; } if (!config_.ip_router_enabled) { return ESP_ERR_NOT_SUPPORTED; } stop_requested_ = false; last_error_.clear(); if (!configureSocket()) { return ESP_FAIL; } ets_device_ = std::make_unique(openknx_namespace_, config_.individual_address); ets_device_->setFunctionPropertyHandlers( [this](uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len, std::vector* response) { 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) { return bridge_.handleFunctionPropertyState(object_index, property_id, data, len, response); }); if (!configureTpUart()) { ets_device_.reset(); closeSockets(); return ESP_FAIL; } 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; } started_ = true; return ESP_OK; } 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_; } void GatewayKnxTpIpRouter::TaskEntry(void* arg) { static_cast(arg)->taskLoop(); } void GatewayKnxTpIpRouter::taskLoop() { std::array buffer{}; while (!stop_requested_) { sockaddr_in remote{}; socklen_t remote_len = sizeof(remote); const int received = recvfrom(udp_sock_, buffer.data(), buffer.size(), 0, reinterpret_cast(&remote), &remote_len); if (received <= 0) { pollTpUart(); if (ets_device_ != nullptr) { ets_device_->loop(); } if (!stop_requested_) { vTaskDelay(pdMS_TO_TICKS(10)); } continue; } handleUdpDatagram(buffer.data(), static_cast(received), remote); pollTpUart(); if (ets_device_ != nullptr) { ets_device_->loop(); } } finishTask(); } void GatewayKnxTpIpRouter::finishTask() { closeSockets(); ets_device_.reset(); started_ = false; task_handle_ = nullptr; vTaskDelete(nullptr); } void GatewayKnxTpIpRouter::closeSockets() { if (udp_sock_ >= 0) { shutdown(udp_sock_, SHUT_RDWR); close(udp_sock_); udp_sock_ = -1; } if (tp_uart_port_ >= 0) { uart_driver_delete(static_cast(tp_uart_port_)); tp_uart_port_ = -1; } } bool GatewayKnxTpIpRouter::configureSocket() { udp_sock_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (udp_sock_ < 0) { last_error_ = "failed to create KNXnet/IP UDP socket"; return false; } int reuse = 1; setsockopt(udp_sock_, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); 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) { last_error_ = "failed to bind KNXnet/IP UDP socket"; closeSockets(); return false; } timeval timeout{}; timeout.tv_sec = 0; timeout.tv_usec = 20000; setsockopt(udp_sock_, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); if (config_.multicast_enabled) { uint8_t multicast_loop = 0; setsockopt(udp_sock_, IPPROTO_IP, IP_MULTICAST_LOOP, &multicast_loop, sizeof(multicast_loop)); ip_mreq mreq{}; mreq.imr_multiaddr.s_addr = inet_addr(config_.multicast_address.c_str()); mreq.imr_interface.s_addr = htonl(INADDR_ANY); if (setsockopt(udp_sock_, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) { ESP_LOGW(kTag, "failed to join KNX multicast group %s", config_.multicast_address.c_str()); } } return true; } bool GatewayKnxTpIpRouter::configureTpUart() { const auto& serial = config_.tp_uart; if (serial.uart_port < 0 || serial.uart_port > 2) { last_error_ = "invalid KNX TP-UART port"; return false; } uart_config_t uart_config{}; uart_config.baud_rate = static_cast(serial.baudrate); uart_config.data_bits = UART_DATA_8_BITS; uart_config.parity = UART_PARITY_EVEN; uart_config.stop_bits = UART_STOP_BITS_1; uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; uart_config.source_clk = UART_SCLK_DEFAULT; const uart_port_t uart_port = static_cast(serial.uart_port); if (uart_param_config(uart_port, &uart_config) != ESP_OK) { last_error_ = "failed to configure KNX TP-UART parameters"; return false; } if (uart_set_pin(uart_port, serial.tx_pin, serial.rx_pin, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE) != ESP_OK) { last_error_ = "failed to configure KNX TP-UART pins"; return false; } if (uart_driver_install(uart_port, serial.rx_buffer_size, serial.tx_buffer_size, 0, nullptr, 0) != ESP_OK) { last_error_ = "failed to install KNX TP-UART driver"; return false; } tp_uart_port_ = serial.uart_port; return initializeTpUart(); } bool GatewayKnxTpIpRouter::initializeTpUart() { if (tp_uart_port_ < 0) { return false; } const uart_port_t uart_port = static_cast(tp_uart_port_); tp_rx_frame_.clear(); tp_last_sent_telegram_.clear(); tp_uart_last_byte_tick_ = 0; tp_uart_extended_frame_ = false; tp_uart_online_ = false; uart_flush_input(uart_port); const uint8_t reset_request = kTpUartResetRequest; if (uart_write_bytes(uart_port, &reset_request, 1) != 1) { last_error_ = "failed to send KNX TP-UART reset request"; return false; } const TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(1500); bool saw_reset = false; std::array buffer{}; while (xTaskGetTickCount() < deadline) { const int read = uart_read_bytes(uart_port, buffer.data(), buffer.size(), pdMS_TO_TICKS(config_.tp_uart.read_timeout_ms)); if (read <= 0) { continue; } for (int index = 0; index < read; ++index) { const uint8_t byte = buffer[static_cast(index)]; if (!saw_reset) { if (byte == kTpUartResetIndication) { saw_reset = true; const std::array set_address{ kTpUartSetAddressRequest, static_cast((effectiveIndividualAddress() >> 8) & 0xff), static_cast(effectiveIndividualAddress() & 0xff), }; uart_write_bytes(uart_port, set_address.data(), set_address.size()); const uint8_t state_request = kTpUartStateRequest; uart_write_bytes(uart_port, &state_request, 1); } continue; } if ((byte & kTpUartStateIndicationMask) == kTpUartStateIndicationMask) { tp_uart_online_ = true; return true; } } } last_error_ = saw_reset ? "timed out waiting for KNX TP-UART state indication" : "timed out waiting for KNX TP-UART reset indication"; return false; } void GatewayKnxTpIpRouter::handleUdpDatagram(const uint8_t* data, size_t len, const sockaddr_in& remote) { uint16_t service = 0; uint16_t total_len = 0; if (!ParseKnxNetIpHeader(data, len, &service, &total_len)) { return; } const uint8_t* body = data + 6; const size_t body_len = total_len - 6; switch (service) { case kServiceRoutingIndication: if (config_.multicast_enabled) { handleRoutingIndication(body, body_len); } break; case kServiceTunnellingRequest: if (config_.tunnel_enabled) { handleTunnellingRequest(body, body_len, remote); } break; case kServiceConnectRequest: if (config_.tunnel_enabled) { handleConnectRequest(body, body_len, remote); } break; case kServiceConnectionStateRequest: handleConnectionStateRequest(body, body_len, remote); break; case kServiceDisconnectRequest: handleDisconnectRequest(body, body_len, remote); break; default: break; } } void GatewayKnxTpIpRouter::handleRoutingIndication(const uint8_t* body, size_t len) { if (body == nullptr || len == 0) { return; } const DaliBridgeResult result = handler_(body, len); if (!result.ok && !result.error.empty()) { ESP_LOGD(kTag, "KNX routing indication ignored: %s", result.error.c_str()); } forwardCemiToTp(body, len); } void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* body, size_t len, const sockaddr_in& remote) { if (body == nullptr || len < 5 || body[0] != 0x04) { return; } const uint8_t channel_id = body[1]; const uint8_t sequence = body[2]; if (!tunnel_connected_ || channel_id != tunnel_channel_id_) { sendTunnellingAck(channel_id, sequence, kKnxErrorConnectionId, remote); return; } if (sequence != expected_tunnel_sequence_) { sendTunnellingAck(channel_id, sequence, kKnxErrorSequenceNumber, remote); return; } expected_tunnel_sequence_ = static_cast((expected_tunnel_sequence_ + 1) & 0xff); sendTunnellingAck(channel_id, sequence, kKnxNoError, remote); const uint8_t* cemi = body + 4; const size_t cemi_len = len - 4; const bool consumed_by_openknx = handleOpenKnxTunnelFrame(cemi, cemi_len); if (consumed_by_openknx) { return; } const DaliBridgeResult result = handler_(cemi, cemi_len); if (!result.ok && !result.error.empty()) { ESP_LOGD(kTag, "KNX tunnel frame not routed to DALI: %s", result.error.c_str()); } forwardCemiToTp(cemi, cemi_len); } void GatewayKnxTpIpRouter::handleConnectRequest(const uint8_t* body, size_t len, const sockaddr_in& remote) { if (body == nullptr || len < 20) { return; } const size_t cri_offset = 16; if (body[cri_offset] < 4 || body[cri_offset + 1] != kKnxConnectionTypeTunnel || body[cri_offset + 2] != kKnxTunnelLayerLink) { sendConnectResponse(0, kKnxErrorConnectionType, remote); return; } if (tunnel_connected_) { sendConnectResponse(0, kKnxErrorNoMoreConnections, remote); return; } tunnel_connected_ = true; expected_tunnel_sequence_ = 0; tunnel_send_sequence_ = 0; tunnel_remote_ = remote; sendConnectResponse(tunnel_channel_id_, kKnxNoError, remote); } void GatewayKnxTpIpRouter::handleConnectionStateRequest(const uint8_t* body, size_t len, const sockaddr_in& remote) { if (body == nullptr || len < 2) { return; } const uint8_t channel_id = body[0]; sendConnectionStateResponse( channel_id, tunnel_connected_ && channel_id == tunnel_channel_id_ ? kKnxNoError : kKnxErrorConnectionId, remote); } void GatewayKnxTpIpRouter::handleDisconnectRequest(const uint8_t* body, size_t len, const sockaddr_in& remote) { if (body == nullptr || len < 2) { return; } const uint8_t channel_id = body[0]; const uint8_t status = tunnel_connected_ && channel_id == tunnel_channel_id_ ? kKnxNoError : kKnxErrorConnectionId; if (status == kKnxNoError) { tunnel_connected_ = false; expected_tunnel_sequence_ = 0; tunnel_send_sequence_ = 0; } sendDisconnectResponse(channel_id, status, remote); } void GatewayKnxTpIpRouter::sendTunnellingAck(uint8_t channel_id, uint8_t sequence, uint8_t status, const sockaddr_in& remote) { const std::vector body{0x04, channel_id, sequence, status}; const auto packet = KnxNetIpPacket(kServiceTunnellingAck, body); SendAll(udp_sock_, packet.data(), packet.size(), remote); } void GatewayKnxTpIpRouter::sendTunnelIndication(const uint8_t* data, size_t len) { if (!tunnel_connected_ || udp_sock_ < 0 || data == nullptr || len == 0) { return; } std::vector body; body.reserve(4 + len); body.push_back(0x04); body.push_back(tunnel_channel_id_); body.push_back(tunnel_send_sequence_++); body.push_back(0x00); body.insert(body.end(), data, data + len); const auto packet = KnxNetIpPacket(kServiceTunnellingRequest, body); SendAll(udp_sock_, packet.data(), packet.size(), tunnel_remote_); } void GatewayKnxTpIpRouter::sendConnectionStateResponse(uint8_t channel_id, uint8_t status, const sockaddr_in& remote) { const std::vector body{channel_id, status}; const auto packet = KnxNetIpPacket(kServiceConnectionStateResponse, body); SendAll(udp_sock_, packet.data(), packet.size(), remote); } void GatewayKnxTpIpRouter::sendDisconnectResponse(uint8_t channel_id, uint8_t status, const sockaddr_in& remote) { const std::vector body{channel_id, status}; const auto packet = KnxNetIpPacket(kServiceDisconnectResponse, body); SendAll(udp_sock_, packet.data(), packet.size(), remote); } void GatewayKnxTpIpRouter::sendConnectResponse(uint8_t channel_id, uint8_t status, const sockaddr_in& remote) { std::vector body; body.reserve(16); body.push_back(channel_id); body.push_back(status); const auto data_endpoint = HpaiForRemote(remote); body.insert(body.end(), data_endpoint.begin(), data_endpoint.end()); body.push_back(0x04); body.push_back(kKnxConnectionTypeTunnel); body.push_back(static_cast((effectiveTunnelAddress() >> 8) & 0xff)); body.push_back(static_cast(effectiveTunnelAddress() & 0xff)); const auto packet = KnxNetIpPacket(kServiceConnectResponse, body); SendAll(udp_sock_, packet.data(), packet.size(), remote); } void GatewayKnxTpIpRouter::sendRoutingIndication(const uint8_t* data, size_t len) { if (!config_.multicast_enabled || udp_sock_ < 0 || data == nullptr || len == 0) { return; } sockaddr_in remote{}; remote.sin_family = AF_INET; remote.sin_port = htons(config_.udp_port); remote.sin_addr.s_addr = inet_addr(config_.multicast_address.c_str()); const std::vector body(data, data + len); const auto packet = KnxNetIpPacket(kServiceRoutingIndication, body); SendAll(udp_sock_, packet.data(), packet.size(), remote); } bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t len) { if (ets_device_ == nullptr) { return false; } const bool consumed = ets_device_->handleTunnelFrame( data, len, [this](const uint8_t* response, size_t response_len) { sendTunnelIndication(response, response_len); }); syncOpenKnxConfigFromDevice(); return consumed; } void GatewayKnxTpIpRouter::syncOpenKnxConfigFromDevice() { if (ets_device_ == nullptr) { return; } const auto snapshot = ets_device_->snapshot(); 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::effectiveIndividualAddress() const { if (ets_device_ != nullptr) { const uint16_t address = ets_device_->individualAddress(); if (address != 0 && address != 0xffff) { return address; } } return config_.individual_address; } uint16_t GatewayKnxTpIpRouter::effectiveTunnelAddress() const { if (ets_device_ != nullptr) { const uint16_t address = ets_device_->tunnelClientAddress(); if (address != 0 && address != 0xffff) { return address; } } uint16_t device = static_cast((config_.individual_address & 0x00ff) + 1); if (device == 0 || device > 0xff) { device = 1; } return static_cast((config_.individual_address & 0xff00) | device); } void GatewayKnxTpIpRouter::pollTpUart() { if (tp_uart_port_ < 0) { return; } std::array buffer{}; const int read = uart_read_bytes(static_cast(tp_uart_port_), buffer.data(), buffer.size(), 0); if (read <= 0) { return; } for (int index = 0; index < read; ++index) { const uint8_t byte = buffer[static_cast(index)]; if (tp_rx_frame_.empty()) { if (IsTpUartControlByte(byte)) { handleTpUartControlByte(byte); continue; } if (byte == 0xcb || (byte & 0x17U) == 0x13U) { continue; } } const TickType_t now = xTaskGetTickCount(); if (!tp_rx_frame_.empty() && tp_uart_last_byte_tick_ != 0 && now - tp_uart_last_byte_tick_ > pdMS_TO_TICKS(1000)) { tp_rx_frame_.clear(); } if (tp_rx_frame_.empty()) { if (IsTpUartFrameStart(byte, &tp_uart_extended_frame_)) { tp_rx_frame_.push_back(byte); tp_uart_last_byte_tick_ = now; } continue; } tp_rx_frame_.push_back(byte); tp_uart_last_byte_tick_ = now; const size_t expected = ExpectedTpFrameSize(tp_rx_frame_.data(), tp_rx_frame_.size()); if (expected == 0) { continue; } if (tp_rx_frame_.size() == expected) { const uint8_t ack = kTpUartAckInfo; uart_write_bytes(static_cast(tp_uart_port_), &ack, 1); handleTpTelegram(tp_rx_frame_.data(), tp_rx_frame_.size()); tp_rx_frame_.clear(); } else if (tp_rx_frame_.size() > expected || tp_rx_frame_.size() > 263U) { tp_rx_frame_.clear(); } } } void GatewayKnxTpIpRouter::handleTpUartControlByte(uint8_t byte) { if (byte == kTpUartResetIndication) { ESP_LOGW(kTag, "KNX TP-UART reset indication received; marking link offline"); tp_uart_online_ = false; return; } if (byte == kTpUartBusy) { last_error_ = "KNX TP-UART bus busy"; ESP_LOGW(kTag, "%s", last_error_.c_str()); return; } if (byte == kTpUartLDataConfirmNegative) { last_error_ = "KNX TP-UART negative confirmation"; ESP_LOGW(kTag, "%s", last_error_.c_str()); return; } if (byte == kTpUartLDataConfirmPositive) { return; } if ((byte & kTpUartStateIndicationMask) == kTpUartStateIndicationMask) { tp_uart_online_ = true; } } void GatewayKnxTpIpRouter::handleTpTelegram(const uint8_t* data, size_t len) { if (data == nullptr || len == 0) { return; } const std::vector telegram(data, data + len); if (!tp_last_sent_telegram_.empty() && TpTelegramEqualsIgnoringRepeatBit(telegram, tp_last_sent_telegram_)) { tp_last_sent_telegram_.clear(); return; } const auto cemi = TpTelegramToCemi(data, len); if (!cemi.has_value()) { return; } const DaliBridgeResult result = handler_(cemi->data(), cemi->size()); if (!result.ok && !result.error.empty()) { ESP_LOGD(kTag, "KNX TP frame not routed to DALI: %s", result.error.c_str()); } sendTunnelIndication(cemi->data(), cemi->size()); sendRoutingIndication(cemi->data(), cemi->size()); } void GatewayKnxTpIpRouter::forwardCemiToTp(const uint8_t* data, size_t len) { if (tp_uart_port_ < 0 || data == nullptr || len == 0 || !tp_uart_online_) { return; } const auto telegram = CemiToTpTelegram(data, len); if (!telegram.has_value()) { return; } tp_last_sent_telegram_ = *telegram; const auto wrapped = WrapTpUartTelegram(*telegram); uart_write_bytes(static_cast(tp_uart_port_), wrapped.data(), wrapped.size()); } } // namespace gateway