From 639fdd860e7c8a7ae920c6f960a37d9a9bff2465 Mon Sep 17 00:00:00 2001 From: Tony Date: Sat, 2 May 2026 03:04:06 +0800 Subject: [PATCH] feat(gateway): implement reconciliation mechanism and command prioritization - Introduced a reconciliation job structure to manage the reconciliation process for gateway channels. - Added methods to schedule and run reconciliation steps, including group, scene, and settings reconciliation. - Implemented a locking mechanism to ensure thread safety during reconciliation operations. - Enhanced command handling in GatewayRuntime to classify commands by priority (control, normal, maintenance). - Updated command enqueueing and processing to respect command priorities, ensuring maintenance commands are handled appropriately. - Added configuration options for enabling/disabling cache functionality in GatewayRuntime. - Improved logging to include cache status during runtime initialization. Co-authored-by: Copilot --- apps/gateway/main/Kconfig.projbuild | 32 ++ apps/gateway/main/app_main.cpp | 29 ++ apps/gateway/sdkconfig | 4 + .../dali_domain/include/dali_domain.hpp | 25 + components/dali_domain/src/dali_domain.cpp | 119 +++++ .../gateway_cache/include/gateway_cache.hpp | 60 +++ .../gateway_cache/src/gateway_cache.cpp | 490 +++++++++++++++++- .../include/gateway_controller.hpp | 27 + .../src/gateway_controller.cpp | 278 +++++++++- .../include/gateway_runtime.hpp | 28 +- .../gateway_runtime/src/gateway_runtime.cpp | 151 +++++- 11 files changed, 1209 insertions(+), 34 deletions(-) diff --git a/apps/gateway/main/Kconfig.projbuild b/apps/gateway/main/Kconfig.projbuild index b018dad..f7e1fb4 100644 --- a/apps/gateway/main/Kconfig.projbuild +++ b/apps/gateway/main/Kconfig.projbuild @@ -221,8 +221,39 @@ endmenu menu "Gateway Cache" +config GATEWAY_CACHE_SUPPORTED + bool "Gateway cache support" + default y + help + Enables the gateway cache facade. When disabled, internal scene and group + commands still persist through direct NVS writes. + +config GATEWAY_CACHE_START_ENABLED + bool "Start gateway cache in deferred mode" + depends on GATEWAY_CACHE_SUPPORTED + default y + help + Starts the deferred RAM cache and background flush task at boot. Disable + this to keep scene and group persistence in direct-NVS mode. + +config GATEWAY_CACHE_RECONCILIATION_ENABLED + bool "Enable cache reconciliation tracking" + depends on GATEWAY_CACHE_SUPPORTED && GATEWAY_CACHE_START_ENABLED + default y + help + Tracks outside DALI bus mutations as update-needed flags for resumable + reconciliation work. + +config GATEWAY_CACHE_FULL_STATE_MIRROR + bool "Enable full DALI state mirror" + depends on GATEWAY_CACHE_RECONCILIATION_ENABLED + default n + help + Enables the heavier full bus-state mirror path for future reconciliation. + config GATEWAY_CACHE_FLUSH_INTERVAL_MS int "Cache flush interval ms" + depends on GATEWAY_CACHE_SUPPORTED && GATEWAY_CACHE_START_ENABLED range 100 600000 default 5000 help @@ -230,6 +261,7 @@ config GATEWAY_CACHE_FLUSH_INTERVAL_MS choice GATEWAY_CACHE_CONFLICT_PRIORITY prompt "Cache conflict priority default" + depends on GATEWAY_CACHE_RECONCILIATION_ENABLED default GATEWAY_CACHE_OUTSIDE_BUS_FIRST help Default source of truth to use when future cache reconciliation detects diff --git a/apps/gateway/main/app_main.cpp b/apps/gateway/main/app_main.cpp index 7283ac9..10f96d2 100644 --- a/apps/gateway/main/app_main.cpp +++ b/apps/gateway/main/app_main.cpp @@ -180,6 +180,30 @@ constexpr bool kCloudBridgeStartupEnabled = true; constexpr bool kCloudBridgeStartupEnabled = false; #endif +#ifdef CONFIG_GATEWAY_CACHE_SUPPORTED +constexpr bool kCacheSupported = true; +#else +constexpr bool kCacheSupported = false; +#endif + +#ifdef CONFIG_GATEWAY_CACHE_START_ENABLED +constexpr bool kCacheStartupEnabled = true; +#else +constexpr bool kCacheStartupEnabled = false; +#endif + +#ifdef CONFIG_GATEWAY_CACHE_RECONCILIATION_ENABLED +constexpr bool kCacheReconciliationEnabled = true; +#else +constexpr bool kCacheReconciliationEnabled = false; +#endif + +#ifdef CONFIG_GATEWAY_CACHE_FULL_STATE_MIRROR +constexpr bool kCacheFullStateMirrorEnabled = true; +#else +constexpr bool kCacheFullStateMirrorEnabled = false; +#endif + #ifdef CONFIG_GATEWAY_CACHE_LOCAL_GATEWAY_FIRST constexpr gateway::GatewayCachePriorityMode kCachePriorityMode = gateway::GatewayCachePriorityMode::kLocalGatewayFirst; @@ -417,6 +441,7 @@ extern "C" void app_main(void) { kProjectVersion, gateway::ReadRuntimeSerialId(), kBleStartupEnabled, + kCacheSupported && kCacheStartupEnabled, }, s_dali_domain.get()); ESP_ERROR_CHECK(s_runtime->start()); @@ -424,6 +449,10 @@ extern "C" void app_main(void) { ESP_ERROR_CHECK(BindConfiguredChannels(*s_dali_domain, *s_runtime)); gateway::GatewayCacheConfig cache_config; + cache_config.cache_enabled = kCacheSupported && kCacheStartupEnabled && s_runtime->cacheEnabled(); + cache_config.reconciliation_enabled = cache_config.cache_enabled && kCacheReconciliationEnabled; + cache_config.full_state_mirror_enabled = cache_config.reconciliation_enabled && + kCacheFullStateMirrorEnabled; cache_config.flush_interval_ms = static_cast(CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS); cache_config.default_priority_mode = kCachePriorityMode; s_cache = std::make_unique(cache_config); diff --git a/apps/gateway/sdkconfig b/apps/gateway/sdkconfig index d86ba61..b6ee934 100644 --- a/apps/gateway/sdkconfig +++ b/apps/gateway/sdkconfig @@ -621,6 +621,10 @@ CONFIG_GATEWAY_CHANNEL2_PHY_DISABLED=y # # Gateway Cache # +CONFIG_GATEWAY_CACHE_SUPPORTED=y +CONFIG_GATEWAY_CACHE_START_ENABLED=y +CONFIG_GATEWAY_CACHE_RECONCILIATION_ENABLED=y +# CONFIG_GATEWAY_CACHE_FULL_STATE_MIRROR is not set CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS=5000 CONFIG_GATEWAY_CACHE_OUTSIDE_BUS_FIRST=y # CONFIG_GATEWAY_CACHE_LOCAL_GATEWAY_FIRST is not set diff --git a/components/dali_domain/include/dali_domain.hpp b/components/dali_domain/include/dali_domain.hpp index 70d4561..068798d 100644 --- a/components/dali_domain/include/dali_domain.hpp +++ b/components/dali_domain/include/dali_domain.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -93,6 +94,21 @@ struct DaliDomainSnapshot { std::map> number_arrays; }; +struct DaliAddressSettingsSnapshot { + std::optional power_on_level; + std::optional system_failure_level; + std::optional min_level; + std::optional max_level; + std::optional fade_time; + std::optional fade_rate; + + bool anyKnown() const { + return power_on_level.has_value() || system_failure_level.has_value() || + min_level.has_value() || max_level.has_value() || fade_time.has_value() || + fade_rate.has_value(); + } +}; + class DaliDomainService { public: DaliDomainService(); @@ -141,6 +157,15 @@ class DaliDomainService { bool on(uint8_t gateway_id, int short_address) const; bool off(uint8_t gateway_id, int short_address) const; bool off(int short_address) const; + std::optional queryGroupMask(uint8_t gateway_id, int short_address) const; + std::optional querySceneLevel(uint8_t gateway_id, int short_address, int scene) const; + std::optional queryAddressSettings(uint8_t gateway_id, + int short_address) const; + bool applyGroupMask(uint8_t gateway_id, int short_address, uint16_t group_mask) const; + bool applySceneLevel(uint8_t gateway_id, int short_address, int scene, + std::optional level) const; + bool applyAddressSettings(uint8_t gateway_id, int short_address, + const DaliAddressSettingsSnapshot& settings) const; bool updateChannelName(uint8_t gateway_id, std::string_view name); bool allocateAllAddr(uint8_t gateway_id, int start_address = 0) const; void stopAllocAddr(uint8_t gateway_id) const; diff --git a/components/dali_domain/src/dali_domain.cpp b/components/dali_domain/src/dali_domain.cpp index c1b3a09..3f65c55 100644 --- a/components/dali_domain/src/dali_domain.cpp +++ b/components/dali_domain/src/dali_domain.cpp @@ -779,6 +779,125 @@ bool DaliDomainService::off(int short_address) const { return off(channels_.front()->config.gateway_id, short_address); } +std::optional DaliDomainService::queryGroupMask(uint8_t gateway_id, + int short_address) const { + const auto* channel = findChannelByGateway(gateway_id); + if (channel == nullptr || channel->dali == nullptr) { + return std::nullopt; + } + + const auto group_mask = channel->dali->base.getGroup(short_address); + if (!group_mask.has_value()) { + return std::nullopt; + } + + return static_cast(*group_mask); +} + +std::optional DaliDomainService::querySceneLevel(uint8_t gateway_id, int short_address, + int scene) const { + const auto* channel = findChannelByGateway(gateway_id); + if (channel == nullptr || channel->dali == nullptr) { + return std::nullopt; + } + + const auto level = channel->dali->base.getScene(short_address, scene); + if (!level.has_value()) { + return std::nullopt; + } + + return static_cast(*level); +} + +std::optional DaliDomainService::queryAddressSettings( + uint8_t gateway_id, int short_address) const { + const auto* channel = findChannelByGateway(gateway_id); + if (channel == nullptr || channel->dali == nullptr) { + return std::nullopt; + } + + DaliAddressSettingsSnapshot settings{}; + + if (const auto value = channel->dali->base.getPowerOnLevel(short_address); value.has_value()) { + settings.power_on_level = static_cast(*value); + } + if (const auto value = channel->dali->base.getSystemFailureLevel(short_address); + value.has_value()) { + settings.system_failure_level = static_cast(*value); + } + if (const auto value = channel->dali->base.getMinLevel(short_address); value.has_value()) { + settings.min_level = static_cast(*value); + } + if (const auto value = channel->dali->base.getMaxLevel(short_address); value.has_value()) { + settings.max_level = static_cast(*value); + } + if (const auto value = channel->dali->base.getFadeTime(short_address); value.has_value()) { + settings.fade_time = static_cast(*value); + } + if (const auto value = channel->dali->base.getFadeRate(short_address); value.has_value()) { + settings.fade_rate = static_cast(*value); + } + + if (!settings.anyKnown()) { + return std::nullopt; + } + + return settings; +} + +bool DaliDomainService::applyGroupMask(uint8_t gateway_id, int short_address, + uint16_t group_mask) const { + const auto* channel = findChannelByGateway(gateway_id); + return channel != nullptr && channel->dali != nullptr && + channel->dali->base.setGroup(short_address, group_mask); +} + +bool DaliDomainService::applySceneLevel(uint8_t gateway_id, int short_address, int scene, + std::optional level) const { + const auto* channel = findChannelByGateway(gateway_id); + if (channel == nullptr || channel->dali == nullptr || !level.has_value()) { + return false; + } + + if (*level == 255U) { + return channel->dali->base.removeScene(short_address, scene); + } + + return channel->dali->base.setDTR(*level) && + channel->dali->base.storeDTRAsSceneBright(short_address, scene); +} + +bool DaliDomainService::applyAddressSettings(uint8_t gateway_id, int short_address, + const DaliAddressSettingsSnapshot& settings) const { + const auto* channel = findChannelByGateway(gateway_id); + if (channel == nullptr || channel->dali == nullptr) { + return false; + } + + bool ok = true; + if (settings.power_on_level.has_value()) { + ok = ok && channel->dali->base.setPowerOnLevel(short_address, *settings.power_on_level); + } + if (settings.system_failure_level.has_value()) { + ok = ok && + channel->dali->base.setSystemFailureLevel(short_address, *settings.system_failure_level); + } + if (settings.min_level.has_value()) { + ok = ok && channel->dali->base.setMinLevel(short_address, *settings.min_level); + } + if (settings.max_level.has_value()) { + ok = ok && channel->dali->base.setMaxLevel(short_address, *settings.max_level); + } + if (settings.fade_time.has_value()) { + ok = ok && channel->dali->base.setFadeTime(short_address, *settings.fade_time); + } + if (settings.fade_rate.has_value()) { + ok = ok && channel->dali->base.setFadeRate(short_address, *settings.fade_rate); + } + + return ok; +} + bool DaliDomainService::updateChannelName(uint8_t gateway_id, std::string_view name) { auto* channel = findChannelByGateway(gateway_id); if (channel == nullptr) { diff --git a/components/gateway_cache/include/gateway_cache.hpp b/components/gateway_cache/include/gateway_cache.hpp index 73c2580..7e86d3f 100644 --- a/components/gateway_cache/include/gateway_cache.hpp +++ b/components/gateway_cache/include/gateway_cache.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "esp_err.h" @@ -20,18 +21,48 @@ enum class GatewayCachePriorityMode : uint8_t { struct GatewayCacheConfig { std::string storage_namespace{"gateway_rt"}; + bool cache_enabled{true}; + bool reconciliation_enabled{true}; + bool full_state_mirror_enabled{false}; uint32_t flush_interval_ms{5000}; uint32_t task_stack_size{4096}; UBaseType_t task_priority{3}; GatewayCachePriorityMode default_priority_mode{GatewayCachePriorityMode::kOutsideBusFirst}; }; +enum class GatewayCacheRawFrameOrigin : uint8_t { + kLocalGateway = 0, + kOutsideBus = 1, +}; + struct GatewayCacheChannelFlags { bool need_update_group{false}; bool need_update_scene{false}; bool need_update_settings{false}; }; +struct GatewayCacheDaliSettingsSnapshot { + std::optional power_on_level; + std::optional system_failure_level; + std::optional min_level; + std::optional max_level; + std::optional fade_time; + std::optional fade_rate; + + bool anyKnown() const { + return power_on_level.has_value() || system_failure_level.has_value() || + min_level.has_value() || max_level.has_value() || fade_time.has_value() || + fade_rate.has_value(); + } +}; + +struct GatewayCacheDaliAddressState { + bool group_mask_known{false}; + uint16_t group_mask{0}; + std::array, 16> scene_levels{}; + GatewayCacheDaliSettingsSnapshot settings; +}; + class GatewayCache { public: struct SceneEntry { @@ -80,20 +111,47 @@ class GatewayCache { std::pair groupMask(uint8_t gateway_id); GatewayCacheChannelFlags channelFlags(uint8_t gateway_id); + GatewayCacheChannelFlags pendingChannelFlags(uint8_t gateway_id); + GatewayCacheDaliAddressState daliAddressState(uint8_t gateway_id, uint8_t short_address); + bool setDaliGroupMask(uint8_t gateway_id, uint8_t short_address, + std::optional group_mask); + bool setDaliSceneLevel(uint8_t gateway_id, uint8_t short_address, uint8_t scene_id, + std::optional level); + bool setDaliSettings(uint8_t gateway_id, uint8_t short_address, + std::optional settings); + bool clearChannelFlagsIfMatched(uint8_t gateway_id, const GatewayCacheChannelFlags& flags); void markGroupUpdateNeeded(uint8_t gateway_id, bool needed = true); void markSceneUpdateNeeded(uint8_t gateway_id, bool needed = true); void markSettingsUpdateNeeded(uint8_t gateway_id, bool needed = true); + bool cacheEnabled() const; + bool reconciliationEnabled() const; + bool fullStateMirrorEnabled() const; + bool observeDaliCommand(uint8_t gateway_id, uint8_t raw_addr, uint8_t command, + GatewayCacheRawFrameOrigin origin); + GatewayCachePriorityMode priorityMode(); void setPriorityMode(GatewayCachePriorityMode mode); private: + struct DtrState { + std::optional dtr0; + std::optional dtr1; + std::optional dtr2; + }; + static void TaskEntry(void* arg); void taskLoop(); bool flushDirty(); bool openStorageLocked(); void closeStorageLocked(); + bool persistSceneLocked(uint8_t gateway_id, uint8_t scene_id, const SceneEntry& scene); + bool persistGroupLocked(uint8_t gateway_id, uint8_t group_id, const GroupEntry& group); + bool commitStorageLocked(); + bool shouldTrackUpdateFlagsLocked() const; + GatewayCacheDaliAddressState& ensureDaliAddressStateLocked(uint8_t gateway_id, + uint8_t short_address); SceneStore& ensureSceneStoreLocked(uint8_t gateway_id); GroupStore& ensureGroupStoreLocked(uint8_t gateway_id); void loadSceneStoreLocked(uint8_t gateway_id, SceneStore& scenes); @@ -109,6 +167,8 @@ class GatewayCache { nvs_handle_t storage_{0}; std::map scenes_; std::map groups_; + std::map> dali_states_; + std::map dtr_states_; std::map channel_flags_; bool dirty_{false}; }; diff --git a/components/gateway_cache/src/gateway_cache.cpp b/components/gateway_cache/src/gateway_cache.cpp index 25eef84..ef88c0f 100644 --- a/components/gateway_cache/src/gateway_cache.cpp +++ b/components/gateway_cache/src/gateway_cache.cpp @@ -14,6 +14,26 @@ namespace { constexpr const char* kTag = "gateway_cache"; constexpr size_t kMaxNameBytes = 32; +constexpr uint8_t kDaliCmdReset = 0x20; +constexpr uint8_t kDaliCmdStoreDtrAsMaxLevel = 0x2A; +constexpr uint8_t kDaliCmdStoreDtrAsMinLevel = 0x2B; +constexpr uint8_t kDaliCmdStoreDtrAsSystemFailureLevel = 0x2C; +constexpr uint8_t kDaliCmdStoreDtrAsPowerOnLevel = 0x2D; +constexpr uint8_t kDaliCmdStoreDtrAsFadeTime = 0x2E; +constexpr uint8_t kDaliCmdStoreDtrAsFadeRate = 0x2F; +constexpr uint8_t kDaliCmdSetSceneMin = 0x40; +constexpr uint8_t kDaliCmdRemoveSceneMax = 0x5F; +constexpr uint8_t kDaliCmdAddToGroupMin = 0x60; +constexpr uint8_t kDaliCmdRemoveFromGroupMax = 0x7F; +constexpr uint8_t kDaliCmdStoreDtrAsShortAddress = 0x80; +constexpr uint8_t kDaliCmdSetDtr0 = 0xA3; +constexpr uint8_t kDaliCmdSpecialProgramShortAddress = 0xB7; +constexpr uint8_t kDaliCmdSetDtr1 = 0xC3; +constexpr uint8_t kDaliCmdSetDtr2 = 0xC5; +constexpr uint8_t kDaliCmdDt8StoreDtrAsColorX = 0xE0; +constexpr uint8_t kDaliCmdDt8StoreDtrAsColorY = 0xE1; +constexpr uint8_t kDaliCmdDt8StorePrimaryMin = 0xF0; +constexpr uint8_t kDaliCmdDt8StartAutoCalibration = 0xF6; class LockGuard { public: @@ -74,6 +94,95 @@ bool IsDefaultGroup(const GatewayCache::GroupEntry& group) { return !group.enabled && group.target_type == 2 && group.target_value == 0; } +bool SameFlags(const GatewayCacheChannelFlags& lhs, const GatewayCacheChannelFlags& rhs) { + return lhs.need_update_group == rhs.need_update_group && + lhs.need_update_scene == rhs.need_update_scene && + lhs.need_update_settings == rhs.need_update_settings; +} + +bool AnyFlagSet(const GatewayCacheChannelFlags& flags) { + return flags.need_update_group || flags.need_update_scene || flags.need_update_settings; +} + +std::optional DecodeShortAddress(uint8_t raw_addr) { + if (raw_addr <= 0x7F && (raw_addr & 0x01) != 0) { + return static_cast(raw_addr >> 1); + } + return std::nullopt; +} + +bool ShouldMirrorObservedMutation(GatewayCacheRawFrameOrigin origin, + GatewayCachePriorityMode priority_mode) { + return origin == GatewayCacheRawFrameOrigin::kLocalGateway || + priority_mode == GatewayCachePriorityMode::kOutsideBusFirst; +} + +void ClearDaliState(GatewayCacheDaliAddressState& state) { + state.group_mask_known = false; + state.group_mask = 0; + state.scene_levels.fill(std::nullopt); + state.settings = {}; +} + +void ApplyObservedSettingsValue(GatewayCacheDaliSettingsSnapshot& settings, uint8_t command, + uint8_t value) { + switch (command) { + case kDaliCmdStoreDtrAsMaxLevel: + settings.max_level = value; + break; + case kDaliCmdStoreDtrAsMinLevel: + settings.min_level = value; + break; + case kDaliCmdStoreDtrAsSystemFailureLevel: + settings.system_failure_level = value; + break; + case kDaliCmdStoreDtrAsPowerOnLevel: + settings.power_on_level = value; + break; + case kDaliCmdStoreDtrAsFadeTime: + settings.fade_time = value; + break; + case kDaliCmdStoreDtrAsFadeRate: + settings.fade_rate = value; + break; + default: + break; + } +} + +GatewayCacheChannelFlags ClassifyDaliMutation(uint8_t raw_addr, uint8_t command) { + GatewayCacheChannelFlags flags; + if (raw_addr == kDaliCmdSpecialProgramShortAddress) { + flags.need_update_settings = true; + return flags; + } + + const bool special_command = raw_addr >= 0xA1 && raw_addr <= 0xC5 && (raw_addr & 0x01) != 0; + if (special_command || (raw_addr & 0x01) == 0) { + return flags; + } + + if (command == kDaliCmdReset) { + flags.need_update_group = true; + flags.need_update_scene = true; + flags.need_update_settings = true; + } else if (command >= kDaliCmdStoreDtrAsMaxLevel && command <= kDaliCmdStoreDtrAsFadeRate) { + flags.need_update_settings = true; + } else if (command >= kDaliCmdSetSceneMin && command <= kDaliCmdRemoveSceneMax) { + flags.need_update_scene = true; + } else if (command >= kDaliCmdAddToGroupMin && command <= kDaliCmdRemoveFromGroupMax) { + flags.need_update_group = true; + } else if (command == kDaliCmdStoreDtrAsShortAddress) { + flags.need_update_settings = true; + } else if (command == kDaliCmdDt8StoreDtrAsColorX || command == kDaliCmdDt8StoreDtrAsColorY || + (command >= kDaliCmdDt8StorePrimaryMin && + command <= kDaliCmdDt8StartAutoCalibration)) { + flags.need_update_settings = true; + } + + return flags; +} + std::string BuildScenePayload(const GatewayCache::SceneEntry& scene) { char payload[32] = {0}; std::snprintf(payload, sizeof(payload), "%u,%u,%u,%u,%u,%u", scene.enabled ? 1 : 0, @@ -103,7 +212,9 @@ GatewayCache::~GatewayCache() { { LockGuard guard(lock_); - flushDirty(); + if (config_.cache_enabled) { + flushDirty(); + } closeStorageLocked(); } @@ -121,6 +232,12 @@ esp_err_t GatewayCache::start() { } } + if (!config_.cache_enabled) { + ESP_LOGI(kTag, "cache disabled namespace=%s persistence=direct-nvs", + config_.storage_namespace.c_str()); + return ESP_OK; + } + if (task_handle_ != nullptr) { return ESP_OK; } @@ -134,24 +251,38 @@ esp_err_t GatewayCache::start() { return ESP_ERR_NO_MEM; } - ESP_LOGI(kTag, "cache started namespace=%s flush_interval_ms=%u", - config_.storage_namespace.c_str(), static_cast(config_.flush_interval_ms)); + ESP_LOGI(kTag, "cache started namespace=%s flush_interval_ms=%u reconciliation=%d full_mirror=%d", + config_.storage_namespace.c_str(), static_cast(config_.flush_interval_ms), + config_.reconciliation_enabled, config_.full_state_mirror_enabled); return ESP_OK; } void GatewayCache::preloadChannel(uint8_t gateway_id) { LockGuard guard(lock_); + if (!config_.cache_enabled) { + return; + } ensureSceneStoreLocked(gateway_id); ensureGroupStoreLocked(gateway_id); } GatewayCache::SceneStore GatewayCache::scenes(uint8_t gateway_id) { LockGuard guard(lock_); + if (!config_.cache_enabled) { + SceneStore store; + loadSceneStoreLocked(gateway_id, store); + return store; + } return ensureSceneStoreLocked(gateway_id); } GatewayCache::GroupStore GatewayCache::groups(uint8_t gateway_id) { LockGuard guard(lock_); + if (!config_.cache_enabled) { + GroupStore store; + loadGroupStoreLocked(gateway_id, store); + return store; + } return ensureGroupStoreLocked(gateway_id); } @@ -160,6 +291,11 @@ GatewayCache::SceneEntry GatewayCache::scene(uint8_t gateway_id, uint8_t scene_i if (scene_id >= 16) { return {}; } + if (!config_.cache_enabled) { + SceneStore store; + loadSceneStoreLocked(gateway_id, store); + return store[scene_id]; + } return ensureSceneStoreLocked(gateway_id)[scene_id]; } @@ -168,6 +304,11 @@ GatewayCache::GroupEntry GatewayCache::group(uint8_t gateway_id, uint8_t group_i if (group_id >= 16) { return {}; } + if (!config_.cache_enabled) { + GroupStore store; + loadGroupStoreLocked(gateway_id, store); + return store[group_id]; + } return ensureGroupStoreLocked(gateway_id)[group_id]; } @@ -176,6 +317,16 @@ bool GatewayCache::setSceneEnabled(uint8_t gateway_id, uint8_t scene_id, bool en if (scene_id >= 16) { return false; } + if (!config_.cache_enabled) { + SceneStore store; + loadSceneStoreLocked(gateway_id, store); + auto& entry = store[scene_id]; + if (entry.enabled == enabled) { + return true; + } + entry.enabled = enabled; + return persistSceneLocked(gateway_id, scene_id, entry); + } auto& entry = ensureSceneStoreLocked(gateway_id)[scene_id]; if (entry.enabled == enabled) { return true; @@ -192,6 +343,21 @@ bool GatewayCache::setSceneDetail(uint8_t gateway_id, uint8_t scene_id, uint8_t if (scene_id >= 16) { return false; } + if (!config_.cache_enabled) { + SceneStore store; + loadSceneStoreLocked(gateway_id, store); + auto& entry = store[scene_id]; + if (entry.brightness == brightness && entry.color_mode == color_mode && entry.data1 == data1 && + entry.data2 == data2 && entry.data3 == data3) { + return true; + } + entry.brightness = brightness; + entry.color_mode = color_mode; + entry.data1 = data1; + entry.data2 = data2; + entry.data3 = data3; + return persistSceneLocked(gateway_id, scene_id, entry); + } auto& entry = ensureSceneStoreLocked(gateway_id)[scene_id]; if (entry.brightness == brightness && entry.color_mode == color_mode && entry.data1 == data1 && entry.data2 == data2 && entry.data3 == data3) { @@ -211,6 +377,17 @@ bool GatewayCache::setSceneName(uint8_t gateway_id, uint8_t scene_id, std::strin if (scene_id >= 16) { return false; } + if (!config_.cache_enabled) { + SceneStore store; + loadSceneStoreLocked(gateway_id, store); + auto& entry = store[scene_id]; + const auto normalized = NormalizeName(name); + if (entry.name == normalized) { + return true; + } + entry.name = normalized; + return persistSceneLocked(gateway_id, scene_id, entry); + } auto& entry = ensureSceneStoreLocked(gateway_id)[scene_id]; const auto normalized = NormalizeName(name); if (entry.name == normalized) { @@ -226,6 +403,16 @@ bool GatewayCache::deleteScene(uint8_t gateway_id, uint8_t scene_id) { if (scene_id >= 16) { return false; } + if (!config_.cache_enabled) { + SceneStore store; + loadSceneStoreLocked(gateway_id, store); + auto& entry = store[scene_id]; + if (IsDefaultScene(entry) && entry.name.empty()) { + return true; + } + entry = SceneEntry{}; + return persistSceneLocked(gateway_id, scene_id, entry); + } auto& entry = ensureSceneStoreLocked(gateway_id)[scene_id]; if (IsDefaultScene(entry) && entry.name.empty()) { return true; @@ -237,7 +424,11 @@ bool GatewayCache::deleteScene(uint8_t gateway_id, uint8_t scene_id) { std::pair GatewayCache::sceneMask(uint8_t gateway_id) { LockGuard guard(lock_); - const auto& store = ensureSceneStoreLocked(gateway_id); + SceneStore direct_store; + if (!config_.cache_enabled) { + loadSceneStoreLocked(gateway_id, direct_store); + } + const auto& store = config_.cache_enabled ? ensureSceneStoreLocked(gateway_id) : direct_store; uint16_t mask = 0; for (size_t index = 0; index < store.size(); ++index) { if (store[index].enabled) { @@ -252,6 +443,16 @@ bool GatewayCache::setGroupEnabled(uint8_t gateway_id, uint8_t group_id, bool en if (group_id >= 16) { return false; } + if (!config_.cache_enabled) { + GroupStore store; + loadGroupStoreLocked(gateway_id, store); + auto& entry = store[group_id]; + if (entry.enabled == enabled) { + return true; + } + entry.enabled = enabled; + return persistGroupLocked(gateway_id, group_id, entry); + } auto& entry = ensureGroupStoreLocked(gateway_id)[group_id]; if (entry.enabled == enabled) { return true; @@ -267,6 +468,17 @@ bool GatewayCache::setGroupDetail(uint8_t gateway_id, uint8_t group_id, uint8_t if (group_id >= 16) { return false; } + if (!config_.cache_enabled) { + GroupStore store; + loadGroupStoreLocked(gateway_id, store); + auto& entry = store[group_id]; + if (entry.target_type == target_type && entry.target_value == target_value) { + return true; + } + entry.target_type = target_type; + entry.target_value = target_value; + return persistGroupLocked(gateway_id, group_id, entry); + } auto& entry = ensureGroupStoreLocked(gateway_id)[group_id]; if (entry.target_type == target_type && entry.target_value == target_value) { return true; @@ -282,6 +494,17 @@ bool GatewayCache::setGroupName(uint8_t gateway_id, uint8_t group_id, std::strin if (group_id >= 16) { return false; } + if (!config_.cache_enabled) { + GroupStore store; + loadGroupStoreLocked(gateway_id, store); + auto& entry = store[group_id]; + const auto normalized = NormalizeName(name); + if (entry.name == normalized) { + return true; + } + entry.name = normalized; + return persistGroupLocked(gateway_id, group_id, entry); + } auto& entry = ensureGroupStoreLocked(gateway_id)[group_id]; const auto normalized = NormalizeName(name); if (entry.name == normalized) { @@ -297,6 +520,16 @@ bool GatewayCache::deleteGroup(uint8_t gateway_id, uint8_t group_id) { if (group_id >= 16) { return false; } + if (!config_.cache_enabled) { + GroupStore store; + loadGroupStoreLocked(gateway_id, store); + auto& entry = store[group_id]; + if (IsDefaultGroup(entry) && entry.name.empty()) { + return true; + } + entry = GroupEntry{}; + return persistGroupLocked(gateway_id, group_id, entry); + } auto& entry = ensureGroupStoreLocked(gateway_id)[group_id]; if (IsDefaultGroup(entry) && entry.name.empty()) { return true; @@ -308,7 +541,11 @@ bool GatewayCache::deleteGroup(uint8_t gateway_id, uint8_t group_id) { std::pair GatewayCache::groupMask(uint8_t gateway_id) { LockGuard guard(lock_); - const auto& store = ensureGroupStoreLocked(gateway_id); + GroupStore direct_store; + if (!config_.cache_enabled) { + loadGroupStoreLocked(gateway_id, direct_store); + } + const auto& store = config_.cache_enabled ? ensureGroupStoreLocked(gateway_id) : direct_store; uint16_t mask = 0; for (size_t index = 0; index < store.size(); ++index) { if (store[index].enabled) { @@ -320,24 +557,191 @@ std::pair GatewayCache::groupMask(uint8_t gateway_id) { GatewayCacheChannelFlags GatewayCache::channelFlags(uint8_t gateway_id) { LockGuard guard(lock_); + if (!shouldTrackUpdateFlagsLocked()) { + return {}; + } return channel_flags_[gateway_id]; } +GatewayCacheChannelFlags GatewayCache::pendingChannelFlags(uint8_t gateway_id) { + LockGuard guard(lock_); + return shouldTrackUpdateFlagsLocked() ? channel_flags_[gateway_id] : GatewayCacheChannelFlags{}; +} + +GatewayCacheDaliAddressState GatewayCache::daliAddressState(uint8_t gateway_id, + uint8_t short_address) { + LockGuard guard(lock_); + if (short_address >= 64) { + return {}; + } + return ensureDaliAddressStateLocked(gateway_id, short_address); +} + +bool GatewayCache::setDaliGroupMask(uint8_t gateway_id, uint8_t short_address, + std::optional group_mask) { + LockGuard guard(lock_); + if (short_address >= 64) { + return false; + } + + auto& state = ensureDaliAddressStateLocked(gateway_id, short_address); + state.group_mask_known = group_mask.has_value(); + state.group_mask = group_mask.value_or(0); + return true; +} + +bool GatewayCache::setDaliSceneLevel(uint8_t gateway_id, uint8_t short_address, uint8_t scene_id, + std::optional level) { + LockGuard guard(lock_); + if (short_address >= 64 || scene_id >= 16) { + return false; + } + + auto& state = ensureDaliAddressStateLocked(gateway_id, short_address); + state.scene_levels[scene_id] = level; + return true; +} + +bool GatewayCache::setDaliSettings(uint8_t gateway_id, uint8_t short_address, + std::optional settings) { + LockGuard guard(lock_); + if (short_address >= 64) { + return false; + } + + auto& state = ensureDaliAddressStateLocked(gateway_id, short_address); + state.settings = settings.value_or(GatewayCacheDaliSettingsSnapshot{}); + return true; +} + +bool GatewayCache::clearChannelFlagsIfMatched(uint8_t gateway_id, + const GatewayCacheChannelFlags& flags) { + LockGuard guard(lock_); + if (!shouldTrackUpdateFlagsLocked()) { + return true; + } + auto& current = channel_flags_[gateway_id]; + if (!SameFlags(current, flags)) { + return false; + } + current = {}; + return true; +} + void GatewayCache::markGroupUpdateNeeded(uint8_t gateway_id, bool needed) { LockGuard guard(lock_); + if (!shouldTrackUpdateFlagsLocked()) { + return; + } channel_flags_[gateway_id].need_update_group = needed; } void GatewayCache::markSceneUpdateNeeded(uint8_t gateway_id, bool needed) { LockGuard guard(lock_); + if (!shouldTrackUpdateFlagsLocked()) { + return; + } channel_flags_[gateway_id].need_update_scene = needed; } void GatewayCache::markSettingsUpdateNeeded(uint8_t gateway_id, bool needed) { LockGuard guard(lock_); + if (!shouldTrackUpdateFlagsLocked()) { + return; + } channel_flags_[gateway_id].need_update_settings = needed; } +bool GatewayCache::cacheEnabled() const { + return config_.cache_enabled; +} + +bool GatewayCache::reconciliationEnabled() const { + return config_.cache_enabled && config_.reconciliation_enabled; +} + +bool GatewayCache::fullStateMirrorEnabled() const { + return reconciliationEnabled() && config_.full_state_mirror_enabled; +} + +bool GatewayCache::observeDaliCommand(uint8_t gateway_id, uint8_t raw_addr, uint8_t command, + GatewayCacheRawFrameOrigin origin) { + LockGuard guard(lock_); + if (!shouldTrackUpdateFlagsLocked()) { + return false; + } + + auto& dtr_state = dtr_states_[gateway_id]; + if (raw_addr == kDaliCmdSetDtr0) { + dtr_state.dtr0 = command; + return false; + } + if (raw_addr == kDaliCmdSetDtr1) { + dtr_state.dtr1 = command; + return false; + } + if (raw_addr == kDaliCmdSetDtr2) { + dtr_state.dtr2 = command; + return false; + } + + const auto detected = ClassifyDaliMutation(raw_addr, command); + if (!AnyFlagSet(detected)) { + return false; + } + + if (ShouldMirrorObservedMutation(origin, priority_mode_)) { + if (command == kDaliCmdReset) { + if (const auto short_address = DecodeShortAddress(raw_addr); short_address.has_value()) { + ClearDaliState(ensureDaliAddressStateLocked(gateway_id, *short_address)); + } else if (auto states = dali_states_.find(gateway_id); states != dali_states_.end()) { + for (auto& state : states->second) { + ClearDaliState(state); + } + } + } else if (const auto short_address = DecodeShortAddress(raw_addr); short_address.has_value()) { + auto& state = ensureDaliAddressStateLocked(gateway_id, *short_address); + + if (command >= kDaliCmdAddToGroupMin && command <= kDaliCmdRemoveFromGroupMax && + state.group_mask_known) { + const uint16_t bit = static_cast(1U << (command & 0x0F)); + if (command < (kDaliCmdAddToGroupMin + 16)) { + state.group_mask |= bit; + } else { + state.group_mask &= static_cast(~bit); + } + } else if (command >= kDaliCmdSetSceneMin && command < (kDaliCmdSetSceneMin + 16) && + dtr_state.dtr0.has_value()) { + state.scene_levels[command - kDaliCmdSetSceneMin] = *dtr_state.dtr0; + } else if (command >= (kDaliCmdSetSceneMin + 16) && command <= kDaliCmdRemoveSceneMax) { + state.scene_levels[command - (kDaliCmdSetSceneMin + 16)] = 255U; + } else if (command >= kDaliCmdStoreDtrAsMaxLevel && command <= kDaliCmdStoreDtrAsFadeRate && + dtr_state.dtr0.has_value()) { + ApplyObservedSettingsValue(state.settings, command, *dtr_state.dtr0); + } + } + } + + if (origin != GatewayCacheRawFrameOrigin::kOutsideBus) { + return false; + } + + auto& current = channel_flags_[gateway_id]; + const bool changed = (!current.need_update_group && detected.need_update_group) || + (!current.need_update_scene && detected.need_update_scene) || + (!current.need_update_settings && detected.need_update_settings); + current.need_update_group = current.need_update_group || detected.need_update_group; + current.need_update_scene = current.need_update_scene || detected.need_update_scene; + current.need_update_settings = current.need_update_settings || detected.need_update_settings; + + if (changed) { + ESP_LOGI(kTag, "outside DALI mutation gateway=%u addr=0x%02x cmd=0x%02x flags g=%d s=%d cfg=%d", + gateway_id, raw_addr, command, current.need_update_group, current.need_update_scene, + current.need_update_settings); + } + return changed; +} + GatewayCachePriorityMode GatewayCache::priorityMode() { LockGuard guard(lock_); return priority_mode_; @@ -363,6 +767,9 @@ void GatewayCache::taskLoop() { bool GatewayCache::flushDirty() { LockGuard guard(lock_); + if (!config_.cache_enabled) { + return true; + } if (!dirty_) { return true; } @@ -441,6 +848,79 @@ void GatewayCache::closeStorageLocked() { } } +bool GatewayCache::persistSceneLocked(uint8_t gateway_id, uint8_t scene_id, + const SceneEntry& scene) { + if (!openStorageLocked()) { + return false; + } + + if (!IsDefaultScene(scene)) { + if (!writeStringLocked(ShortKey("sc", gateway_id, scene_id), BuildScenePayload(scene))) { + return false; + } + } else if (!eraseKeyLocked(ShortKey("sc", gateway_id, scene_id))) { + return false; + } + + if (!scene.name.empty()) { + if (!writeStringLocked(ShortKey("sn", gateway_id, scene_id), scene.name)) { + return false; + } + } else if (!eraseKeyLocked(ShortKey("sn", gateway_id, scene_id))) { + return false; + } + + return commitStorageLocked(); +} + +bool GatewayCache::persistGroupLocked(uint8_t gateway_id, uint8_t group_id, + const GroupEntry& group) { + if (!openStorageLocked()) { + return false; + } + + if (!IsDefaultGroup(group)) { + if (!writeStringLocked(ShortKey("gr", gateway_id, group_id), BuildGroupPayload(group))) { + return false; + } + } else if (!eraseKeyLocked(ShortKey("gr", gateway_id, group_id))) { + return false; + } + + if (!group.name.empty()) { + if (!writeStringLocked(ShortKey("gn", gateway_id, group_id), group.name)) { + return false; + } + } else if (!eraseKeyLocked(ShortKey("gn", gateway_id, group_id))) { + return false; + } + + return commitStorageLocked(); +} + +bool GatewayCache::commitStorageLocked() { + if (storage_ == 0) { + return false; + } + const esp_err_t commit_err = nvs_commit(storage_); + if (commit_err != ESP_OK) { + ESP_LOGE(kTag, "cache commit failed: %s", esp_err_to_name(commit_err)); + return false; + } + return true; +} + +bool GatewayCache::shouldTrackUpdateFlagsLocked() const { + return config_.cache_enabled && config_.reconciliation_enabled; +} + +GatewayCacheDaliAddressState& GatewayCache::ensureDaliAddressStateLocked(uint8_t gateway_id, + uint8_t short_address) { + auto [it, inserted] = dali_states_.try_emplace(gateway_id); + (void)inserted; + return it->second[short_address]; +} + GatewayCache::SceneStore& GatewayCache::ensureSceneStoreLocked(uint8_t gateway_id) { auto [it, inserted] = scenes_.try_emplace(gateway_id); if (inserted) { diff --git a/components/gateway_controller/include/gateway_controller.hpp b/components/gateway_controller/include/gateway_controller.hpp index 97759de..65a735a 100644 --- a/components/gateway_controller/include/gateway_controller.hpp +++ b/components/gateway_controller/include/gateway_controller.hpp @@ -1,8 +1,10 @@ #pragma once #include +#include #include #include +#include #include #include #include @@ -10,6 +12,7 @@ #include "gateway_cache.hpp" #include "esp_err.h" #include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" #include "freertos/task.h" namespace gateway { @@ -83,9 +86,30 @@ class GatewayController { GatewayControllerSnapshot snapshot(); private: + struct ReconciliationJob { + enum class Phase : uint8_t { + kReloadFlags = 0, + kGroups = 1, + kScenes = 2, + kSettings = 3, + }; + + GatewayCacheChannelFlags flags{}; + Phase phase{Phase::kReloadFlags}; + uint8_t short_address{0}; + uint8_t scene_id{0}; + }; + static void TaskEntry(void* arg); void taskLoop(); void dispatchCommand(const std::vector& command); + void scheduleReconciliation(uint8_t gateway_id); + bool hasPendingReconciliation() const; + bool runMaintenanceStep(); + bool runReconciliationStep(uint8_t gateway_id, ReconciliationJob& job); + void reconcileGroupStep(uint8_t gateway_id, uint8_t short_address); + void reconcileSceneStep(uint8_t gateway_id, uint8_t short_address, uint8_t scene_id); + void reconcileSettingsStep(uint8_t gateway_id, uint8_t short_address); bool hasGateway(uint8_t gateway_id) const; std::vector gatewayIds() const; @@ -131,10 +155,13 @@ class GatewayController { GatewayCache& cache_; GatewayControllerConfig config_; TaskHandle_t task_handle_{nullptr}; + SemaphoreHandle_t maintenance_lock_{nullptr}; std::vector notification_sinks_; std::vector ble_state_sinks_; std::vector wifi_state_sinks_; std::vector gateway_name_sinks_; + std::map reconciliation_jobs_; + std::atomic maintenance_activity_gateway_{-1}; bool setup_mode_{false}; bool wireless_setup_mode_{false}; bool ble_enabled_{false}; diff --git a/components/gateway_controller/src/gateway_controller.cpp b/components/gateway_controller/src/gateway_controller.cpp index cf18679..4911ba9 100644 --- a/components/gateway_controller/src/gateway_controller.cpp +++ b/components/gateway_controller/src/gateway_controller.cpp @@ -16,6 +16,31 @@ namespace { constexpr const char* kTag = "gateway_controller"; constexpr size_t kMaxNameBytes = 32; +constexpr uint8_t kDaliShortAddressCount = 64; +constexpr uint8_t kDaliSceneCount = 16; +constexpr TickType_t kMaintenancePollTicks = pdMS_TO_TICKS(20); + +class LockGuard { + public: + explicit LockGuard(SemaphoreHandle_t lock) : lock_(lock) { + if (lock_ != nullptr) { + xSemaphoreTake(lock_, portMAX_DELAY); + } + } + + ~LockGuard() { + if (lock_ != nullptr) { + xSemaphoreGive(lock_); + } + } + + private: + SemaphoreHandle_t lock_; +}; + +bool AnyFlagSet(const GatewayCacheChannelFlags& flags) { + return flags.need_update_group || flags.need_update_scene || flags.need_update_settings; +} std::string NormalizeName(std::string_view name) { std::string normalized(name); @@ -71,9 +96,18 @@ const char* PhyKindToString(DaliPhyKind phy_kind) { GatewayController::GatewayController(GatewayRuntime& runtime, DaliDomainService& dali_domain, GatewayCache& cache, GatewayControllerConfig config) - : runtime_(runtime), dali_domain_(dali_domain), cache_(cache), config_(config) {} + : runtime_(runtime), + dali_domain_(dali_domain), + cache_(cache), + config_(config), + maintenance_lock_(xSemaphoreCreateMutex()) {} -GatewayController::~GatewayController() = default; +GatewayController::~GatewayController() { + if (maintenance_lock_ != nullptr) { + vSemaphoreDelete(maintenance_lock_); + maintenance_lock_ = nullptr; + } +} esp_err_t GatewayController::start() { const auto device_info = runtime_.deviceInfo(); @@ -112,7 +146,7 @@ bool GatewayController::enqueueCommandFrame(const std::vector& frame) { ESP_LOGW(kTag, "dropped invalid command frame len=%u", static_cast(frame.size())); return false; } - if (!runtime_.enqueueCommand(frame)) { + if (!runtime_.enqueueCommand(frame, GatewayRuntime::classifyCommandPriority(frame))) { if (runtime_.lastEnqueueDropReason() != GatewayRuntime::CommandDropReason::kDuplicate) { ESP_LOGW(kTag, "dropped command frame reason=%d", static_cast(runtime_.lastEnqueueDropReason())); @@ -210,15 +244,223 @@ void GatewayController::TaskEntry(void* arg) { void GatewayController::taskLoop() { while (true) { - bool drained = false; - while (auto command = runtime_.popNextCommand()) { - drained = true; + bool worked = false; + if (auto command = runtime_.popNextCommand()) { + worked = true; dispatchCommand(*command); runtime_.completeCurrentCommand(); } - if (!drained) { - ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + if (!worked) { + worked = runMaintenanceStep(); } + if (!worked) { + ulTaskNotifyTake(pdTRUE, hasPendingReconciliation() ? kMaintenancePollTicks : portMAX_DELAY); + } + } +} + +void GatewayController::scheduleReconciliation(uint8_t gateway_id) { + if (!cache_.reconciliationEnabled()) { + return; + } + + auto flags = cache_.pendingChannelFlags(gateway_id); + if (cache_.fullStateMirrorEnabled() && AnyFlagSet(flags)) { + flags = {true, true, true}; + } + if (!AnyFlagSet(flags)) { + return; + } + + { + LockGuard guard(maintenance_lock_); + reconciliation_jobs_.try_emplace(gateway_id); + } + + if (task_handle_ != nullptr) { + xTaskNotifyGive(task_handle_); + } +} + +bool GatewayController::hasPendingReconciliation() const { + LockGuard guard(maintenance_lock_); + return !reconciliation_jobs_.empty(); +} + +bool GatewayController::runMaintenanceStep() { + if (!cache_.reconciliationEnabled()) { + return false; + } + + uint8_t gateway_id = 0; + ReconciliationJob job; + { + LockGuard guard(maintenance_lock_); + if (reconciliation_jobs_.empty()) { + return false; + } + const auto it = reconciliation_jobs_.begin(); + gateway_id = it->first; + job = it->second; + } + + if (runtime_.shouldYieldMaintenance(gateway_id)) { + return false; + } + + const bool keep_job = runReconciliationStep(gateway_id, job); + + { + LockGuard guard(maintenance_lock_); + auto it = reconciliation_jobs_.find(gateway_id); + if (it == reconciliation_jobs_.end()) { + return true; + } + if (keep_job) { + it->second = job; + } else { + reconciliation_jobs_.erase(it); + } + } + return true; +} + +bool GatewayController::runReconciliationStep(uint8_t gateway_id, ReconciliationJob& job) { + if (job.phase == ReconciliationJob::Phase::kReloadFlags) { + job.flags = cache_.pendingChannelFlags(gateway_id); + if (cache_.fullStateMirrorEnabled() && AnyFlagSet(job.flags)) { + job.flags = {true, true, true}; + } + if (!AnyFlagSet(job.flags)) { + return false; + } + + job.short_address = 0; + job.scene_id = 0; + if (job.flags.need_update_group) { + job.phase = ReconciliationJob::Phase::kGroups; + } else if (job.flags.need_update_scene) { + job.phase = ReconciliationJob::Phase::kScenes; + } else { + job.phase = ReconciliationJob::Phase::kSettings; + } + } + + switch (job.phase) { + case ReconciliationJob::Phase::kGroups: + reconcileGroupStep(gateway_id, job.short_address++); + if (job.short_address >= kDaliShortAddressCount) { + job.short_address = 0; + if (job.flags.need_update_scene) { + job.phase = ReconciliationJob::Phase::kScenes; + } else if (job.flags.need_update_settings) { + job.phase = ReconciliationJob::Phase::kSettings; + } else if (!cache_.clearChannelFlagsIfMatched(gateway_id, job.flags)) { + job.phase = ReconciliationJob::Phase::kReloadFlags; + } else { + return false; + } + } + return true; + case ReconciliationJob::Phase::kScenes: + reconcileSceneStep(gateway_id, job.short_address, job.scene_id); + ++job.scene_id; + if (job.scene_id >= kDaliSceneCount) { + job.scene_id = 0; + ++job.short_address; + } + if (job.short_address >= kDaliShortAddressCount) { + job.short_address = 0; + if (job.flags.need_update_settings) { + job.phase = ReconciliationJob::Phase::kSettings; + } else if (!cache_.clearChannelFlagsIfMatched(gateway_id, job.flags)) { + job.phase = ReconciliationJob::Phase::kReloadFlags; + } else { + return false; + } + } + return true; + case ReconciliationJob::Phase::kSettings: + reconcileSettingsStep(gateway_id, job.short_address++); + if (job.short_address >= kDaliShortAddressCount) { + job.short_address = 0; + if (!cache_.clearChannelFlagsIfMatched(gateway_id, job.flags)) { + job.phase = ReconciliationJob::Phase::kReloadFlags; + } else { + return false; + } + } + return true; + case ReconciliationJob::Phase::kReloadFlags: + default: + return true; + } +} + +void GatewayController::reconcileGroupStep(uint8_t gateway_id, uint8_t short_address) { + const auto policy = cache_.priorityMode(); + const auto state = cache_.daliAddressState(gateway_id, short_address); + + if (policy == GatewayCachePriorityMode::kLocalGatewayFirst && state.group_mask_known) { + maintenance_activity_gateway_.store(gateway_id); + 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_.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); + } + return; + } + + cache_.setDaliGroupMask(gateway_id, short_address, + dali_domain_.queryGroupMask(gateway_id, short_address)); +} + +void GatewayController::reconcileSceneStep(uint8_t gateway_id, uint8_t short_address, + uint8_t scene_id) { + const auto policy = cache_.priorityMode(); + const auto state = cache_.daliAddressState(gateway_id, short_address); + + if (policy == GatewayCachePriorityMode::kLocalGatewayFirst && + state.scene_levels[scene_id].has_value()) { + maintenance_activity_gateway_.store(gateway_id); + dali_domain_.applySceneLevel(gateway_id, short_address, scene_id, state.scene_levels[scene_id]); + maintenance_activity_gateway_.store(-1); + } + + cache_.setDaliSceneLevel(gateway_id, short_address, scene_id, + dali_domain_.querySceneLevel(gateway_id, short_address, scene_id)); +} + +void GatewayController::reconcileSettingsStep(uint8_t gateway_id, uint8_t short_address) { + const auto policy = cache_.priorityMode(); + const auto state = cache_.daliAddressState(gateway_id, short_address); + + if (policy == GatewayCachePriorityMode::kLocalGatewayFirst && state.settings.anyKnown()) { + maintenance_activity_gateway_.store(gateway_id); + dali_domain_.applyAddressSettings(gateway_id, short_address, { + state.settings.power_on_level, + state.settings.system_failure_level, + state.settings.min_level, + state.settings.max_level, + state.settings.fade_time, + state.settings.fade_rate, + }); + maintenance_activity_gateway_.store(-1); + } + + const auto settings = dali_domain_.queryAddressSettings(gateway_id, short_address); + if (settings.has_value()) { + cache_.setDaliSettings(gateway_id, short_address, + GatewayCacheDaliSettingsSnapshot{settings->power_on_level, + settings->system_failure_level, + settings->min_level, + settings->max_level, + settings->fade_time, + settings->fade_rate}); + } else { + cache_.setDaliSettings(gateway_id, short_address, std::nullopt); } } @@ -480,10 +722,6 @@ void GatewayController::handleDaliRawFrame(const DaliRawFrame& frame) { if (frame.data.size() != 2 && frame.data.size() != 3) { return; } - if (setup_mode_ || dali_domain_.isAllocAddr(frame.gateway_id) || - runtime_.hasActiveQueryCommand(frame.gateway_id)) { - return; - } uint8_t addr = 0; uint8_t data = 0; @@ -498,6 +736,22 @@ void GatewayController::handleDaliRawFrame(const DaliRawFrame& frame) { data = frame.data[2]; } + const bool maintenance_activity = maintenance_activity_gateway_.load() == frame.gateway_id; + const bool local_activity = maintenance_activity || runtime_.hasActiveCommand(frame.gateway_id) || + 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); + } + + if (setup_mode_ || dali_domain_.isAllocAddr(frame.gateway_id) || maintenance_activity || + runtime_.hasActiveQueryCommand(frame.gateway_id)) { + return; + } + publishPayload(frame.gateway_id, {0x01, frame.gateway_id, addr, data}); } diff --git a/components/gateway_runtime/include/gateway_runtime.hpp b/components/gateway_runtime/include/gateway_runtime.hpp index 86c526a..6c04605 100644 --- a/components/gateway_runtime/include/gateway_runtime.hpp +++ b/components/gateway_runtime/include/gateway_runtime.hpp @@ -35,6 +35,7 @@ struct GatewayRuntimeConfig { std::string_view version; std::string serial_id; bool default_ble_enabled{true}; + bool default_cache_enabled{true}; size_t command_queue_capacity{16}; }; @@ -59,6 +60,9 @@ class GatewaySettingsStore { bool getBleEnabled(bool default_value = false) const; bool setBleEnabled(bool enabled); + bool getCacheEnabled(bool default_value = true) const; + bool setCacheEnabled(bool enabled); + std::optional getWifiSsid() const; std::optional getWifiPassword() const; bool setWifiCredentials(std::string_view ssid, std::string_view password); @@ -83,6 +87,12 @@ class GatewayRuntime { kQueueFull, }; + enum class CommandPriority : uint8_t { + kControl = 0, + kNormal = 1, + kMaintenance = 2, + }; + GatewayRuntime(BootProfile profile, GatewayRuntimeConfig config, DaliDomainService* dali_domain); ~GatewayRuntime(); @@ -93,11 +103,16 @@ class GatewayRuntime { static bool hasValidChecksum(const std::vector& frame); static bool isGatewayCommandFrame(const std::vector& frame); static std::vector buildNotificationFrame(const std::vector& payload); + static CommandPriority classifyCommandPriority(const std::vector& command); - bool enqueueCommand(std::vector command); + bool enqueueCommand(std::vector command, + CommandPriority priority = CommandPriority::kNormal); std::optional> popNextCommand(); void completeCurrentCommand(); bool hasPendingQueryCommand(const std::vector& command) const; + bool hasPendingControlCommand(uint8_t gateway_id) const; + bool shouldYieldMaintenance(uint8_t gateway_id) const; + bool hasActiveCommand(uint8_t gateway_id) const; bool hasActiveQueryCommand(uint8_t gateway_id) const; CommandDropReason lastEnqueueDropReason() const; @@ -109,6 +124,8 @@ class GatewayRuntime { GatewayDeviceInfo deviceInfo() const; bool bleEnabled() const; bool setBleEnabled(bool enabled); + bool cacheEnabled() const; + bool setCacheEnabled(bool enabled); std::string gatewayName(uint8_t gateway_id) const; bool setGatewayName(uint8_t gateway_id, std::string_view name); std::string gatewaySerialHex(uint8_t gateway_id) const; @@ -118,6 +135,9 @@ class GatewayRuntime { private: bool isQueryCommand(const std::vector& command) const; + size_t pendingCommandCountLocked() const; + std::deque>& queueForPriorityLocked(CommandPriority priority); + const std::deque>& queueForPriorityLocked(CommandPriority priority) const; std::optional queryCommandKey(const std::vector& command) const; std::string defaultGatewayName(uint8_t gateway_id) const; std::vector serialBytes() const; @@ -128,10 +148,14 @@ class GatewayRuntime { DaliDomainService* dali_domain_; GatewaySettingsStore settings_; std::optional> current_command_; - std::deque> pending_commands_; + CommandPriority current_command_priority_{CommandPriority::kNormal}; + std::deque> control_commands_; + std::deque> normal_commands_; + std::deque> maintenance_commands_; mutable std::map gateway_names_; size_t gateway_count_{0}; bool ble_enabled_{false}; + bool cache_enabled_{true}; CommandDropReason last_enqueue_drop_reason_{CommandDropReason::kNone}; std::function command_address_resolver_; std::optional wireless_info_; diff --git a/components/gateway_runtime/src/gateway_runtime.cpp b/components/gateway_runtime/src/gateway_runtime.cpp index 13897a9..026af3f 100644 --- a/components/gateway_runtime/src/gateway_runtime.cpp +++ b/components/gateway_runtime/src/gateway_runtime.cpp @@ -18,6 +18,7 @@ namespace { constexpr const char* kTag = "gateway_runtime"; constexpr const char* kNamespace = "gateway_rt"; constexpr const char* kBleEnabledKey = "ble_enabled"; +constexpr const char* kCacheEnabledKey = "cache_enabled"; constexpr const char* kWifiSsidKey = "wifi_ssid"; constexpr const char* kWifiPasswordKey = "wifi_passwd"; constexpr size_t kMaxGatewayNameBytes = 32; @@ -109,6 +110,28 @@ bool GatewaySettingsStore::setBleEnabled(bool enabled) { nvs_commit(handle_) == ESP_OK; } +bool GatewaySettingsStore::getCacheEnabled(bool default_value) const { + if (handle_ == 0) { + return default_value; + } + + uint8_t enabled = default_value ? 1 : 0; + if (nvs_get_u8(handle_, kCacheEnabledKey, &enabled) != ESP_OK) { + return default_value; + } + + return enabled != 0; +} + +bool GatewaySettingsStore::setCacheEnabled(bool enabled) { + if (handle_ == 0) { + return false; + } + + return nvs_set_u8(handle_, kCacheEnabledKey, enabled ? 1 : 0) == ESP_OK && + nvs_commit(handle_) == ESP_OK; +} + std::optional GatewaySettingsStore::getWifiSsid() const { return readString(kWifiSsidKey); } @@ -222,6 +245,7 @@ esp_err_t GatewayRuntime::start() { } ble_enabled_ = settings_.getBleEnabled(config_.default_ble_enabled); + cache_enabled_ = settings_.getCacheEnabled(config_.default_cache_enabled); if (!wireless_info_.has_value()) { WirelessInfo info; @@ -239,10 +263,10 @@ esp_err_t GatewayRuntime::start() { } ESP_LOGI(kTag, - "runtime project=%.*s version=%.*s serial=%s ble=%d dali_bound=%d", + "runtime project=%.*s version=%.*s serial=%s ble=%d cache=%d dali_bound=%d", static_cast(config_.project_name.size()), config_.project_name.data(), static_cast(config_.version.size()), config_.version.data(), - config_.serial_id.c_str(), ble_enabled_, + config_.serial_id.c_str(), ble_enabled_, cache_enabled_, dali_domain_ != nullptr && dali_domain_->isBound()); return ESP_OK; } @@ -283,7 +307,30 @@ std::vector GatewayRuntime::buildNotificationFrame(const std::vector command) { +GatewayRuntime::CommandPriority GatewayRuntime::classifyCommandPriority( + const std::vector& command) { + if (command.size() < 5 || !isGatewayCommandFrame(command)) { + return CommandPriority::kNormal; + } + + const uint8_t opcode = command[3]; + const uint8_t addr = command[4]; + if (opcode == 0x30 && (addr == 1 || addr == 2)) { + return CommandPriority::kMaintenance; + } + if (opcode == 0x32) { + return CommandPriority::kMaintenance; + } + if (opcode == 0x00 || opcode == 0x01 || opcode == 0x03 || opcode == 0x04 || opcode == 0x07 || + opcode == 0x08 || opcode == 0x10 || opcode == 0x11 || opcode == 0x12 || opcode == 0x13 || + opcode == 0x17 || opcode == 0x18 || opcode == 0x37 || opcode == 0x38 || + (opcode == 0x30 && addr == 0)) { + return CommandPriority::kControl; + } + return CommandPriority::kNormal; +} + +bool GatewayRuntime::enqueueCommand(std::vector command, CommandPriority priority) { LockGuard guard(command_lock_); last_enqueue_drop_reason_ = CommandDropReason::kNone; if (isQueryCommand(command) && hasPendingQueryCommand(command)) { @@ -291,25 +338,30 @@ bool GatewayRuntime::enqueueCommand(std::vector command) { return false; } - if (pending_commands_.size() >= config_.command_queue_capacity) { + if (pendingCommandCountLocked() >= config_.command_queue_capacity) { last_enqueue_drop_reason_ = CommandDropReason::kQueueFull; return false; } - pending_commands_.push_back(std::move(command)); + queueForPriorityLocked(priority).push_back(std::move(command)); return true; } std::optional> GatewayRuntime::popNextCommand() { LockGuard guard(command_lock_); - if (pending_commands_.empty()) { - current_command_.reset(); - return std::nullopt; + for (const auto priority : {CommandPriority::kControl, CommandPriority::kNormal, + CommandPriority::kMaintenance}) { + auto& queue = queueForPriorityLocked(priority); + if (!queue.empty()) { + current_command_ = std::move(queue.front()); + current_command_priority_ = priority; + queue.pop_front(); + return current_command_; + } } - current_command_ = std::move(pending_commands_.front()); - pending_commands_.pop_front(); - return current_command_; + current_command_.reset(); + return std::nullopt; } void GatewayRuntime::completeCurrentCommand() { @@ -328,10 +380,29 @@ bool GatewayRuntime::hasPendingQueryCommand(const std::vector& command) return true; } - return std::any_of(pending_commands_.begin(), pending_commands_.end(), - [&](const std::vector& pending) { - return queryCommandKey(pending) == command_key; - }); + const auto matches = [&](const std::vector& pending) { + return queryCommandKey(pending) == command_key; + }; + return std::any_of(control_commands_.begin(), control_commands_.end(), matches) || + std::any_of(normal_commands_.begin(), normal_commands_.end(), matches) || + std::any_of(maintenance_commands_.begin(), maintenance_commands_.end(), matches); +} + +bool GatewayRuntime::hasPendingControlCommand(uint8_t gateway_id) const { + LockGuard guard(command_lock_); + return std::any_of(control_commands_.begin(), control_commands_.end(), [gateway_id](const auto& command) { + return command.size() > 2 && command[2] == gateway_id; + }); +} + +bool GatewayRuntime::shouldYieldMaintenance(uint8_t gateway_id) const { + return hasPendingControlCommand(gateway_id); +} + +bool GatewayRuntime::hasActiveCommand(uint8_t gateway_id) const { + LockGuard guard(command_lock_); + return current_command_.has_value() && current_command_->size() > 2 && + (*current_command_)[2] == gateway_id; } bool GatewayRuntime::hasActiveQueryCommand(uint8_t gateway_id) const { @@ -422,6 +493,26 @@ bool GatewayRuntime::setBleEnabled(bool enabled) { return true; } +bool GatewayRuntime::cacheEnabled() const { + LockGuard guard(command_lock_); + return cache_enabled_; +} + +bool GatewayRuntime::setCacheEnabled(bool enabled) { + { + LockGuard guard(command_lock_); + if (cache_enabled_ == enabled) { + return true; + } + } + if (!settings_.setCacheEnabled(enabled)) { + return false; + } + LockGuard guard(command_lock_); + cache_enabled_ = enabled; + return true; +} + std::string GatewayRuntime::gatewayName(uint8_t gateway_id) const { LockGuard guard(command_lock_); const auto cached = gateway_names_.find(gateway_id); @@ -492,6 +583,36 @@ bool GatewayRuntime::isQueryCommand(const std::vector& command) const { command[3] <= 0x16; } +size_t GatewayRuntime::pendingCommandCountLocked() const { + return control_commands_.size() + normal_commands_.size() + maintenance_commands_.size(); +} + +std::deque>& GatewayRuntime::queueForPriorityLocked( + CommandPriority priority) { + switch (priority) { + case CommandPriority::kControl: + return control_commands_; + case CommandPriority::kMaintenance: + return maintenance_commands_; + case CommandPriority::kNormal: + default: + return normal_commands_; + } +} + +const std::deque>& GatewayRuntime::queueForPriorityLocked( + CommandPriority priority) const { + switch (priority) { + case CommandPriority::kControl: + return control_commands_; + case CommandPriority::kMaintenance: + return maintenance_commands_; + case CommandPriority::kNormal: + default: + return normal_commands_; + } +} + std::optional GatewayRuntime::queryCommandKey( const std::vector& command) const { if (!isQueryCommand(command)) {