From 57950e7b0b5385568016cbf3c4e819cca56f47b1 Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 19 May 2026 10:47:44 +0800 Subject: [PATCH] feat(gateway): implement commissioning scan functionality with options for new, randomize, delete, and assign Signed-off-by: Tony --- .../gateway_knx/include/gateway_knx.hpp | 14 + components/gateway_knx/src/gateway_knx.cpp | 376 +++++++++++++----- knx | 2 +- 3 files changed, 288 insertions(+), 104 deletions(-) diff --git a/components/gateway_knx/include/gateway_knx.hpp b/components/gateway_knx/include/gateway_knx.hpp index 36edd6b..7c5d502 100644 --- a/components/gateway_knx/include/gateway_knx.hpp +++ b/components/gateway_knx/include/gateway_knx.hpp @@ -125,6 +125,13 @@ struct GatewayKnxCommissioningBallast { uint8_t short_address{0xff}; }; +struct GatewayKnxReg1ScanOptions { + bool only_new{false}; + bool randomize{false}; + bool delete_all{false}; + bool assign{false}; +}; + std::optional GatewayKnxConfigFromValue(const DaliValue* value); DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config); bool GatewayKnxConfigUsesTpUart(const GatewayKnxConfig& config); @@ -143,6 +150,7 @@ std::string GatewayKnxGroupAddressString(uint16_t group_address); class GatewayKnxBridge { public: explicit GatewayKnxBridge(DaliBridgeEngine& engine); + ~GatewayKnxBridge(); void setConfig(const GatewayKnxConfig& config); void setRuntimeContext(const openknx::EtsDeviceRuntime* runtime); @@ -196,11 +204,17 @@ class GatewayKnxBridge { std::vector* response); bool handleReg1FoundEvgsState(const uint8_t* data, size_t len, std::vector* response); + static void CommissioningScanTaskEntry(void* arg); + void runCommissioningScanTask(); DaliBridgeEngine& engine_; GatewayKnxConfig config_; const openknx::EtsDeviceRuntime* runtime_{nullptr}; std::map> ets_bindings_by_group_address_; + SemaphoreHandle_t commissioning_lock_{nullptr}; + TaskHandle_t commissioning_scan_task_{nullptr}; + std::atomic_bool commissioning_scan_cancel_requested_{false}; + GatewayKnxReg1ScanOptions commissioning_scan_options_; bool commissioning_scan_done_{true}; bool commissioning_assign_done_{true}; std::vector commissioning_found_ballasts_; diff --git a/components/gateway_knx/src/gateway_knx.cpp b/components/gateway_knx/src/gateway_knx.cpp index 6623389..d9dbb5e 100644 --- a/components/gateway_knx/src/gateway_knx.cpp +++ b/components/gateway_knx/src/gateway_knx.cpp @@ -141,10 +141,12 @@ constexpr uint8_t kReg1DeviceTypeDt8 = 8; constexpr uint8_t kReg1ColorTypeTw = 1; constexpr uint8_t kDaliDeviceTypeNone = 0xfe; constexpr uint8_t kDaliDeviceTypeMultiple = 0xff; +constexpr uint32_t kCommissioningScanTaskStackSize = 8192; constexpr uint16_t kGroupObjectTableObjectType = OT_GRP_OBJ_TABLE; constexpr uint8_t kPidGoDiagnostics = 0x42; constexpr uint8_t kGoDiagnosticsReservedByte = 0x00; constexpr uint8_t kGoDiagnosticsGroupWriteService = 0x01; +constexpr uint8_t kGoDiagnosticsCompactPayloadFlag = 0x80; struct DecodedGroupWrite { uint16_t group_address{0}; @@ -569,15 +571,25 @@ std::optional DecodeGoDiagnosticsGroupWrite( if (data[0] != kGoDiagnosticsReservedByte || data[1] != kGoDiagnosticsGroupWriteService) { return std::nullopt; } - const size_t encoded_length = data[2]; - if (encoded_length < 2 || len != encoded_length + 3) { - return std::nullopt; + const uint8_t encoded_length = data[2]; + size_t payload_len = 0; + if ((encoded_length & kGoDiagnosticsCompactPayloadFlag) != 0) { + payload_len = static_cast(encoded_length & ~kGoDiagnosticsCompactPayloadFlag); + if (payload_len == 0 || len != payload_len + 5) { + return std::nullopt; + } + } else { + const size_t expanded_length = encoded_length; + if (expanded_length < 2 || len != expanded_length + 3) { + return std::nullopt; + } + payload_len = expanded_length - 2; } DecodedGoDiagnosticsGroupWrite out; out.group_address = ReadBe16(data + 3); out.payload = data + 5; - out.payload_len = encoded_length - 2; + out.payload_len = payload_len; return out; } @@ -1469,7 +1481,213 @@ std::optional EtsBindingForAssociation(uint8_t main_group } // namespace -GatewayKnxBridge::GatewayKnxBridge(DaliBridgeEngine& engine) : engine_(engine) {} +GatewayKnxBridge::GatewayKnxBridge(DaliBridgeEngine& engine) : engine_(engine) { + commissioning_lock_ = xSemaphoreCreateMutex(); + if (commissioning_lock_ == nullptr) { + ESP_LOGE(kTag, "Failed to create REG1-Dali commissioning mutex"); + } +} + +GatewayKnxBridge::~GatewayKnxBridge() { + commissioning_scan_cancel_requested_.store(true, std::memory_order_release); + if (commissioning_lock_ == nullptr) { + return; + } + + while (true) { + TaskHandle_t task_handle = nullptr; + { + SemaphoreGuard guard(commissioning_lock_); + task_handle = commissioning_scan_task_; + } + if (task_handle == nullptr) { + break; + } + vTaskDelay(pdMS_TO_TICKS(10)); + } + + vSemaphoreDelete(commissioning_lock_); + commissioning_lock_ = nullptr; +} + +void GatewayKnxBridge::CommissioningScanTaskEntry(void* arg) { + auto* bridge = static_cast(arg); + if (bridge != nullptr) { + bridge->runCommissioningScanTask(); + } + vTaskDelete(nullptr); +} + +void GatewayKnxBridge::runCommissioningScanTask() { + if (commissioning_lock_ == nullptr) { + return; + } + + GatewayKnxReg1ScanOptions options; + { + SemaphoreGuard guard(commissioning_lock_); + options = commissioning_scan_options_; + } + + auto is_cancelled = [this]() { + return commissioning_scan_cancel_requested_.load(std::memory_order_acquire); + }; + auto record_ballast = [this](const GatewayKnxCommissioningBallast& ballast) { + if (commissioning_lock_ == nullptr) { + return; + } + SemaphoreGuard guard(commissioning_lock_); + if (commissioning_scan_cancel_requested_.load(std::memory_order_acquire)) { + return; + } + commissioning_found_ballasts_.push_back(ballast); + }; + auto finish_scan = [this](bool clear_results) { + if (commissioning_lock_ == nullptr) { + return; + } + SemaphoreGuard guard(commissioning_lock_); + if (clear_results) { + commissioning_found_ballasts_.clear(); + } + commissioning_scan_done_ = true; + commissioning_scan_cancel_requested_.store(false, std::memory_order_release); + commissioning_scan_task_ = nullptr; + }; + + ESP_LOGI(kTag, "REG1-Dali scan start onlyNew=%d randomize=%d deleteAll=%d assign=%d", + options.only_new, options.randomize, options.delete_all, options.assign); + + bool clear_results = false; + std::array used_addresses{}; + + do { + if (is_cancelled()) { + clear_results = true; + break; + } + + if (options.assign && !options.delete_all) { + used_addresses = QueryUsedShortAddresses(engine_); + } + if (is_cancelled()) { + clear_results = true; + break; + } + + const bool initialized = SendRaw(engine_, DALI_CMD_SPECIAL_TERMINATE, DALI_CMD_OFF, + "knx-function-scan-terminate-prev") && + SendRawExt(engine_, DALI_CMD_SPECIAL_INITIALIZE, + options.only_new ? DALI_CMD_STOP_FADE : DALI_CMD_OFF, + "knx-function-scan-init") && + SendRawExt(engine_, DALI_CMD_SPECIAL_INITIALIZE, + options.only_new ? DALI_CMD_STOP_FADE : DALI_CMD_OFF, + "knx-function-scan-init-repeat"); + if (!initialized) { + ESP_LOGW(kTag, "REG1-Dali scan failed during initialize"); + break; + } + if (is_cancelled()) { + clear_results = true; + break; + } + + if (options.delete_all) { + const bool removed = SendRaw(engine_, DALI_CMD_SPECIAL_SET_DTR0, 0xff, + "knx-function-scan-clear-short-dtr") && + SendRawExt(engine_, 0xff, DALI_CMD_STORE_DTR_AS_SHORT_ADDRESS, + "knx-function-scan-clear-short"); + if (!removed) { + ESP_LOGW(kTag, "REG1-Dali scan failed while clearing short addresses"); + break; + } + } + if (is_cancelled()) { + clear_results = true; + break; + } + + if (options.randomize) { + const bool randomized = SendRawExt(engine_, DALI_CMD_SPECIAL_RANDOMIZE, DALI_CMD_OFF, + "knx-function-scan-randomize") && + SendRawExt(engine_, DALI_CMD_SPECIAL_RANDOMIZE, DALI_CMD_OFF, + "knx-function-scan-randomize-repeat"); + if (!randomized) { + ESP_LOGW(kTag, "REG1-Dali scan failed while randomizing addresses"); + break; + } + } + + while (!is_cancelled()) { + const auto random_address = FindLowestSelectedRandomAddress(engine_); + if (!random_address.has_value()) { + break; + } + + GatewayKnxCommissioningBallast ballast; + ballast.high = static_cast((random_address.value() >> 16) & 0xff); + ballast.middle = static_cast((random_address.value() >> 8) & 0xff); + ballast.low = static_cast(random_address.value() & 0xff); + ballast.short_address = 0xff; + + if (options.assign) { + const auto next_address = NextFreeShortAddress(used_addresses); + if (!next_address.has_value()) { + ESP_LOGW(kTag, "REG1-Dali scan has no free short address left for 0x%06x", + static_cast(random_address.value())); + break; + } + if (!SendRaw(engine_, DALI_CMD_SPECIAL_PROGRAM_SHORT_ADDRESS, + DaliComm::toCmdAddr(next_address.value()), + "knx-function-scan-program-short") || + !VerifyShortAddress(engine_, next_address.value())) { + ESP_LOGW(kTag, "REG1-Dali scan failed to program short address %u", + static_cast(next_address.value())); + break; + } + used_addresses[next_address.value()] = true; + ballast.short_address = next_address.value(); + } else { + ballast.short_address = QuerySelectedShortAddress(engine_).value_or(0xff); + } + + record_ballast(ballast); + ESP_LOGI(kTag, "REG1-Dali scan found random=0x%02X%02X%02X short=%u", + ballast.high, ballast.middle, ballast.low, + static_cast(ballast.short_address)); + + if (is_cancelled()) { + clear_results = true; + break; + } + if (!SendRaw(engine_, DALI_CMD_SPECIAL_WITHDRAW, DALI_CMD_OFF, + "knx-function-scan-withdraw")) { + ESP_LOGW(kTag, "REG1-Dali scan failed while withdrawing matched device"); + break; + } + } + + if (is_cancelled()) { + clear_results = true; + } + } while (false); + + SendRaw(engine_, DALI_CMD_SPECIAL_TERMINATE, DALI_CMD_OFF, + "knx-function-scan-terminate"); + finish_scan(clear_results); + + if (clear_results) { + ESP_LOGI(kTag, "REG1-Dali scan cancelled"); + } else { + size_t found_count = 0; + { + SemaphoreGuard guard(commissioning_lock_); + found_count = commissioning_found_ballasts_.size(); + } + ESP_LOGI(kTag, "REG1-Dali scan completed count=%u", + static_cast(found_count)); + } +} void GatewayKnxBridge::setConfig(const GatewayKnxConfig& config) { config_ = config; @@ -1775,112 +1993,34 @@ bool GatewayKnxBridge::handleReg1ScanCommand(const uint8_t* data, size_t len, if (len < 5 || response == nullptr) { return false; } - commissioning_scan_done_ = false; - commissioning_found_ballasts_.clear(); - - const bool only_new = data[1] == 1; - const bool randomize = data[2] == 1; - const bool delete_all = data[3] == 1; - const bool assign = data[4] == 1; - ESP_LOGI(kTag, "REG1-Dali scan start onlyNew=%d randomize=%d deleteAll=%d assign=%d", - only_new, randomize, delete_all, assign); - - std::array used_addresses{}; - if (assign && !delete_all) { - used_addresses = QueryUsedShortAddresses(engine_); - } - - const bool initialized = SendRaw(engine_, DALI_CMD_SPECIAL_TERMINATE, DALI_CMD_OFF, - "knx-function-scan-terminate-prev") && - SendRawExt(engine_, DALI_CMD_SPECIAL_INITIALIZE, - only_new ? DALI_CMD_STOP_FADE : DALI_CMD_OFF, - "knx-function-scan-init") && - SendRawExt(engine_, DALI_CMD_SPECIAL_INITIALIZE, - only_new ? DALI_CMD_STOP_FADE : DALI_CMD_OFF, - "knx-function-scan-init-repeat"); - if (!initialized) { - ESP_LOGW(kTag, "REG1-Dali scan failed during initialize"); - commissioning_scan_done_ = true; + if (commissioning_lock_ == nullptr) { + ESP_LOGE(kTag, "REG1-Dali scan unavailable: commissioning mutex missing"); response->clear(); return true; } - if (delete_all) { - const bool removed = SendRaw(engine_, DALI_CMD_SPECIAL_SET_DTR0, 0xff, - "knx-function-scan-clear-short-dtr") && - SendRawExt(engine_, 0xff, DALI_CMD_STORE_DTR_AS_SHORT_ADDRESS, - "knx-function-scan-clear-short"); - if (!removed) { - ESP_LOGW(kTag, "REG1-Dali scan failed while clearing short addresses"); - commissioning_scan_done_ = true; - response->clear(); - return true; - } + SemaphoreGuard guard(commissioning_lock_); + if (commissioning_scan_task_ != nullptr) { + ESP_LOGW(kTag, "REG1-Dali scan request ignored while a scan is already running"); + response->clear(); + return true; } - if (randomize) { - const bool randomized = SendRawExt(engine_, DALI_CMD_SPECIAL_RANDOMIZE, DALI_CMD_OFF, - "knx-function-scan-randomize") && - SendRawExt(engine_, DALI_CMD_SPECIAL_RANDOMIZE, DALI_CMD_OFF, - "knx-function-scan-randomize-repeat"); - if (!randomized) { - ESP_LOGW(kTag, "REG1-Dali scan failed while randomizing addresses"); - commissioning_scan_done_ = true; - response->clear(); - return true; - } + commissioning_scan_options_.only_new = data[1] == 1; + commissioning_scan_options_.randomize = data[2] == 1; + commissioning_scan_options_.delete_all = data[3] == 1; + commissioning_scan_options_.assign = data[4] == 1; + commissioning_scan_cancel_requested_.store(false, std::memory_order_release); + commissioning_scan_done_ = false; + commissioning_found_ballasts_.clear(); + + if (xTaskCreate(&GatewayKnxBridge::CommissioningScanTaskEntry, "gw_knx_scan", + kCommissioningScanTaskStackSize, this, tskIDLE_PRIORITY + 1, + &commissioning_scan_task_) != pdPASS) { + commissioning_scan_done_ = true; + ESP_LOGE(kTag, "Failed to start REG1-Dali commissioning scan task"); } - while (true) { - const auto random_address = FindLowestSelectedRandomAddress(engine_); - if (!random_address.has_value()) { - break; - } - - GatewayKnxCommissioningBallast ballast; - ballast.high = static_cast((random_address.value() >> 16) & 0xff); - ballast.middle = static_cast((random_address.value() >> 8) & 0xff); - ballast.low = static_cast(random_address.value() & 0xff); - ballast.short_address = 0xff; - - if (assign) { - const auto next_address = NextFreeShortAddress(used_addresses); - if (!next_address.has_value()) { - ESP_LOGW(kTag, "REG1-Dali scan has no free short address left for 0x%06x", - static_cast(random_address.value())); - break; - } - if (!SendRaw(engine_, DALI_CMD_SPECIAL_PROGRAM_SHORT_ADDRESS, - DaliComm::toCmdAddr(next_address.value()), - "knx-function-scan-program-short") || - !VerifyShortAddress(engine_, next_address.value())) { - ESP_LOGW(kTag, "REG1-Dali scan failed to program short address %u", - static_cast(next_address.value())); - break; - } - used_addresses[next_address.value()] = true; - ballast.short_address = next_address.value(); - } else { - ballast.short_address = QuerySelectedShortAddress(engine_).value_or(0xff); - } - - commissioning_found_ballasts_.push_back(ballast); - ESP_LOGI(kTag, "REG1-Dali scan found random=0x%02X%02X%02X short=%u", - ballast.high, ballast.middle, ballast.low, - static_cast(ballast.short_address)); - - if (!SendRaw(engine_, DALI_CMD_SPECIAL_WITHDRAW, DALI_CMD_OFF, - "knx-function-scan-withdraw")) { - ESP_LOGW(kTag, "REG1-Dali scan failed while withdrawing matched device"); - break; - } - } - - SendRaw(engine_, DALI_CMD_SPECIAL_TERMINATE, DALI_CMD_OFF, - "knx-function-scan-terminate"); - commissioning_scan_done_ = true; - ESP_LOGI(kTag, "REG1-Dali scan completed count=%u", - static_cast(commissioning_found_ballasts_.size())); response->clear(); return true; } @@ -2110,6 +2250,15 @@ bool GatewayKnxBridge::handleReg1ScanState(const uint8_t* data, size_t len, return false; } response->clear(); + if (commissioning_lock_ != nullptr) { + SemaphoreGuard guard(commissioning_lock_); + response->push_back(commissioning_scan_done_ ? 1 : 0); + if (data[0] == kReg1FunctionScan) { + response->push_back(static_cast( + std::min(commissioning_found_ballasts_.size(), 0xff))); + } + return true; + } response->push_back(commissioning_scan_done_ ? 1 : 0); if (data[0] == kReg1FunctionScan) { response->push_back(static_cast( @@ -2132,6 +2281,27 @@ bool GatewayKnxBridge::handleReg1FoundEvgsState(const uint8_t* data, size_t len, if (len < 2 || response == nullptr) { return false; } + if (commissioning_lock_ != nullptr) { + SemaphoreGuard guard(commissioning_lock_); + if (data[1] == 254) { + commissioning_scan_cancel_requested_.store(true, std::memory_order_release); + commissioning_scan_done_ = true; + commissioning_found_ballasts_.clear(); + response->clear(); + return true; + } + const size_t index = data[1]; + response->clear(); + response->push_back(index < commissioning_found_ballasts_.size() ? 1 : 0); + if (index < commissioning_found_ballasts_.size()) { + const auto& ballast = commissioning_found_ballasts_[index]; + response->push_back(ballast.high); + response->push_back(ballast.middle); + response->push_back(ballast.low); + response->push_back(ballast.short_address); + } + return true; + } if (data[1] == 254) { commissioning_found_ballasts_.clear(); response->clear(); diff --git a/knx b/knx index af9be62..135f109 160000 --- a/knx +++ b/knx @@ -1 +1 @@ -Subproject commit af9be625290d145e5d0dac80c3d66eca01a0a637 +Subproject commit 135f109061620ebba7e3c5f3b9bf60c3e586d829