feat(gateway): implement commissioning scan functionality with options for new, randomize, delete, and assign

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-19 10:47:44 +08:00
parent a3f03719f9
commit 57950e7b0b
3 changed files with 288 additions and 104 deletions
@@ -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<GatewayKnxConfig> 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<uint8_t>* response);
bool handleReg1FoundEvgsState(const uint8_t* data, size_t len,
std::vector<uint8_t>* response);
static void CommissioningScanTaskEntry(void* arg);
void runCommissioningScanTask();
DaliBridgeEngine& engine_;
GatewayKnxConfig config_;
const openknx::EtsDeviceRuntime* runtime_{nullptr};
std::map<uint16_t, std::vector<GatewayKnxDaliBinding>> 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<GatewayKnxCommissioningBallast> commissioning_found_ballasts_;
+273 -103
View File
@@ -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<DecodedGoDiagnosticsGroupWrite> 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<size_t>(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<GatewayKnxDaliBinding> 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<GatewayKnxBridge*>(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<bool, 64> 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<uint8_t>((random_address.value() >> 16) & 0xff);
ballast.middle = static_cast<uint8_t>((random_address.value() >> 8) & 0xff);
ballast.low = static_cast<uint8_t>(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<unsigned>(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<unsigned>(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<unsigned>(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<unsigned>(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<bool, 64> 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<uint8_t>((random_address.value() >> 16) & 0xff);
ballast.middle = static_cast<uint8_t>((random_address.value() >> 8) & 0xff);
ballast.low = static_cast<uint8_t>(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<unsigned>(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<unsigned>(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<unsigned>(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<unsigned>(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<uint8_t>(
std::min<size_t>(commissioning_found_ballasts_.size(), 0xff)));
}
return true;
}
response->push_back(commissioning_scan_done_ ? 1 : 0);
if (data[0] == kReg1FunctionScan) {
response->push_back(static_cast<uint8_t>(
@@ -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();
+1 -1
Submodule knx updated: af9be62529...135f109061