From 865bf8425a91f07a5a813b3da5d82e4e6a504047 Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 26 May 2026 22:21:36 +0800 Subject: [PATCH] feat(gateway): enhance DALI host activity tracking and presence management Signed-off-by: Tony --- apps/gateway/.vscode/settings.json | 3 + apps/gateway/sdkconfig | 1 + .../dali_domain/include/dali_domain.hpp | 14 ++ components/dali_domain/src/dali_domain.cpp | 114 ++++++++++- .../gateway_bridge/src/gateway_bridge.cpp | 46 +++++ .../gateway_cache/include/gateway_cache.hpp | 16 ++ .../gateway_cache/src/gateway_cache.cpp | 94 +++++++++ .../include/gateway_controller.hpp | 11 +- .../src/gateway_controller.cpp | 181 ++++++++++++++---- 9 files changed, 441 insertions(+), 39 deletions(-) create mode 100644 apps/gateway/.vscode/settings.json diff --git a/apps/gateway/.vscode/settings.json b/apps/gateway/.vscode/settings.json new file mode 100644 index 0000000..ad00ae8 --- /dev/null +++ b/apps/gateway/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "idf.currentSetup": "/Users/tonylu/.espressif/v6.0/esp-idf" +} \ No newline at end of file diff --git a/apps/gateway/sdkconfig b/apps/gateway/sdkconfig index 92c8d9c..cb14d87 100644 --- a/apps/gateway/sdkconfig +++ b/apps/gateway/sdkconfig @@ -692,6 +692,7 @@ CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED=y # # KNX Settings # +CONFIG_GATEWAY_KNX_INSTANCE_COUNT=1 CONFIG_GATEWAY_KNX_BRIDGE_SUPPORTED=y CONFIG_GATEWAY_START_KNX_BRIDGE_ENABLED=y CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED=y diff --git a/components/dali_domain/include/dali_domain.hpp b/components/dali_domain/include/dali_domain.hpp index b1c9351..e941787 100644 --- a/components/dali_domain/include/dali_domain.hpp +++ b/components/dali_domain/include/dali_domain.hpp @@ -127,6 +127,11 @@ class DaliDomainService { bool resetBus(uint8_t gateway_id) const; bool isBusIdle(uint8_t gateway_id, uint32_t quiet_ms) const; + void markHostActivity(uint8_t gateway_id) const; + void markHostCommandFrame(uint8_t gateway_id, uint8_t raw_addr, uint8_t command) const; + bool hasRecentHostActivity(uint8_t gateway_id, uint32_t window_ms) const; + bool matchesRecentHostCommandFrame(uint8_t gateway_id, uint8_t raw_addr, uint8_t command, + uint32_t window_ms) const; bool writeBridgeFrame(uint8_t gateway_id, const uint8_t* data, size_t len) const; std::vector transactBridgeFrame(uint8_t gateway_id, const uint8_t* data, size_t len) const; @@ -184,6 +189,12 @@ class DaliDomainService { private: struct DaliChannel; + struct RecentHostCommandFrame { + uint8_t raw_addr{0}; + uint8_t command{0}; + TickType_t tick{0}; + bool valid{false}; + }; DaliChannel* findChannelByGateway(uint8_t gateway_id); const DaliChannel* findChannelByGateway(uint8_t gateway_id) const; @@ -204,6 +215,9 @@ class DaliDomainService { SemaphoreHandle_t raw_frame_sink_lock_{nullptr}; mutable SemaphoreHandle_t bus_activity_lock_{nullptr}; mutable std::map last_bus_activity_ticks_; + mutable SemaphoreHandle_t host_activity_lock_{nullptr}; + mutable std::map last_host_activity_ticks_; + mutable std::map recent_host_command_frames_; TaskHandle_t raw_frame_task_handle_{nullptr}; }; diff --git a/components/dali_domain/src/dali_domain.cpp b/components/dali_domain/src/dali_domain.cpp index 184d8e2..2275304 100644 --- a/components/dali_domain/src/dali_domain.cpp +++ b/components/dali_domain/src/dali_domain.cpp @@ -315,7 +315,8 @@ struct DaliDomainService::DaliChannel { DaliDomainService::DaliDomainService() : raw_frame_sink_lock_(xSemaphoreCreateMutex()), - bus_activity_lock_(xSemaphoreCreateMutex()) { + bus_activity_lock_(xSemaphoreCreateMutex()), + host_activity_lock_(xSemaphoreCreateMutex()) { esp_log_level_set(TAG, (esp_log_level_t)CONFIG_DALI_LOG_LEVEL); } @@ -328,6 +329,10 @@ DaliDomainService::~DaliDomainService() { vSemaphoreDelete(bus_activity_lock_); bus_activity_lock_ = nullptr; } + if (host_activity_lock_ != nullptr) { + vSemaphoreDelete(host_activity_lock_); + host_activity_lock_ = nullptr; + } } bool DaliDomainService::bindTransport(const DaliChannelConfig& config, DaliTransportHooks hooks) { @@ -568,11 +573,85 @@ bool DaliDomainService::isBusIdle(uint8_t gateway_id, uint32_t quiet_ms) const { return (xTaskGetTickCount() - last_activity) >= pdMS_TO_TICKS(quiet_ms); } +void DaliDomainService::markHostActivity(uint8_t gateway_id) const { + if (host_activity_lock_ != nullptr) { + xSemaphoreTake(host_activity_lock_, portMAX_DELAY); + } + last_host_activity_ticks_[gateway_id] = xTaskGetTickCount(); + if (host_activity_lock_ != nullptr) { + xSemaphoreGive(host_activity_lock_); + } +} + +void DaliDomainService::markHostCommandFrame(uint8_t gateway_id, uint8_t raw_addr, + uint8_t command) const { + const TickType_t now = xTaskGetTickCount(); + if (host_activity_lock_ != nullptr) { + xSemaphoreTake(host_activity_lock_, portMAX_DELAY); + } + last_host_activity_ticks_[gateway_id] = now; + recent_host_command_frames_[gateway_id] = RecentHostCommandFrame{raw_addr, command, now, true}; + if (host_activity_lock_ != nullptr) { + xSemaphoreGive(host_activity_lock_); + } +} + +bool DaliDomainService::hasRecentHostActivity(uint8_t gateway_id, uint32_t window_ms) const { + if (window_ms == 0) { + return false; + } + + TickType_t last_activity = 0; + if (host_activity_lock_ != nullptr) { + xSemaphoreTake(host_activity_lock_, portMAX_DELAY); + } + if (const auto it = last_host_activity_ticks_.find(gateway_id); + it != last_host_activity_ticks_.end()) { + last_activity = it->second; + } + if (host_activity_lock_ != nullptr) { + xSemaphoreGive(host_activity_lock_); + } + if (last_activity == 0) { + return false; + } + return (xTaskGetTickCount() - last_activity) < pdMS_TO_TICKS(window_ms); +} + +bool DaliDomainService::matchesRecentHostCommandFrame(uint8_t gateway_id, uint8_t raw_addr, + uint8_t command, + uint32_t window_ms) const { + if (window_ms == 0) { + return false; + } + + RecentHostCommandFrame frame; + if (host_activity_lock_ != nullptr) { + xSemaphoreTake(host_activity_lock_, portMAX_DELAY); + } + if (const auto it = recent_host_command_frames_.find(gateway_id); + it != recent_host_command_frames_.end()) { + frame = it->second; + } + if (host_activity_lock_ != nullptr) { + xSemaphoreGive(host_activity_lock_); + } + if (!frame.valid || frame.raw_addr != raw_addr || frame.command != command) { + return false; + } + return (xTaskGetTickCount() - frame.tick) < pdMS_TO_TICKS(window_ms); +} + bool DaliDomainService::writeBridgeFrame(uint8_t gateway_id, const uint8_t* data, size_t len) const { const auto* channel = findChannelByGateway(gateway_id); if (channel == nullptr || !channel->hooks.send) { return false; } + if (data != nullptr && len == 3 && (data[0] == 0x10 || data[0] == 0x11)) { + markHostCommandFrame(gateway_id, data[1], data[2]); + } else { + markHostActivity(gateway_id); + } markBusActivity(gateway_id); return channel->hooks.send(data, len); } @@ -584,6 +663,11 @@ std::vector DaliDomainService::transactBridgeFrame(uint8_t gateway_id, if (channel == nullptr || !channel->hooks.transact) { return {}; } + if (data != nullptr && len == 3 && (data[0] == 0x10 || data[0] == 0x11 || data[0] == 0x12)) { + markHostCommandFrame(gateway_id, data[1], data[2]); + } else { + markHostActivity(gateway_id); + } markBusActivity(gateway_id); return channel->hooks.transact(data, len); } @@ -593,6 +677,7 @@ bool DaliDomainService::sendRaw(uint8_t gateway_id, uint8_t raw_addr, uint8_t co if (channel == nullptr || channel->comm == nullptr) { return false; } + markHostCommandFrame(gateway_id, raw_addr, command); markBusActivity(gateway_id); return channel->comm->sendRawNew(raw_addr, command); } @@ -602,6 +687,7 @@ bool DaliDomainService::sendExtRaw(uint8_t gateway_id, uint8_t raw_addr, uint8_t if (channel == nullptr || channel->comm == nullptr) { return false; } + markHostCommandFrame(gateway_id, raw_addr, command); markBusActivity(gateway_id); return channel->comm->sendExtRawNew(raw_addr, command); } @@ -612,6 +698,7 @@ std::optional DaliDomainService::queryRaw(uint8_t gateway_id, uint8_t r if (channel == nullptr || channel->comm == nullptr) { return std::nullopt; } + markHostCommandFrame(gateway_id, raw_addr, command); markBusActivity(gateway_id); return channel->comm->queryRawNew(raw_addr, command); } @@ -683,6 +770,23 @@ std::optional DaliDomainService::dt1Snapshot(uint8_t gateway PutOptionalInt(snapshot, "emergencyModeRaw", detailed->emergencyMode); PutOptionalInt(snapshot, "featuresRaw", detailed->feature); PutOptionalInt(snapshot, "deviceStatusRaw", detailed->deviceStatus); + PutOptionalInt(snapshot, "batteryChargeLevel", detailed->batteryChargeLevel); + PutOptionalInt(snapshot, "functionTestDelayTime", detailed->functionTestDelayTime); + PutOptionalInt(snapshot, "durationTestDelayTime", detailed->durationTestDelayTime); + PutOptionalInt(snapshot, "functionTestIntervalDays", detailed->functionTestIntervalDays); + PutOptionalInt(snapshot, "durationTestIntervalWeeks", detailed->durationTestIntervalWeeks); + PutOptionalInt(snapshot, "testExecutionTimeoutDays", detailed->testExecutionTimeoutDays); + PutOptionalInt(snapshot, "prolongTimeHalfMinutes", detailed->prolongTimeHalfMinutes); + PutOptionalInt(snapshot, "durationTestResultMinutes", detailed->durationTestResultMinutes); + PutOptionalInt(snapshot, "lampEmergencyTimeHours", detailed->lampEmergencyTimeHours); + PutOptionalInt(snapshot, "lampTotalOperationTimeHours", detailed->lampTotalOperationTimeHours); + PutOptionalInt(snapshot, "emergencyLevel", detailed->emergencyLevel); + PutOptionalInt(snapshot, "emergencyMinLevel", detailed->emergencyMinLevel); + PutOptionalInt(snapshot, "emergencyMaxLevel", detailed->emergencyMaxLevel); + PutOptionalInt(snapshot, "ratedDurationMinutes", detailed->ratedDurationMinutes); + PutOptionalInt(snapshot, "extendedVersion", detailed->extendedVersion); + PutOptionalInt(snapshot, "physicalMinLevel", detailed->physicalMinLevel); + PutOptionalInt(snapshot, "emergencyDeviceTypeCode", detailed->emergencyDeviceTypeCode); snapshot.bools["testInProgress"] = detailed->testInProgress; snapshot.bools["lampFailure"] = detailed->lampFailure; @@ -736,6 +840,8 @@ std::optional DaliDomainService::dt1Snapshot(uint8_t gateway snapshot.bools["hardwiredInhibitSupported"] = features.hardwiredInhibitSupported(); snapshot.bools["physicalSelectionSupported"] = features.physicalSelectionSupported(); snapshot.bools["relightInRestModeSupported"] = features.relightInRestModeSupported(); + snapshot.ints["derivedEmergencyDeviceTypeCode"] = + features.emergencyDeviceTypeCode(detailed->physicalMinLevel); } if (detailed->deviceStatus.has_value()) { const DaliDT1DeviceStatus status(detailed->deviceStatus.value()); @@ -1208,9 +1314,11 @@ std::optional DaliDomainService::queryAddressSettin DaliAddressSettingsSnapshot settings{}; - if (const auto value = channel->dali->base.getPowerOnLevel(short_address); value.has_value()) { - settings.power_on_level = static_cast(*value); + const auto power_on_level = channel->dali->base.getPowerOnLevel(short_address); + if (!power_on_level.has_value()) { + return std::nullopt; } + settings.power_on_level = static_cast(*power_on_level); if (const auto value = channel->dali->base.getSystemFailureLevel(short_address); value.has_value()) { settings.system_failure_level = static_cast(*value); diff --git a/components/gateway_bridge/src/gateway_bridge.cpp b/components/gateway_bridge/src/gateway_bridge.cpp index 7444409..a021797 100644 --- a/components/gateway_bridge/src/gateway_bridge.cpp +++ b/components/gateway_bridge/src/gateway_bridge.cpp @@ -587,9 +587,39 @@ bool OperationRequiresDt1(BridgeOperation operation) { case BridgeOperation::getEmergencyLevel: case BridgeOperation::getEmergencyStatus: case BridgeOperation::getEmergencyFailureStatus: + case BridgeOperation::getDt1Snapshot: case BridgeOperation::startEmergencyFunctionTest: case BridgeOperation::stopEmergencyTest: case BridgeOperation::startEmergencyDurationTest: + case BridgeOperation::dt1Rest: + case BridgeOperation::dt1Inhibit: + case BridgeOperation::dt1RelightResetInhibit: + case BridgeOperation::dt1StartIdentification: + case BridgeOperation::dt1ResetFunctionTestDoneFlag: + case BridgeOperation::dt1ResetDurationTestDoneFlag: + case BridgeOperation::dt1ResetLampTime: + case BridgeOperation::dt1StoreEmergencyLevel: + case BridgeOperation::dt1StoreTestDelayTime: + case BridgeOperation::dt1StoreFunctionTestInterval: + case BridgeOperation::dt1StoreDurationTestInterval: + case BridgeOperation::dt1StoreTestExecutionTimeout: + case BridgeOperation::dt1StoreProlongTime: + case BridgeOperation::dt1PerformDtrSelectedFunction: + case BridgeOperation::dt1GetBatteryCharge: + case BridgeOperation::dt1GetFunctionTestDelayTime: + case BridgeOperation::dt1GetDurationTestDelayTime: + case BridgeOperation::dt1GetFunctionTestInterval: + case BridgeOperation::dt1GetDurationTestInterval: + case BridgeOperation::dt1GetTestExecutionTimeout: + case BridgeOperation::dt1GetProlongTime: + case BridgeOperation::dt1GetDurationTestResult: + case BridgeOperation::dt1GetLampEmergencyTime: + case BridgeOperation::dt1GetLampTotalOperationTime: + case BridgeOperation::dt1GetEmergencyMinLevel: + case BridgeOperation::dt1GetEmergencyMaxLevel: + case BridgeOperation::dt1GetRatedDuration: + case BridgeOperation::dt1GetExtendedVersion: + case BridgeOperation::dt1GetEmergencyDeviceType: return true; default: return false; @@ -617,6 +647,22 @@ bool BridgeOperationReadable(BridgeOperation operation) { case BridgeOperation::getEmergencyLevel: case BridgeOperation::getEmergencyStatus: case BridgeOperation::getEmergencyFailureStatus: + case BridgeOperation::getDt1Snapshot: + case BridgeOperation::dt1GetBatteryCharge: + case BridgeOperation::dt1GetFunctionTestDelayTime: + case BridgeOperation::dt1GetDurationTestDelayTime: + case BridgeOperation::dt1GetFunctionTestInterval: + case BridgeOperation::dt1GetDurationTestInterval: + case BridgeOperation::dt1GetTestExecutionTimeout: + case BridgeOperation::dt1GetProlongTime: + case BridgeOperation::dt1GetDurationTestResult: + case BridgeOperation::dt1GetLampEmergencyTime: + case BridgeOperation::dt1GetLampTotalOperationTime: + case BridgeOperation::dt1GetEmergencyMinLevel: + case BridgeOperation::dt1GetEmergencyMaxLevel: + case BridgeOperation::dt1GetRatedDuration: + case BridgeOperation::dt1GetExtendedVersion: + case BridgeOperation::dt1GetEmergencyDeviceType: return true; default: return false; diff --git a/components/gateway_cache/include/gateway_cache.hpp b/components/gateway_cache/include/gateway_cache.hpp index 68b7f1e..c9069fa 100644 --- a/components/gateway_cache/include/gateway_cache.hpp +++ b/components/gateway_cache/include/gateway_cache.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "esp_err.h" #include "freertos/FreeRTOS.h" @@ -42,6 +43,12 @@ enum class GatewayCacheDaliTargetKind : uint8_t { kBroadcast = 2, }; +enum class GatewayCacheDaliPresence : uint8_t { + kUnknown = 0, + kOnline = 1, + kOffline = 2, +}; + struct GatewayCacheDaliTarget { GatewayCacheDaliTargetKind kind{GatewayCacheDaliTargetKind::kShortAddress}; uint8_t value{0}; @@ -138,6 +145,12 @@ class GatewayCache { GatewayCacheChannelFlags channelFlags(uint8_t gateway_id); GatewayCacheChannelFlags pendingChannelFlags(uint8_t gateway_id); GatewayCacheDaliAddressState daliAddressState(uint8_t gateway_id, uint8_t short_address); + GatewayCacheDaliPresence daliAddressPresence(uint8_t gateway_id, uint8_t short_address); + void markDaliAddressPresence(uint8_t gateway_id, uint8_t short_address, + GatewayCacheDaliPresence presence); + std::optional decodeDaliTarget(uint8_t raw_addr); + std::vector reconciliationAddresses( + uint8_t gateway_id, std::optional target); GatewayCacheDaliRuntimeStatus daliGroupStatus(uint8_t gateway_id, uint8_t group_id); GatewayCacheDaliRuntimeStatus daliBroadcastStatus(uint8_t gateway_id); bool setDaliGroupMask(uint8_t gateway_id, uint8_t short_address, @@ -183,6 +196,8 @@ class GatewayCache { bool commitStorageLocked(); bool shouldTrackUpdateFlagsLocked() const; uint32_t nextDaliRuntimeRevisionLocked(); + void markDaliAddressPresenceLocked(uint8_t gateway_id, uint8_t short_address, + GatewayCacheDaliPresence presence); bool mirrorDaliCommandLocked(uint8_t gateway_id, uint8_t raw_addr, uint8_t command); void clearDaliTargetStateLocked(uint8_t gateway_id, const GatewayCacheDaliTarget& target, uint32_t revision); @@ -225,6 +240,7 @@ class GatewayCache { std::map scenes_; std::map groups_; std::map> dali_states_; + std::map> dali_presence_; std::map> dali_group_status_; std::map dali_broadcast_status_; std::map dtr_states_; diff --git a/components/gateway_cache/src/gateway_cache.cpp b/components/gateway_cache/src/gateway_cache.cpp index d7d89a2..c825469 100644 --- a/components/gateway_cache/src/gateway_cache.cpp +++ b/components/gateway_cache/src/gateway_cache.cpp @@ -759,6 +759,86 @@ GatewayCacheDaliAddressState GatewayCache::daliAddressState(uint8_t gateway_id, return ensureDaliAddressStateLocked(gateway_id, short_address); } +GatewayCacheDaliPresence GatewayCache::daliAddressPresence(uint8_t gateway_id, + uint8_t short_address) { + LockGuard guard(lock_); + if (short_address >= 64) { + return GatewayCacheDaliPresence::kUnknown; + } + if (const auto it = dali_presence_.find(gateway_id); it != dali_presence_.end()) { + return it->second[short_address]; + } + return GatewayCacheDaliPresence::kUnknown; +} + +void GatewayCache::markDaliAddressPresence(uint8_t gateway_id, uint8_t short_address, + GatewayCacheDaliPresence presence) { + LockGuard guard(lock_); + markDaliAddressPresenceLocked(gateway_id, short_address, presence); +} + +std::optional GatewayCache::decodeDaliTarget(uint8_t raw_addr) { + return DecodeDaliTarget(raw_addr); +} + +std::vector GatewayCache::reconciliationAddresses( + uint8_t gateway_id, std::optional target) { + LockGuard guard(lock_); + std::vector addresses; + + auto presence = [&](uint8_t short_address) { + if (const auto it = dali_presence_.find(gateway_id); it != dali_presence_.end()) { + return it->second[short_address]; + } + return GatewayCacheDaliPresence::kUnknown; + }; + auto add_if_known_online = [&](uint8_t short_address) { + if (short_address < 64 && presence(short_address) == GatewayCacheDaliPresence::kOnline) { + addresses.push_back(short_address); + } + }; + + if (!target.has_value()) { + for (uint8_t short_address = 0; short_address < 64; ++short_address) { + add_if_known_online(short_address); + } + return addresses; + } + + switch (target->kind) { + case GatewayCacheDaliTargetKind::kShortAddress: + if (target->value < 64 && presence(target->value) != GatewayCacheDaliPresence::kOffline) { + addresses.push_back(target->value); + } + break; + case GatewayCacheDaliTargetKind::kGroup: { + if (target->value >= 16) { + break; + } + const uint16_t bit = static_cast(1U << target->value); + auto [states_it, inserted] = dali_states_.try_emplace(gateway_id); + if (inserted) { + loadDaliStateStoreLocked(gateway_id, states_it->second); + } + for (uint8_t short_address = 0; short_address < states_it->second.size(); ++short_address) { + const auto& state = states_it->second[short_address]; + if (state.group_mask_known && (state.group_mask & bit) != 0) { + add_if_known_online(short_address); + } + } + break; + } + case GatewayCacheDaliTargetKind::kBroadcast: + for (uint8_t short_address = 0; short_address < 64; ++short_address) { + add_if_known_online(short_address); + } + break; + default: + break; + } + return addresses; +} + GatewayCacheDaliRuntimeStatus GatewayCache::daliGroupStatus(uint8_t gateway_id, uint8_t group_id) { LockGuard guard(lock_); @@ -1535,6 +1615,20 @@ bool GatewayCache::shouldTrackUpdateFlagsLocked() const { return config_.cache_enabled && config_.reconciliation_enabled; } +void GatewayCache::markDaliAddressPresenceLocked(uint8_t gateway_id, uint8_t short_address, + GatewayCacheDaliPresence presence) { + if (short_address >= 64) { + return; + } + auto& states = dali_presence_[gateway_id]; + const auto previous = states[short_address]; + states[short_address] = presence; + if (previous != presence) { + ESP_LOGD(kTag, "presence gateway=%u short=%u state=%u", gateway_id, short_address, + static_cast(presence)); + } +} + GatewayCacheDaliAddressState& GatewayCache::ensureDaliAddressStateLocked(uint8_t gateway_id, uint8_t short_address) { auto [it, inserted] = dali_states_.try_emplace(gateway_id); diff --git a/components/gateway_controller/include/gateway_controller.hpp b/components/gateway_controller/include/gateway_controller.hpp index 98257c2..e248ed6 100644 --- a/components/gateway_controller/include/gateway_controller.hpp +++ b/components/gateway_controller/include/gateway_controller.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,8 @@ struct GatewayControllerConfig { bool cache_supported{true}; uint32_t cache_refresh_interval_ms{120000}; uint32_t cache_refresh_idle_ms{100}; + uint32_t cache_host_snooze_ms{5000}; + uint32_t cache_host_echo_ms{250}; }; struct GatewayChannelSnapshot { @@ -101,7 +104,9 @@ class GatewayController { GatewayCacheChannelFlags flags{}; Phase phase{Phase::kReloadFlags}; - uint8_t short_address{0}; + std::optional target; + std::vector addresses; + size_t address_index{0}; uint8_t scene_id{0}; }; @@ -120,9 +125,11 @@ class GatewayController { static void TaskEntry(void* arg); void taskLoop(); void dispatchCommand(const std::vector& command); - void scheduleReconciliation(uint8_t gateway_id); + void scheduleReconciliation(uint8_t gateway_id, + std::optional target = std::nullopt); bool hasPendingReconciliation() const; bool cacheRefreshEnabled() const; + bool cacheMaintenanceSnoozed(uint8_t gateway_id) const; bool runMaintenanceStep(); bool runReconciliationStep(uint8_t gateway_id, ReconciliationJob& job); bool runCacheRefreshStep(); diff --git a/components/gateway_controller/src/gateway_controller.cpp b/components/gateway_controller/src/gateway_controller.cpp index b738474..5674325 100644 --- a/components/gateway_controller/src/gateway_controller.cpp +++ b/components/gateway_controller/src/gateway_controller.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include namespace gateway { @@ -77,6 +78,43 @@ bool AnyFlagSet(const GatewayCacheChannelFlags& flags) { return flags.need_update_group || flags.need_update_scene || flags.need_update_settings; } +bool SameTarget(const std::optional& lhs, + const std::optional& rhs) { + if (lhs.has_value() != rhs.has_value()) { + return false; + } + if (!lhs.has_value()) { + return true; + } + return lhs->kind == rhs->kind && lhs->value == rhs->value; +} + +bool IsDaliHostCommandOpcode(uint8_t opcode) { + switch (opcode) { + case 0x07: + case 0x08: + case 0x10: + case 0x11: + case 0x12: + case 0x13: + case 0x14: + case 0x15: + case 0x16: + case 0x17: + case 0x18: + case 0x30: + case 0x32: + case 0x37: + case 0x38: + case 0xA0: + case 0xA2: + case kBridgeTransportRequestOpcode: + return true; + default: + return false; + } +} + std::string NormalizeName(std::string_view name) { std::string normalized(name); if (normalized.size() > kMaxNameBytes) { @@ -375,7 +413,8 @@ void GatewayController::taskLoop() { } } -void GatewayController::scheduleReconciliation(uint8_t gateway_id) { +void GatewayController::scheduleReconciliation(uint8_t gateway_id, + std::optional target) { if (!cache_.reconciliationEnabled()) { return; } @@ -390,7 +429,16 @@ void GatewayController::scheduleReconciliation(uint8_t gateway_id) { { LockGuard guard(maintenance_lock_); - reconciliation_jobs_.try_emplace(gateway_id); + auto [it, inserted] = reconciliation_jobs_.try_emplace(gateway_id); + if (inserted) { + it->second.target = target; + } else if (!SameTarget(it->second.target, target)) { + it->second.target.reset(); + it->second.phase = ReconciliationJob::Phase::kReloadFlags; + it->second.addresses.clear(); + it->second.address_index = 0; + it->second.scene_id = 0; + } } if (task_handle_ != nullptr) { @@ -408,6 +456,11 @@ bool GatewayController::cacheRefreshEnabled() const { config_.cache_refresh_interval_ms > 0; } +bool GatewayController::cacheMaintenanceSnoozed(uint8_t gateway_id) const { + return config_.cache_host_snooze_ms > 0 && + dali_domain_.hasRecentHostActivity(gateway_id, config_.cache_host_snooze_ms); +} + bool GatewayController::runMaintenanceStep() { if (cache_.reconciliationEnabled()) { bool has_job = false; @@ -427,6 +480,9 @@ bool GatewayController::runMaintenanceStep() { if (runtime_.shouldYieldMaintenance(gateway_id)) { return false; } + if (cacheMaintenanceSnoozed(gateway_id)) { + return false; + } const bool keep_job = runReconciliationStep(gateway_id, job); @@ -459,8 +515,13 @@ bool GatewayController::runReconciliationStep(uint8_t gateway_id, Reconciliation return false; } - job.short_address = 0; + job.addresses = cache_.reconciliationAddresses(gateway_id, job.target); + job.address_index = 0; job.scene_id = 0; + if (job.addresses.empty()) { + cache_.clearChannelFlagsIfMatched(gateway_id, job.flags); + return false; + } if (job.flags.need_update_group) { job.phase = ReconciliationJob::Phase::kGroups; } else if (job.flags.need_update_scene) { @@ -472,9 +533,9 @@ bool GatewayController::runReconciliationStep(uint8_t gateway_id, Reconciliation switch (job.phase) { case ReconciliationJob::Phase::kGroups: - reconcileGroupStep(gateway_id, job.short_address++); - if (job.short_address >= kDaliShortAddressCount) { - job.short_address = 0; + reconcileGroupStep(gateway_id, job.addresses[job.address_index++]); + if (job.address_index >= job.addresses.size()) { + job.address_index = 0; if (job.flags.need_update_scene) { job.phase = ReconciliationJob::Phase::kScenes; } else if (job.flags.need_update_settings) { @@ -486,15 +547,22 @@ bool GatewayController::runReconciliationStep(uint8_t gateway_id, Reconciliation } } return true; - case ReconciliationJob::Phase::kScenes: - reconcileSceneStep(gateway_id, job.short_address, job.scene_id); - ++job.scene_id; + case ReconciliationJob::Phase::kScenes: { + const uint8_t short_address = job.addresses[job.address_index]; + if (cache_.daliAddressPresence(gateway_id, short_address) == + GatewayCacheDaliPresence::kOffline) { + job.scene_id = 0; + ++job.address_index; + } else { + reconcileSceneStep(gateway_id, short_address, job.scene_id); + ++job.scene_id; + } if (job.scene_id >= kDaliSceneCount) { job.scene_id = 0; - ++job.short_address; + ++job.address_index; } - if (job.short_address >= kDaliShortAddressCount) { - job.short_address = 0; + if (job.address_index >= job.addresses.size()) { + job.address_index = 0; if (job.flags.need_update_settings) { job.phase = ReconciliationJob::Phase::kSettings; } else if (!cache_.clearChannelFlagsIfMatched(gateway_id, job.flags)) { @@ -504,10 +572,15 @@ bool GatewayController::runReconciliationStep(uint8_t gateway_id, Reconciliation } } return true; - case ReconciliationJob::Phase::kSettings: - reconcileSettingsStep(gateway_id, job.short_address++); - if (job.short_address >= kDaliShortAddressCount) { - job.short_address = 0; + } + case ReconciliationJob::Phase::kSettings: { + const uint8_t short_address = job.addresses[job.address_index++]; + if (cache_.daliAddressPresence(gateway_id, short_address) != + GatewayCacheDaliPresence::kOffline) { + reconcileSettingsStep(gateway_id, short_address); + } + if (job.address_index >= job.addresses.size()) { + job.address_index = 0; if (!cache_.clearChannelFlagsIfMatched(gateway_id, job.flags)) { job.phase = ReconciliationJob::Phase::kReloadFlags; } else { @@ -515,6 +588,7 @@ bool GatewayController::runReconciliationStep(uint8_t gateway_id, Reconciliation } } return true; + } case ReconciliationJob::Phase::kReloadFlags: default: return true; @@ -542,22 +616,36 @@ bool GatewayController::runCacheRefreshStep() { } if (runtime_.shouldYieldMaintenance(channel.gateway_id) || dali_domain_.isAllocAddr(channel.gateway_id) || + cacheMaintenanceSnoozed(channel.gateway_id) || !dali_domain_.isBusIdle(channel.gateway_id, config_.cache_refresh_idle_ms)) { continue; } + auto advance_job = [&]() { + ++job.short_address; + if (job.short_address >= kDaliShortAddressCount) { + job.short_address = 0; + job.next_due_tick = xTaskGetTickCount() + interval_ticks; + } else { + job.next_due_tick = xTaskGetTickCount(); + } + }; + + if (cache_.daliAddressPresence(channel.gateway_id, job.short_address) == + GatewayCacheDaliPresence::kOffline) { + advance_job(); + return true; + } + maintenance_activity_gateway_.store(channel.gateway_id); const auto actual_level = dali_domain_.queryActualLevel(channel.gateway_id, job.short_address); maintenance_activity_gateway_.store(-1); + cache_.markDaliAddressPresence(channel.gateway_id, job.short_address, + actual_level.has_value() + ? GatewayCacheDaliPresence::kOnline + : GatewayCacheDaliPresence::kOffline); cache_.setDaliActualLevel(channel.gateway_id, job.short_address, actual_level); - - ++job.short_address; - if (job.short_address >= kDaliShortAddressCount) { - job.short_address = 0; - job.next_due_tick = xTaskGetTickCount() + interval_ticks; - } else { - job.next_due_tick = xTaskGetTickCount(); - } + advance_job(); return true; } @@ -573,6 +661,10 @@ void GatewayController::reconcileGroupStep(uint8_t gateway_id, uint8_t short_add const bool applied = dali_domain_.applyGroupMask(gateway_id, short_address, state.group_mask); maintenance_activity_gateway_.store(-1); const auto verified_mask = dali_domain_.queryGroupMask(gateway_id, short_address); + cache_.markDaliAddressPresence(gateway_id, short_address, + verified_mask.has_value() + ? GatewayCacheDaliPresence::kOnline + : GatewayCacheDaliPresence::kOffline); cache_.setDaliGroupMask(gateway_id, short_address, verified_mask); if (!applied && verified_mask.has_value()) { ESP_LOGW(kTag, "group reconcile fallback gateway=%u short=%u", gateway_id, short_address); @@ -580,8 +672,11 @@ void GatewayController::reconcileGroupStep(uint8_t gateway_id, uint8_t short_add return; } - cache_.setDaliGroupMask(gateway_id, short_address, - dali_domain_.queryGroupMask(gateway_id, short_address)); + const auto group_mask = dali_domain_.queryGroupMask(gateway_id, short_address); + cache_.markDaliAddressPresence(gateway_id, short_address, + group_mask.has_value() ? GatewayCacheDaliPresence::kOnline + : GatewayCacheDaliPresence::kOffline); + cache_.setDaliGroupMask(gateway_id, short_address, group_mask); } void GatewayController::reconcileSceneStep(uint8_t gateway_id, uint8_t short_address, @@ -596,8 +691,11 @@ void GatewayController::reconcileSceneStep(uint8_t gateway_id, uint8_t short_add maintenance_activity_gateway_.store(-1); } - cache_.setDaliSceneLevel(gateway_id, short_address, scene_id, - dali_domain_.querySceneLevel(gateway_id, short_address, scene_id)); + const auto level = dali_domain_.querySceneLevel(gateway_id, short_address, scene_id); + cache_.markDaliAddressPresence(gateway_id, short_address, + level.has_value() ? GatewayCacheDaliPresence::kOnline + : GatewayCacheDaliPresence::kOffline); + cache_.setDaliSceneLevel(gateway_id, short_address, scene_id, level); } void GatewayController::reconcileSettingsStep(uint8_t gateway_id, uint8_t short_address) { @@ -618,6 +716,9 @@ void GatewayController::reconcileSettingsStep(uint8_t gateway_id, uint8_t short_ } const auto settings = dali_domain_.queryAddressSettings(gateway_id, short_address); + cache_.markDaliAddressPresence(gateway_id, short_address, + settings.has_value() ? GatewayCacheDaliPresence::kOnline + : GatewayCacheDaliPresence::kOffline); if (settings.has_value()) { cache_.setDaliSettings(gateway_id, short_address, GatewayCacheDaliSettingsSnapshot{settings->power_on_level, @@ -645,6 +746,9 @@ void GatewayController::dispatchCommand(const std::vector& command) { ESP_LOGW(kTag, "command for unknown gateway=%u opcode=0x%02x", gateway_id, opcode); return; } + if (IsDaliHostCommandOpcode(opcode)) { + dali_domain_.markHostActivity(gateway_id); + } switch (opcode) { case 0x00: @@ -1021,14 +1125,18 @@ void GatewayController::handleDaliRawFrame(const DaliRawFrame& frame) { } const bool maintenance_activity = maintenance_activity_gateway_.load() == frame.gateway_id; + const bool host_echo_activity = + dali_domain_.matchesRecentHostCommandFrame(frame.gateway_id, addr, data, + config_.cache_host_echo_ms) || + dali_domain_.hasRecentHostActivity(frame.gateway_id, config_.cache_host_echo_ms); const bool local_activity = maintenance_activity || runtime_.hasActiveCommand(frame.gateway_id) || - dali_domain_.isAllocAddr(frame.gateway_id); + host_echo_activity || dali_domain_.isAllocAddr(frame.gateway_id); const bool flagged = cache_.observeDaliCommand(frame.gateway_id, addr, data, local_activity ? GatewayCacheRawFrameOrigin::kLocalGateway : GatewayCacheRawFrameOrigin::kOutsideBus); if (flagged) { - scheduleReconciliation(frame.gateway_id); + scheduleReconciliation(frame.gateway_id, cache_.decodeDaliTarget(addr)); } if (setup_mode_ || dali_domain_.isAllocAddr(frame.gateway_id) || maintenance_activity || @@ -1057,26 +1165,31 @@ bool GatewayController::sendExtRawAndMirror(uint8_t gateway_id, uint8_t raw_addr } bool GatewayController::setBrightAndMirror(uint8_t gateway_id, int dec_address, uint8_t level) { + const uint8_t raw_addr = rawArcAddressFromDec(dec_address); + dali_domain_.markHostCommandFrame(gateway_id, raw_addr, level); const bool sent = dali_domain_.setBright(gateway_id, dec_address, level); if (sent) { - cache_.mirrorDaliCommand(gateway_id, rawArcAddressFromDec(dec_address), level); + cache_.mirrorDaliCommand(gateway_id, raw_addr, level); } return sent; } bool GatewayController::offAndMirror(uint8_t gateway_id, int dec_address) { + const uint8_t raw_addr = rawCommandAddressFromDec(dec_address); + dali_domain_.markHostCommandFrame(gateway_id, raw_addr, kDaliCmdOff); const bool sent = dali_domain_.off(gateway_id, dec_address); if (sent) { - cache_.mirrorDaliCommand(gateway_id, rawCommandAddressFromDec(dec_address), kDaliCmdOff); + cache_.mirrorDaliCommand(gateway_id, raw_addr, kDaliCmdOff); } return sent; } bool GatewayController::onAndMirror(uint8_t gateway_id, int dec_address) { + const uint8_t raw_addr = rawCommandAddressFromDec(dec_address); + dali_domain_.markHostCommandFrame(gateway_id, raw_addr, kDaliCmdRecallMax); const bool sent = dali_domain_.on(gateway_id, dec_address); if (sent) { - cache_.mirrorDaliCommand(gateway_id, rawCommandAddressFromDec(dec_address), - kDaliCmdRecallMax); + cache_.mirrorDaliCommand(gateway_id, raw_addr, kDaliCmdRecallMax); } return sent; }