diff --git a/README.md b/README.md index f3562bd..d1da95f 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This folder hosts the native ESP-IDF C++ rewrite of the Lua DALI gateway. - `dali_domain/`: native DALI domain facade over `dali_cpp` and raw frame sinks. - `gateway_cache/`: DALI scene/group/settings/runtime cache used by controller reconciliation and protocol bridges. - `gateway_bridge/`: per-channel bridge provisioning, command execution, protocol startup, and HTTP bridge actions. - - `openknx_idf/`: ESP-IDF port layer for the OpenKNX `gateway/knx` and `gateway/tpuart` submodules, including NVS-backed OpenKNX memory, UDP multicast/unicast plumbing, and a native TP-UART interface without the Arduino framework. + - `openknx_idf/`: ESP-IDF port layer for the OpenKNX `gateway/knx` and `gateway/tpuart` submodules, including NVS-backed OpenKNX memory, ETS cEMI programming support, UDP multicast/unicast plumbing, and a native TP-UART interface without the Arduino framework. - `gateway_modbus/`: gateway-owned Modbus TCP/RTU/ASCII config, generated DALI point tables, and provisioned Modbus model override dispatch. - `gateway_bacnet/`: BACnet/IP server adapter backed by bacnet-stack, including the gateway-owned BACnet bridge model adapter. - `gateway_ble/`: NimBLE GATT bridge for BLE transport parity on `FFF1`/`FFF2`/`FFF3`, including raw DALI notifications. diff --git a/apps/gateway/sdkconfig b/apps/gateway/sdkconfig index f91984c..f094324 100644 --- a/apps/gateway/sdkconfig +++ b/apps/gateway/sdkconfig @@ -666,7 +666,20 @@ CONFIG_GATEWAY_MODBUS_TCP_PORT=1502 CONFIG_GATEWAY_MODBUS_UNIT_ID=1 CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED=y # CONFIG_GATEWAY_START_BACNET_BRIDGE_ENABLED is not set -# CONFIG_GATEWAY_KNX_BRIDGE_SUPPORTED is not set +CONFIG_GATEWAY_KNX_BRIDGE_SUPPORTED=y +CONFIG_GATEWAY_START_KNX_BRIDGE_ENABLED=y +CONFIG_GATEWAY_KNX_MAIN_GROUP=0 +CONFIG_GATEWAY_KNX_TUNNEL_ENABLED=y +CONFIG_GATEWAY_KNX_MULTICAST_ENABLED=y +CONFIG_GATEWAY_KNX_UDP_PORT=3671 +CONFIG_GATEWAY_KNX_MULTICAST_ADDRESS="224.0.23.12" +CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS=4353 +CONFIG_GATEWAY_KNX_TP_UART_PORT=0 +CONFIG_GATEWAY_KNX_TP_TX_PIN=-1 +CONFIG_GATEWAY_KNX_TP_RX_PIN=-1 +CONFIG_GATEWAY_KNX_TP_BAUDRATE=19200 +CONFIG_GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE=8192 +CONFIG_GATEWAY_BRIDGE_KNX_TASK_PRIORITY=5 CONFIG_GATEWAY_CLOUD_BRIDGE_SUPPORTED=y # CONFIG_GATEWAY_START_CLOUD_BRIDGE_ENABLED is not set CONFIG_GATEWAY_BRIDGE_MODBUS_TASK_STACK_SIZE=6144 @@ -675,16 +688,7 @@ CONFIG_GATEWAY_BRIDGE_BACNET_TASK_STACK_SIZE=8192 CONFIG_GATEWAY_BRIDGE_BACNET_TASK_PRIORITY=5 CONFIG_GATEWAY_USB_STARTUP_DEBUG_JTAG=y # CONFIG_GATEWAY_USB_STARTUP_SETUP_SERIAL is not set -CONFIG_GATEWAY_485_CONTROL_ENABLED=y -CONFIG_GATEWAY_485_CONTROL_BAUDRATE=9600 -CONFIG_GATEWAY_485_CONTROL_TX_PIN=-1 -CONFIG_GATEWAY_485_CONTROL_RX_PIN=-1 -CONFIG_GATEWAY_485_CONTROL_RX_BUFFER=256 -CONFIG_GATEWAY_485_CONTROL_TX_BUFFER=256 -CONFIG_GATEWAY_485_CONTROL_READ_TIMEOUT_MS=20 -CONFIG_GATEWAY_485_CONTROL_WRITE_TIMEOUT_MS=20 -CONFIG_GATEWAY_485_CONTROL_TASK_STACK_SIZE=4096 -CONFIG_GATEWAY_485_CONTROL_TASK_PRIORITY=4 +# CONFIG_GATEWAY_485_CONTROL_ENABLED is not set # end of Gateway Startup Services # diff --git a/apps/gateway/sdkconfig.old b/apps/gateway/sdkconfig.old index 8dd48a0..f8cb68f 100644 --- a/apps/gateway/sdkconfig.old +++ b/apps/gateway/sdkconfig.old @@ -666,6 +666,7 @@ CONFIG_GATEWAY_MODBUS_TCP_PORT=1502 CONFIG_GATEWAY_MODBUS_UNIT_ID=1 CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED=y # CONFIG_GATEWAY_START_BACNET_BRIDGE_ENABLED is not set +# CONFIG_GATEWAY_KNX_BRIDGE_SUPPORTED is not set CONFIG_GATEWAY_CLOUD_BRIDGE_SUPPORTED=y # CONFIG_GATEWAY_START_CLOUD_BRIDGE_ENABLED is not set CONFIG_GATEWAY_BRIDGE_MODBUS_TASK_STACK_SIZE=6144 @@ -744,6 +745,7 @@ CONFIG_COMPILER_ORPHAN_SECTIONS_WARNING=y # # CONFIG_APPTRACE_DEST_JTAG is not set CONFIG_APPTRACE_DEST_NONE=y +# CONFIG_APPTRACE_DEST_UART0 is not set # CONFIG_APPTRACE_DEST_UART1 is not set # CONFIG_APPTRACE_DEST_UART2 is not set # CONFIG_APPTRACE_DEST_USB_CDC is not set @@ -1787,18 +1789,15 @@ CONFIG_ESP_MAIN_TASK_AFFINITY_CPU0=y # CONFIG_ESP_MAIN_TASK_AFFINITY_NO_AFFINITY is not set CONFIG_ESP_MAIN_TASK_AFFINITY=0x0 CONFIG_ESP_MINIMAL_SHARED_STACK_SIZE=2048 -CONFIG_ESP_CONSOLE_UART_DEFAULT=y +# CONFIG_ESP_CONSOLE_UART_DEFAULT is not set # CONFIG_ESP_CONSOLE_USB_CDC is not set -# CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG is not set +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y # CONFIG_ESP_CONSOLE_UART_CUSTOM is not set # CONFIG_ESP_CONSOLE_NONE is not set -# CONFIG_ESP_CONSOLE_SECONDARY_NONE is not set -CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG=y +CONFIG_ESP_CONSOLE_SECONDARY_NONE=y CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG_ENABLED=y -CONFIG_ESP_CONSOLE_UART=y -CONFIG_ESP_CONSOLE_UART_NUM=0 -CONFIG_ESP_CONSOLE_ROM_SERIAL_PORT_NUM=0 -CONFIG_ESP_CONSOLE_UART_BAUDRATE=115200 +CONFIG_ESP_CONSOLE_UART_NUM=-1 +CONFIG_ESP_CONSOLE_ROM_SERIAL_PORT_NUM=4 CONFIG_ESP_INT_WDT=y CONFIG_ESP_INT_WDT_TIMEOUT_MS=300 CONFIG_ESP_INT_WDT_CHECK_CPU1=y diff --git a/components/gateway_bridge/src/gateway_bridge.cpp b/components/gateway_bridge/src/gateway_bridge.cpp index fb248a7..94219b8 100644 --- a/components/gateway_bridge/src/gateway_bridge.cpp +++ b/components/gateway_bridge/src/gateway_bridge.cpp @@ -1276,7 +1276,8 @@ struct GatewayBridgeService::ChannelRuntime { return result; } return knx->handleCemiFrame(data, len); - }); + }, + openKnxNamespace()); if (knx_config.has_value()) { knx->setConfig(knx_config.value()); knx_router->setConfig(knx_config.value()); @@ -1308,10 +1309,15 @@ struct GatewayBridgeService::ChannelRuntime { return; } const auto snapshot = openknx::LoadEtsMemorySnapshot(openKnxNamespace()); - if (snapshot.associations.empty()) { + const bool has_downloaded_address = snapshot.individual_address != 0 && + snapshot.individual_address != 0xffff; + if (!snapshot.configured && !has_downloaded_address && snapshot.associations.empty()) { return; } GatewayKnxConfig updated = active_config.value(); + if (has_downloaded_address) { + updated.individual_address = snapshot.individual_address; + } updated.ets_associations.clear(); updated.ets_associations.reserve(snapshot.associations.size()); for (const auto& association : snapshot.associations) { @@ -1319,9 +1325,9 @@ struct GatewayBridgeService::ChannelRuntime { association.group_address, association.group_object_number}); } knx_config = std::move(updated); - ESP_LOGI(kTag, "gateway=%u loaded %u OpenKNX ETS associations from NVS namespace %s", - channel.gateway_id, static_cast(snapshot.associations.size()), - openKnxNamespace().c_str()); + ESP_LOGI(kTag, "gateway=%u loaded OpenKNX ETS address=0x%04x associations=%u from NVS namespace %s", + channel.gateway_id, snapshot.individual_address, + static_cast(snapshot.associations.size()), openKnxNamespace().c_str()); } std::optional diagnosticSnapshotLocked(int short_address, diff --git a/components/gateway_knx/include/gateway_knx.hpp b/components/gateway_knx/include/gateway_knx.hpp index d7bcf69..54f8b33 100644 --- a/components/gateway_knx/include/gateway_knx.hpp +++ b/components/gateway_knx/include/gateway_knx.hpp @@ -13,12 +13,17 @@ #include #include #include +#include #include #include #include namespace gateway { +namespace openknx { +class EtsDeviceRuntime; +} + constexpr uint16_t kGatewayKnxDefaultUdpPort = 3671; constexpr const char* kGatewayKnxDefaultMulticastAddress = "224.0.23.12"; constexpr uint32_t kGatewayKnxDefaultTpBaudrate = 19200; @@ -96,6 +101,13 @@ struct GatewayKnxDaliBinding { GatewayKnxDaliTarget target; }; +struct GatewayKnxCommissioningBallast { + uint8_t high{0}; + uint8_t middle{0}; + uint8_t low{0}; + uint8_t short_address{0xff}; +}; + std::optional GatewayKnxConfigFromValue(const DaliValue* value); DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config); @@ -122,6 +134,12 @@ class GatewayKnxBridge { DaliBridgeResult handleCemiFrame(const uint8_t* data, size_t len); DaliBridgeResult handleGroupWrite(uint16_t group_address, const uint8_t* data, size_t len); + bool handleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id, + const uint8_t* data, size_t len, + std::vector* response); + bool handleFunctionPropertyState(uint8_t object_index, uint8_t property_id, + const uint8_t* data, size_t len, + std::vector* response); private: DaliBridgeResult executeForDecodedWrite(uint16_t group_address, @@ -133,16 +151,43 @@ class GatewayKnxBridge { const uint8_t* data, size_t len); void rebuildEtsBindings(); + bool handleReg1TypeCommand(const uint8_t* data, size_t len, + std::vector* response); + bool handleReg1ScanCommand(const uint8_t* data, size_t len, + std::vector* response); + bool handleReg1AssignCommand(const uint8_t* data, size_t len, + std::vector* response); + bool handleReg1EvgWriteCommand(const uint8_t* data, size_t len, + std::vector* response); + bool handleReg1EvgReadCommand(const uint8_t* data, size_t len, + std::vector* response); + bool handleReg1SetSceneCommand(const uint8_t* data, size_t len, + std::vector* response); + bool handleReg1GetSceneCommand(const uint8_t* data, size_t len, + std::vector* response); + bool handleReg1IdentifyCommand(const uint8_t* data, size_t len, + std::vector* response); + bool handleReg1ScanState(const uint8_t* data, size_t len, + std::vector* response); + bool handleReg1AssignState(const uint8_t* data, size_t len, + std::vector* response); + bool handleReg1FoundEvgsState(const uint8_t* data, size_t len, + std::vector* response); + DaliBridgeEngine& engine_; GatewayKnxConfig config_; std::map> ets_bindings_by_group_address_; + bool commissioning_scan_done_{true}; + bool commissioning_assign_done_{true}; + std::vector commissioning_found_ballasts_; }; class GatewayKnxTpIpRouter { public: using CemiFrameHandler = std::function; - GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHandler handler); + GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHandler handler, + std::string openknx_namespace = "openknx"); ~GatewayKnxTpIpRouter(); void setConfig(const GatewayKnxConfig& config); @@ -179,6 +224,10 @@ class GatewayKnxTpIpRouter { void sendConnectResponse(uint8_t channel_id, uint8_t status, const ::sockaddr_in& remote); void sendRoutingIndication(const uint8_t* data, size_t len); + bool handleOpenKnxTunnelFrame(const uint8_t* data, size_t len); + void syncOpenKnxConfigFromDevice(); + uint16_t effectiveIndividualAddress() const; + uint16_t effectiveTunnelAddress() const; void pollTpUart(); void handleTpUartControlByte(uint8_t byte); void handleTpTelegram(const uint8_t* data, size_t len); @@ -186,7 +235,9 @@ class GatewayKnxTpIpRouter { GatewayKnxBridge& bridge_; CemiFrameHandler handler_; + std::string openknx_namespace_; GatewayKnxConfig config_; + std::unique_ptr ets_device_; TaskHandle_t task_handle_{nullptr}; std::atomic_bool stop_requested_{false}; std::atomic_bool started_{false}; diff --git a/components/gateway_knx/src/gateway_knx.cpp b/components/gateway_knx/src/gateway_knx.cpp index 70b7f1a..c932e57 100644 --- a/components/gateway_knx/src/gateway_knx.cpp +++ b/components/gateway_knx/src/gateway_knx.cpp @@ -1,9 +1,11 @@ #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 @@ -62,6 +64,20 @@ 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}; @@ -293,6 +309,88 @@ std::optional DecodeCemiGroupWrite(const uint8_t* data, size_ 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) { @@ -923,6 +1021,421 @@ DaliBridgeResult GatewayKnxBridge::handleGroupWrite(uint16_t group_address, cons 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) { @@ -1011,8 +1524,11 @@ DaliBridgeResult GatewayKnxBridge::executeForDecodedWrite(uint16_t group_address } } -GatewayKnxTpIpRouter::GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHandler handler) - : bridge_(bridge), handler_(std::move(handler)) {} +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(); } @@ -1032,7 +1548,19 @@ esp_err_t GatewayKnxTpIpRouter::start(uint32_t task_stack_size, UBaseType_t task 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; } @@ -1075,6 +1603,9 @@ void GatewayKnxTpIpRouter::taskLoop() { reinterpret_cast(&remote), &remote_len); if (received <= 0) { pollTpUart(); + if (ets_device_ != nullptr) { + ets_device_->loop(); + } if (!stop_requested_) { vTaskDelay(pdMS_TO_TICKS(10)); } @@ -1082,12 +1613,16 @@ void GatewayKnxTpIpRouter::taskLoop() { } 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); @@ -1209,8 +1744,8 @@ bool GatewayKnxTpIpRouter::initializeTpUart() { saw_reset = true; const std::array set_address{ kTpUartSetAddressRequest, - static_cast((config_.individual_address >> 8) & 0xff), - static_cast(config_.individual_address & 0xff), + 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; @@ -1296,6 +1831,10 @@ void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* body, size_t l 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()); @@ -1400,8 +1939,8 @@ void GatewayKnxTpIpRouter::sendConnectResponse(uint8_t channel_id, uint8_t statu body.insert(body.end(), data_endpoint.begin(), data_endpoint.end()); body.push_back(0x04); body.push_back(kKnxConnectionTypeTunnel); - body.push_back(static_cast((config_.individual_address >> 8) & 0xff)); - body.push_back(static_cast(config_.individual_address & 0xff)); + 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); } @@ -1419,6 +1958,79 @@ void GatewayKnxTpIpRouter::sendRoutingIndication(const uint8_t* data, size_t len 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; diff --git a/components/openknx_idf/CMakeLists.txt b/components/openknx_idf/CMakeLists.txt index 633538c..322b8ac 100644 --- a/components/openknx_idf/CMakeLists.txt +++ b/components/openknx_idf/CMakeLists.txt @@ -29,6 +29,7 @@ idf_component_register( SRCS "src/arduino_compat.cpp" "src/esp_idf_platform.cpp" + "src/ets_device_runtime.cpp" "src/ets_memory_loader.cpp" "src/tpuart_uart_interface.cpp" ${OPENKNX_SRCS} @@ -54,6 +55,7 @@ target_compile_definitions(${COMPONENT_LIB} PUBLIC KNX_FLASH_SIZE=4096 KNX_NO_AUTOMATIC_GLOBAL_INSTANCE KNX_NO_SPI + USE_CEMI_SERVER ) target_compile_options(${COMPONENT_LIB} PRIVATE diff --git a/components/openknx_idf/include/openknx_idf/ets_device_runtime.h b/components/openknx_idf/include/openknx_idf/ets_device_runtime.h new file mode 100644 index 0000000..36d8872 --- /dev/null +++ b/components/openknx_idf/include/openknx_idf/ets_device_runtime.h @@ -0,0 +1,60 @@ +#pragma once + +#include "openknx_idf/esp_idf_platform.h" +#include "openknx_idf/ets_memory_loader.h" + +#include "knx/bau07B0.h" +#include "knx/cemi_frame.h" + +#include +#include +#include +#include +#include + +namespace gateway::openknx { + +class EtsDeviceRuntime { + public: + using CemiFrameSender = std::function; + using FunctionPropertyHandler = std::function* response)>; + + EtsDeviceRuntime(std::string nvs_namespace, uint16_t fallback_individual_address); + ~EtsDeviceRuntime(); + + uint16_t individualAddress() const; + uint16_t tunnelClientAddress() const; + bool configured() const; + EtsMemorySnapshot snapshot() const; + + void setFunctionPropertyHandlers(FunctionPropertyHandler command_handler, + FunctionPropertyHandler state_handler); + + bool handleTunnelFrame(const uint8_t* data, size_t len, CemiFrameSender sender); + void loop(); + + private: + static void EmitTunnelFrame(CemiFrame& frame, void* context); + static bool HandleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id, + uint8_t length, uint8_t* data, + uint8_t* result_data, uint8_t& result_length); + static bool HandleFunctionPropertyState(uint8_t object_index, uint8_t property_id, + uint8_t length, uint8_t* data, + uint8_t* result_data, uint8_t& result_length); + static uint16_t DefaultTunnelClientAddress(uint16_t individual_address); + static bool DispatchFunctionProperty(FunctionPropertyHandler* handler, uint8_t object_index, + uint8_t property_id, uint8_t length, uint8_t* data, + uint8_t* result_data, uint8_t& result_length); + bool shouldConsumeTunnelFrame(CemiFrame& frame) const; + + std::string nvs_namespace_; + EspIdfPlatform platform_; + Bau07B0 device_; + CemiFrameSender sender_; + FunctionPropertyHandler command_handler_; + FunctionPropertyHandler state_handler_; +}; + +} // namespace gateway::openknx diff --git a/components/openknx_idf/include/openknx_idf/ets_memory_loader.h b/components/openknx_idf/include/openknx_idf/ets_memory_loader.h index e838545..f3b1e0e 100644 --- a/components/openknx_idf/include/openknx_idf/ets_memory_loader.h +++ b/components/openknx_idf/include/openknx_idf/ets_memory_loader.h @@ -13,6 +13,7 @@ struct EtsAssociation { struct EtsMemorySnapshot { bool configured{false}; + uint16_t individual_address{0}; std::vector associations; }; diff --git a/components/openknx_idf/include/openknx_idf/openknx_idf.h b/components/openknx_idf/include/openknx_idf/openknx_idf.h index d036c53..e8023a3 100644 --- a/components/openknx_idf/include/openknx_idf/openknx_idf.h +++ b/components/openknx_idf/include/openknx_idf/openknx_idf.h @@ -1,6 +1,7 @@ #pragma once #include "openknx_idf/ets_memory_loader.h" +#include "openknx_idf/ets_device_runtime.h" #include "openknx_idf/esp_idf_platform.h" #include "openknx_idf/tpuart_uart_interface.h" diff --git a/components/openknx_idf/src/ets_device_runtime.cpp b/components/openknx_idf/src/ets_device_runtime.cpp new file mode 100644 index 0000000..222834a --- /dev/null +++ b/components/openknx_idf/src/ets_device_runtime.cpp @@ -0,0 +1,227 @@ +#include "openknx_idf/ets_device_runtime.h" + +#include "knx/cemi_server.h" +#include "knx/property.h" + +#include +#include +#include +#include + +namespace gateway::openknx { +namespace { + +thread_local EtsDeviceRuntime* active_function_property_runtime = nullptr; + +class ActiveFunctionPropertyRuntimeScope { + public: + explicit ActiveFunctionPropertyRuntimeScope(EtsDeviceRuntime* runtime) + : previous_(active_function_property_runtime) { + active_function_property_runtime = runtime; + } + + ~ActiveFunctionPropertyRuntimeScope() { active_function_property_runtime = previous_; } + + private: + EtsDeviceRuntime* previous_; +}; + +constexpr uint16_t kInvalidIndividualAddress = 0xffff; +constexpr uint16_t kReg1DaliManufacturerId = 0x00a4; +constexpr uint8_t kReg1DaliApplicationNumber = 0x01; +constexpr uint8_t kReg1DaliApplicationVersion = 0x05; +constexpr uint8_t kReg1DaliOrderNumber[10] = {'R', 'E', 'G', '1', '-', 'D', 'a', 'l', 'i', 0}; + +bool IsUsableIndividualAddress(uint16_t address) { + return address != 0 && address != kInvalidIndividualAddress; +} + +void ApplyReg1DaliIdentity(Bau07B0& device, EspIdfPlatform& platform) { + device.deviceObject().manufacturerId(kReg1DaliManufacturerId); + device.deviceObject().bauNumber(platform.uniqueSerialNumber()); + device.deviceObject().orderNumber(kReg1DaliOrderNumber); + const uint8_t program_version[5] = {0x00, 0xa4, 0x00, kReg1DaliApplicationNumber, + kReg1DaliApplicationVersion}; + device.parameters().property(PID_PROG_VERSION)->write(program_version); +} + +} // namespace + +EtsDeviceRuntime::EtsDeviceRuntime(std::string nvs_namespace, + uint16_t fallback_individual_address) + : nvs_namespace_(std::move(nvs_namespace)), + platform_(nullptr, nvs_namespace_.c_str()), + device_(platform_) { + ApplyReg1DaliIdentity(device_, platform_); + if (IsUsableIndividualAddress(fallback_individual_address)) { + device_.deviceObject().individualAddress(fallback_individual_address); + } + device_.readMemory(); + if (!IsUsableIndividualAddress(device_.deviceObject().individualAddress()) && + IsUsableIndividualAddress(fallback_individual_address)) { + device_.deviceObject().individualAddress(fallback_individual_address); + } + if (auto* server = device_.getCemiServer()) { + server->clientAddress(DefaultTunnelClientAddress(device_.deviceObject().individualAddress())); + server->tunnelFrameCallback(&EtsDeviceRuntime::EmitTunnelFrame, this); + } + device_.functionPropertyCallback(&EtsDeviceRuntime::HandleFunctionPropertyCommand); + device_.functionPropertyStateCallback(&EtsDeviceRuntime::HandleFunctionPropertyState); +} + +EtsDeviceRuntime::~EtsDeviceRuntime() { + device_.functionPropertyCallback(nullptr); + device_.functionPropertyStateCallback(nullptr); + if (auto* server = device_.getCemiServer()) { + server->tunnelFrameCallback(nullptr, nullptr); + } +} + +uint16_t EtsDeviceRuntime::individualAddress() const { + return const_cast(device_).deviceObject().individualAddress(); +} + +uint16_t EtsDeviceRuntime::tunnelClientAddress() const { + if (auto* server = const_cast(device_).getCemiServer()) { + return server->clientAddress(); + } + return DefaultTunnelClientAddress(individualAddress()); +} + +bool EtsDeviceRuntime::configured() const { return const_cast(device_).configured(); } + +EtsMemorySnapshot EtsDeviceRuntime::snapshot() const { + EtsMemorySnapshot out; + auto& device = const_cast(device_); + out.configured = device.configured(); + out.individual_address = device.deviceObject().individualAddress(); + device.forEachEtsAssociation( + [](uint16_t group_address, uint16_t group_object_number, void* context) { + auto* associations = static_cast*>(context); + if (associations != nullptr) { + associations->push_back(EtsAssociation{group_address, group_object_number}); + } + }, + &out.associations); + std::sort(out.associations.begin(), out.associations.end(), + [](const EtsAssociation& lhs, const EtsAssociation& rhs) { + if (lhs.group_address != rhs.group_address) { + return lhs.group_address < rhs.group_address; + } + return lhs.group_object_number < rhs.group_object_number; + }); + out.associations.erase( + std::unique(out.associations.begin(), out.associations.end(), + [](const EtsAssociation& lhs, const EtsAssociation& rhs) { + return lhs.group_address == rhs.group_address && + lhs.group_object_number == rhs.group_object_number; + }), + out.associations.end()); + return out; +} + +void EtsDeviceRuntime::setFunctionPropertyHandlers(FunctionPropertyHandler command_handler, + FunctionPropertyHandler state_handler) { + command_handler_ = std::move(command_handler); + state_handler_ = std::move(state_handler); +} + +bool EtsDeviceRuntime::handleTunnelFrame(const uint8_t* data, size_t len, + CemiFrameSender sender) { + auto* server = device_.getCemiServer(); + if (server == nullptr || data == nullptr || len < 2) { + return false; + } + std::vector frame_data(data, data + len); + CemiFrame frame(frame_data.data(), static_cast(frame_data.size())); + const bool consumed = shouldConsumeTunnelFrame(frame); + if (!consumed) { + return false; + } + sender_ = std::move(sender); + ActiveFunctionPropertyRuntimeScope callback_scope(this); + server->frameReceived(frame); + loop(); + sender_ = nullptr; + return consumed; +} + +void EtsDeviceRuntime::loop() { device_.loop(); } + +void EtsDeviceRuntime::EmitTunnelFrame(CemiFrame& frame, void* context) { + auto* self = static_cast(context); + if (self == nullptr || !self->sender_) { + return; + } + self->sender_(frame.data(), frame.dataLength()); +} + +bool EtsDeviceRuntime::HandleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id, + uint8_t length, uint8_t* data, + uint8_t* result_data, + uint8_t& result_length) { + if (active_function_property_runtime == nullptr) { + return false; + } + return DispatchFunctionProperty(&active_function_property_runtime->command_handler_, object_index, + property_id, length, data, result_data, result_length); +} + +bool EtsDeviceRuntime::HandleFunctionPropertyState(uint8_t object_index, uint8_t property_id, + uint8_t length, uint8_t* data, + uint8_t* result_data, + uint8_t& result_length) { + if (active_function_property_runtime == nullptr) { + return false; + } + return DispatchFunctionProperty(&active_function_property_runtime->state_handler_, object_index, + property_id, length, data, result_data, result_length); +} + +bool EtsDeviceRuntime::DispatchFunctionProperty(FunctionPropertyHandler* handler, + uint8_t object_index, uint8_t property_id, + uint8_t length, uint8_t* data, + uint8_t* result_data, uint8_t& result_length) { + if (handler == nullptr || !*handler || result_data == nullptr) { + return false; + } + std::vector response; + if (!(*handler)(object_index, property_id, data, length, &response)) { + return false; + } + result_length = static_cast(std::min(response.size(), result_length)); + if (result_length > 0) { + std::copy_n(response.begin(), result_length, result_data); + } + return true; +} + +uint16_t EtsDeviceRuntime::DefaultTunnelClientAddress(uint16_t individual_address) { + if (!IsUsableIndividualAddress(individual_address)) { + return 0x1101; + } + const uint16_t line_base = individual_address & 0xff00; + uint16_t device = static_cast((individual_address & 0x00ff) + 1); + if (device == 0 || device > 0xff) { + device = 1; + } + return static_cast(line_base | device); +} + +bool EtsDeviceRuntime::shouldConsumeTunnelFrame(CemiFrame& frame) const { + switch (frame.messageCode()) { + case M_PropRead_req: + case M_PropWrite_req: + case M_Reset_req: + case M_FuncPropCommand_req: + case M_FuncPropStateRead_req: + return true; + case L_data_req: + return frame.addressType() == IndividualAddress && + frame.destinationAddress() == individualAddress(); + default: + return false; + } +} + +} // namespace gateway::openknx diff --git a/components/openknx_idf/src/ets_memory_loader.cpp b/components/openknx_idf/src/ets_memory_loader.cpp index be3e19c..8d051c9 100644 --- a/components/openknx_idf/src/ets_memory_loader.cpp +++ b/components/openknx_idf/src/ets_memory_loader.cpp @@ -3,8 +3,11 @@ #include "openknx_idf/esp_idf_platform.h" #include "knx/bau07B0.h" +#include "knx/property.h" #include +#include +#include namespace gateway::openknx { namespace { @@ -18,18 +21,45 @@ void CollectAssociation(uint16_t group_address, uint16_t group_object_number, associations->push_back(EtsAssociation{group_address, group_object_number}); } +bool IsErasedMemory(const uint8_t* data, size_t size) { + if (data == nullptr) { + return false; + } + return std::all_of(data, data + size, [](uint8_t value) { return value == 0xff; }); +} + +constexpr uint16_t kReg1DaliManufacturerId = 0x00a4; +constexpr uint8_t kReg1DaliApplicationNumber = 0x01; +constexpr uint8_t kReg1DaliApplicationVersion = 0x05; +constexpr uint8_t kReg1DaliOrderNumber[10] = {'R', 'E', 'G', '1', '-', 'D', 'a', 'l', 'i', 0}; + +void ApplyReg1DaliIdentity(Bau07B0& device, EspIdfPlatform& platform) { + device.deviceObject().manufacturerId(kReg1DaliManufacturerId); + device.deviceObject().bauNumber(platform.uniqueSerialNumber()); + device.deviceObject().orderNumber(kReg1DaliOrderNumber); + const uint8_t program_version[5] = {0x00, 0xa4, 0x00, kReg1DaliApplicationNumber, + kReg1DaliApplicationVersion}; + device.parameters().property(PID_PROG_VERSION)->write(program_version); +} + } // namespace EtsMemorySnapshot LoadEtsMemorySnapshot(const std::string& nvs_namespace) { EspIdfPlatform platform(nullptr, nvs_namespace.c_str()); - Bau07B0 device(platform); - device.deviceObject().manufacturerId(0xfa); - device.deviceObject().bauNumber(platform.uniqueSerialNumber()); - device.readMemory(); - EtsMemorySnapshot snapshot; - snapshot.configured = device.configured(); - device.forEachEtsAssociation(CollectAssociation, &snapshot.associations); + const uint8_t* memory = platform.getNonVolatileMemoryStart(); + const size_t memory_size = platform.getNonVolatileMemorySize(); + if (memory == nullptr || memory_size == 0 || IsErasedMemory(memory, memory_size)) { + return snapshot; + } + + auto device = std::make_unique(platform); + ApplyReg1DaliIdentity(*device, platform); + device->readMemory(); + + snapshot.configured = device->configured(); + snapshot.individual_address = device->deviceObject().individualAddress(); + device->forEachEtsAssociation(CollectAssociation, &snapshot.associations); std::sort(snapshot.associations.begin(), snapshot.associations.end(), [](const EtsAssociation& lhs, const EtsAssociation& rhs) { if (lhs.group_address != rhs.group_address) { diff --git a/knx b/knx index b747f62..339d847 160000 --- a/knx +++ b/knx @@ -1 +1 @@ -Subproject commit b747f6284d528a4a4aeb65c17e07b57bca647da7 +Subproject commit 339d8472e79d1c7f652a54f1097f0a476e5f1b80 diff --git a/knx_dali_gw b/knx_dali_gw index 5cd7e66..6064d84 160000 --- a/knx_dali_gw +++ b/knx_dali_gw @@ -1 +1 @@ -Subproject commit 5cd7e66bf0e0aa692973e32a2acd6e941a05e8be +Subproject commit 6064d845202c0971b121fbca23c7d378d52a5d41