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:
@@ -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_;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user