#include "gateway_cache.hpp" #include #include #include #include #include #include "esp_log.h" namespace gateway { namespace { constexpr const char* kTag = "gateway_cache"; constexpr size_t kMaxNameBytes = 32; constexpr uint8_t kDaliRawBroadcastArc = 0xFE; constexpr uint8_t kDaliRawBroadcastCommand = 0xFF; constexpr uint8_t kDaliGroupRawMin = 0x80; constexpr uint8_t kDaliGroupRawMax = 0x9F; constexpr uint8_t kDaliCmdOff = 0x00; constexpr uint8_t kDaliCmdRecallMax = 0x05; constexpr uint8_t kDaliCmdRecallMin = 0x06; constexpr uint8_t kDaliCmdGoToSceneMin = 0x10; constexpr uint8_t kDaliCmdGoToSceneMax = 0x1F; 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: explicit LockGuard(SemaphoreHandle_t lock) : lock_(lock) { if (lock_ != nullptr) { xSemaphoreTakeRecursive(lock_, portMAX_DELAY); } } ~LockGuard() { if (lock_ != nullptr) { xSemaphoreGiveRecursive(lock_); } } private: SemaphoreHandle_t lock_; }; std::string ShortKey(const char* prefix, uint8_t gateway_id, uint8_t slot) { char key[16] = {0}; std::snprintf(key, sizeof(key), "%s%u_%u", prefix, gateway_id, slot); return std::string(key); } std::vector ParseCsv(std::string_view raw) { std::vector values; size_t start = 0; while (start < raw.size()) { const size_t comma = raw.find(',', start); const size_t end = comma == std::string_view::npos ? raw.size() : comma; if (end > start) { values.push_back(static_cast(std::strtol(std::string(raw.substr(start, end - start)).c_str(), nullptr, 10))); } if (comma == std::string_view::npos) { break; } start = comma + 1; } return values; } std::string NormalizeName(std::string_view name) { std::string normalized(name); if (normalized.size() > kMaxNameBytes) { normalized.resize(kMaxNameBytes); } return normalized; } bool IsDefaultScene(const GatewayCache::SceneEntry& scene) { return !scene.enabled && scene.brightness == 254 && scene.color_mode == 2 && scene.data1 == 0 && scene.data2 == 0 && scene.data3 == 0; } 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 DecodeDaliTarget(uint8_t raw_addr) { if (raw_addr <= 0x7F) { return GatewayCacheDaliTarget{GatewayCacheDaliTargetKind::kShortAddress, static_cast(raw_addr >> 1)}; } if (raw_addr >= kDaliGroupRawMin && raw_addr <= kDaliGroupRawMax) { return GatewayCacheDaliTarget{GatewayCacheDaliTargetKind::kGroup, static_cast((raw_addr - kDaliGroupRawMin) >> 1)}; } if (raw_addr == kDaliRawBroadcastArc || raw_addr == kDaliRawBroadcastCommand) { return GatewayCacheDaliTarget{GatewayCacheDaliTargetKind::kBroadcast, 0}; } 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 = {}; state.status = {}; } 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, scene.brightness, scene.color_mode, scene.data1, scene.data2, scene.data3); return std::string(payload); } std::string BuildGroupPayload(const GatewayCache::GroupEntry& group) { char payload[24] = {0}; std::snprintf(payload, sizeof(payload), "%u,%u,%u", group.enabled ? 1 : 0, group.target_type, group.target_value); return std::string(payload); } } // namespace GatewayCache::GatewayCache(GatewayCacheConfig config) : config_(std::move(config)), priority_mode_(config_.default_priority_mode), lock_(xSemaphoreCreateRecursiveMutex()) {} GatewayCache::~GatewayCache() { if (task_handle_ != nullptr) { vTaskDelete(task_handle_); task_handle_ = nullptr; } { LockGuard guard(lock_); if (config_.cache_enabled) { flushDirty(); } closeStorageLocked(); } if (lock_ != nullptr) { vSemaphoreDelete(lock_); lock_ = nullptr; } } esp_err_t GatewayCache::start() { { LockGuard guard(lock_); if (!openStorageLocked()) { return ESP_FAIL; } } 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; } const BaseType_t created = xTaskCreate(&GatewayCache::TaskEntry, "gateway_cache", config_.task_stack_size, this, config_.task_priority, &task_handle_); if (created != pdPASS) { task_handle_ = nullptr; ESP_LOGE(kTag, "failed to create cache task"); return ESP_ERR_NO_MEM; } 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); } GatewayCache::SceneEntry GatewayCache::scene(uint8_t gateway_id, uint8_t scene_id) { LockGuard guard(lock_); 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]; } GatewayCache::GroupEntry GatewayCache::group(uint8_t gateway_id, uint8_t group_id) { LockGuard guard(lock_); 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]; } bool GatewayCache::setSceneEnabled(uint8_t gateway_id, uint8_t scene_id, bool enabled) { LockGuard guard(lock_); 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; } entry.enabled = enabled; dirty_ = true; return true; } bool GatewayCache::setSceneDetail(uint8_t gateway_id, uint8_t scene_id, uint8_t brightness, uint8_t color_mode, uint8_t data1, uint8_t data2, uint8_t data3) { LockGuard guard(lock_); 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) { return true; } entry.brightness = brightness; entry.color_mode = color_mode; entry.data1 = data1; entry.data2 = data2; entry.data3 = data3; dirty_ = true; return true; } bool GatewayCache::setSceneName(uint8_t gateway_id, uint8_t scene_id, std::string_view name) { LockGuard guard(lock_); 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) { return true; } entry.name = normalized; dirty_ = true; return true; } bool GatewayCache::deleteScene(uint8_t gateway_id, uint8_t scene_id) { LockGuard guard(lock_); 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; } entry = SceneEntry{}; dirty_ = true; return true; } std::pair GatewayCache::sceneMask(uint8_t gateway_id) { LockGuard guard(lock_); 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) { mask |= static_cast(1U << index); } } return {static_cast(mask & 0xff), static_cast((mask >> 8) & 0xff)}; } bool GatewayCache::setGroupEnabled(uint8_t gateway_id, uint8_t group_id, bool enabled) { LockGuard guard(lock_); 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; } entry.enabled = enabled; dirty_ = true; return true; } bool GatewayCache::setGroupDetail(uint8_t gateway_id, uint8_t group_id, uint8_t target_type, uint8_t target_value) { LockGuard guard(lock_); 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; } entry.target_type = target_type; entry.target_value = target_value; dirty_ = true; return true; } bool GatewayCache::setGroupName(uint8_t gateway_id, uint8_t group_id, std::string_view name) { LockGuard guard(lock_); 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) { return true; } entry.name = normalized; dirty_ = true; return true; } bool GatewayCache::deleteGroup(uint8_t gateway_id, uint8_t group_id) { LockGuard guard(lock_); 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; } entry = GroupEntry{}; dirty_ = true; return true; } std::pair GatewayCache::groupMask(uint8_t gateway_id) { LockGuard guard(lock_); 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) { mask |= static_cast(1U << index); } } return {static_cast(mask & 0xff), static_cast((mask >> 8) & 0xff)}; } 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); } GatewayCacheDaliRuntimeStatus GatewayCache::daliGroupStatus(uint8_t gateway_id, uint8_t group_id) { LockGuard guard(lock_); if (group_id >= 16) { return {}; } return ensureDaliGroupStatusLocked(gateway_id, group_id); } GatewayCacheDaliRuntimeStatus GatewayCache::daliBroadcastStatus(uint8_t gateway_id) { LockGuard guard(lock_); return ensureDaliBroadcastStatusLocked(gateway_id); } 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); refreshDaliAddressAggregateStatusLocked(gateway_id, state); 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::mirrorDaliCommand(uint8_t gateway_id, uint8_t raw_addr, uint8_t command) { LockGuard guard(lock_); if (!config_.cache_enabled) { return false; } return mirrorDaliCommandLocked(gateway_id, raw_addr, command); } bool GatewayCache::observeDaliCommand(uint8_t gateway_id, uint8_t raw_addr, uint8_t command, GatewayCacheRawFrameOrigin origin) { LockGuard guard(lock_); if (!config_.cache_enabled) { return false; } if (ShouldMirrorObservedMutation(origin, priority_mode_)) { mirrorDaliCommandLocked(gateway_id, raw_addr, command); } if (!shouldTrackUpdateFlagsLocked()) { return false; } const auto detected = ClassifyDaliMutation(raw_addr, command); if (!AnyFlagSet(detected)) { return false; } 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_; } void GatewayCache::setPriorityMode(GatewayCachePriorityMode mode) { LockGuard guard(lock_); priority_mode_ = mode; } uint32_t GatewayCache::nextDaliRuntimeRevisionLocked() { ++dali_runtime_revision_; if (dali_runtime_revision_ == 0) { ++dali_runtime_revision_; } return dali_runtime_revision_; } bool GatewayCache::mirrorDaliCommandLocked(uint8_t gateway_id, uint8_t raw_addr, uint8_t command) { 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 target = DecodeDaliTarget(raw_addr); if (!target.has_value()) { return false; } const bool arc_power_frame = (raw_addr & 0x01) == 0; if (arc_power_frame) { if (command > 254) { return false; } GatewayCacheDaliRuntimeStatus status; status.actual_level = command; status.revision = nextDaliRuntimeRevisionLocked(); applyDaliTargetRuntimeStatusLocked(gateway_id, *target, status); return true; } if (command == kDaliCmdReset) { clearDaliTargetStateLocked(gateway_id, *target, nextDaliRuntimeRevisionLocked()); return true; } if (command == kDaliCmdOff || command == kDaliCmdRecallMax) { GatewayCacheDaliRuntimeStatus status; status.actual_level = command == kDaliCmdOff ? 0 : 254; status.revision = nextDaliRuntimeRevisionLocked(); applyDaliTargetRuntimeStatusLocked(gateway_id, *target, status); return true; } if (command == kDaliCmdRecallMin) { GatewayCacheDaliRuntimeStatus status; status.use_min_level = true; status.revision = nextDaliRuntimeRevisionLocked(); applyDaliTargetRuntimeStatusLocked(gateway_id, *target, status); return true; } if (command >= kDaliCmdGoToSceneMin && command <= kDaliCmdGoToSceneMax) { GatewayCacheDaliRuntimeStatus status; status.scene_id = static_cast(command - kDaliCmdGoToSceneMin); status.revision = nextDaliRuntimeRevisionLocked(); applyDaliTargetRuntimeStatusLocked(gateway_id, *target, status); return true; } if (command >= kDaliCmdAddToGroupMin && command <= kDaliCmdRemoveFromGroupMax) { applyDaliTargetGroupMutationLocked(gateway_id, *target, static_cast(command & 0x0F), command < (kDaliCmdAddToGroupMin + 16)); return true; } if (command >= kDaliCmdSetSceneMin && command < (kDaliCmdSetSceneMin + 16) && dtr_state.dtr0.has_value()) { applyDaliTargetSceneLevelLocked(gateway_id, *target, static_cast(command - kDaliCmdSetSceneMin), *dtr_state.dtr0); return true; } if (command >= (kDaliCmdSetSceneMin + 16) && command <= kDaliCmdRemoveSceneMax) { applyDaliTargetSceneLevelLocked( gateway_id, *target, static_cast(command - (kDaliCmdSetSceneMin + 16)), static_cast(255U)); return true; } if (command >= kDaliCmdStoreDtrAsMaxLevel && command <= kDaliCmdStoreDtrAsFadeRate && dtr_state.dtr0.has_value()) { applyDaliTargetSettingsLocked(gateway_id, *target, command, *dtr_state.dtr0); return true; } return false; } void GatewayCache::clearDaliTargetStateLocked(uint8_t gateway_id, const GatewayCacheDaliTarget& target, uint32_t revision) { auto clear_state = [revision](GatewayCacheDaliAddressState& state) { ClearDaliState(state); state.status.revision = revision; }; switch (target.kind) { case GatewayCacheDaliTargetKind::kShortAddress: if (target.value < 64) { clear_state(ensureDaliAddressStateLocked(gateway_id, target.value)); } break; case GatewayCacheDaliTargetKind::kGroup: { if (target.value >= 16) { break; } auto& group_status = ensureDaliGroupStatusLocked(gateway_id, target.value); group_status = {}; group_status.revision = revision; const uint16_t bit = static_cast(1U << target.value); if (auto states = dali_states_.find(gateway_id); states != dali_states_.end()) { for (auto& state : states->second) { if (state.group_mask_known && (state.group_mask & bit) != 0) { clear_state(state); } } } break; } case GatewayCacheDaliTargetKind::kBroadcast: { auto& broadcast_status = ensureDaliBroadcastStatusLocked(gateway_id); broadcast_status = {}; broadcast_status.revision = revision; auto& group_statuses = dali_group_status_[gateway_id]; for (auto& group_status : group_statuses) { group_status = {}; group_status.revision = revision; } auto& states = dali_states_[gateway_id]; for (auto& state : states) { clear_state(state); } break; } default: break; } } void GatewayCache::applyDaliTargetRuntimeStatusLocked( uint8_t gateway_id, const GatewayCacheDaliTarget& target, const GatewayCacheDaliRuntimeStatus& status) { if (!status.anyKnown()) { return; } switch (target.kind) { case GatewayCacheDaliTargetKind::kShortAddress: if (target.value < 64) { applyDaliRuntimeStatusToAddressLocked(ensureDaliAddressStateLocked(gateway_id, target.value), status); } break; case GatewayCacheDaliTargetKind::kGroup: { if (target.value >= 16) { break; } ensureDaliGroupStatusLocked(gateway_id, target.value) = status; const uint16_t bit = static_cast(1U << target.value); if (auto states = dali_states_.find(gateway_id); states != dali_states_.end()) { for (auto& state : states->second) { if (state.group_mask_known && (state.group_mask & bit) != 0) { applyDaliRuntimeStatusToAddressLocked(state, status); } } } break; } case GatewayCacheDaliTargetKind::kBroadcast: { ensureDaliBroadcastStatusLocked(gateway_id) = status; auto& states = dali_states_[gateway_id]; for (auto& state : states) { applyDaliRuntimeStatusToAddressLocked(state, status); } break; } default: break; } } void GatewayCache::applyDaliRuntimeStatusToAddressLocked( GatewayCacheDaliAddressState& state, const GatewayCacheDaliRuntimeStatus& status) { if (!status.anyKnown() || status.revision <= state.status.revision) { return; } if (status.scene_id.has_value()) { state.status.scene_id = status.scene_id; state.status.use_min_level = false; const uint8_t scene_id = *status.scene_id; if (scene_id < state.scene_levels.size()) { const auto scene_level = state.scene_levels[scene_id]; if (scene_level.has_value() && *scene_level != 255U) { state.status.actual_level = *scene_level; } else if (status.actual_level.has_value()) { state.status.actual_level = status.actual_level; } } } else { state.status.scene_id.reset(); state.status.use_min_level = status.use_min_level; if (status.use_min_level) { state.status.actual_level = state.settings.min_level; } else { state.status.actual_level = status.actual_level; } } state.status.revision = status.revision; } void GatewayCache::applyDaliTargetGroupMutationLocked(uint8_t gateway_id, const GatewayCacheDaliTarget& target, uint8_t group_id, bool add_to_group) { if (group_id >= 16) { return; } const uint16_t bit = static_cast(1U << group_id); auto apply = [&](GatewayCacheDaliAddressState& state) { if (!state.group_mask_known) { return; } if (add_to_group) { state.group_mask |= bit; } else { state.group_mask &= static_cast(~bit); } refreshDaliAddressAggregateStatusLocked(gateway_id, state); }; switch (target.kind) { case GatewayCacheDaliTargetKind::kShortAddress: if (target.value < 64) { apply(ensureDaliAddressStateLocked(gateway_id, target.value)); } break; case GatewayCacheDaliTargetKind::kGroup: { if (target.value >= 16) { break; } const uint16_t target_bit = static_cast(1U << target.value); if (auto states = dali_states_.find(gateway_id); states != dali_states_.end()) { for (auto& state : states->second) { if (state.group_mask_known && (state.group_mask & target_bit) != 0) { apply(state); } } } break; } case GatewayCacheDaliTargetKind::kBroadcast: { auto& states = dali_states_[gateway_id]; for (auto& state : states) { apply(state); } break; } default: break; } } void GatewayCache::applyDaliTargetSceneLevelLocked(uint8_t gateway_id, const GatewayCacheDaliTarget& target, uint8_t scene_id, std::optional level) { if (scene_id >= 16) { return; } auto apply = [scene_id, level](GatewayCacheDaliAddressState& state) { state.scene_levels[scene_id] = level; if (state.status.scene_id.has_value() && *state.status.scene_id == scene_id && level.has_value() && *level != 255U) { state.status.actual_level = *level; } }; switch (target.kind) { case GatewayCacheDaliTargetKind::kShortAddress: if (target.value < 64) { apply(ensureDaliAddressStateLocked(gateway_id, target.value)); } break; case GatewayCacheDaliTargetKind::kGroup: { if (target.value >= 16) { break; } const uint16_t bit = static_cast(1U << target.value); if (auto states = dali_states_.find(gateway_id); states != dali_states_.end()) { for (auto& state : states->second) { if (state.group_mask_known && (state.group_mask & bit) != 0) { apply(state); } } } break; } case GatewayCacheDaliTargetKind::kBroadcast: { auto& states = dali_states_[gateway_id]; for (auto& state : states) { apply(state); } break; } default: break; } } void GatewayCache::applyDaliTargetSettingsLocked(uint8_t gateway_id, const GatewayCacheDaliTarget& target, uint8_t command, uint8_t value) { auto apply = [command, value](GatewayCacheDaliAddressState& state) { ApplyObservedSettingsValue(state.settings, command, value); if (command == kDaliCmdStoreDtrAsMinLevel && state.status.use_min_level) { state.status.actual_level = value; } }; switch (target.kind) { case GatewayCacheDaliTargetKind::kShortAddress: if (target.value < 64) { apply(ensureDaliAddressStateLocked(gateway_id, target.value)); } break; case GatewayCacheDaliTargetKind::kGroup: { if (target.value >= 16) { break; } const uint16_t bit = static_cast(1U << target.value); if (auto states = dali_states_.find(gateway_id); states != dali_states_.end()) { for (auto& state : states->second) { if (state.group_mask_known && (state.group_mask & bit) != 0) { apply(state); } } } break; } case GatewayCacheDaliTargetKind::kBroadcast: { auto& states = dali_states_[gateway_id]; for (auto& state : states) { apply(state); } break; } default: break; } } void GatewayCache::refreshDaliAddressAggregateStatusLocked(uint8_t gateway_id, GatewayCacheDaliAddressState& state) { if (!state.group_mask_known) { return; } if (const auto broadcast = dali_broadcast_status_.find(gateway_id); broadcast != dali_broadcast_status_.end()) { applyDaliRuntimeStatusToAddressLocked(state, broadcast->second); } const auto groups = dali_group_status_.find(gateway_id); if (groups == dali_group_status_.end()) { return; } for (uint8_t group_id = 0; group_id < groups->second.size(); ++group_id) { const uint16_t bit = static_cast(1U << group_id); if ((state.group_mask & bit) != 0) { applyDaliRuntimeStatusToAddressLocked(state, groups->second[group_id]); } } } void GatewayCache::TaskEntry(void* arg) { static_cast(arg)->taskLoop(); } void GatewayCache::taskLoop() { const TickType_t interval_ticks = std::max(1, pdMS_TO_TICKS(config_.flush_interval_ms)); while (true) { vTaskDelay(interval_ticks); flushDirty(); } } bool GatewayCache::flushDirty() { LockGuard guard(lock_); if (!config_.cache_enabled) { return true; } if (!dirty_) { return true; } if (!openStorageLocked()) { return false; } for (const auto& [gateway_id, store] : scenes_) { for (uint8_t scene_id = 0; scene_id < store.size(); ++scene_id) { const auto& entry = store[scene_id]; if (!IsDefaultScene(entry)) { if (!writeStringLocked(ShortKey("sc", gateway_id, scene_id), BuildScenePayload(entry))) { return false; } } else if (!eraseKeyLocked(ShortKey("sc", gateway_id, scene_id))) { return false; } if (!entry.name.empty()) { if (!writeStringLocked(ShortKey("sn", gateway_id, scene_id), entry.name)) { return false; } } else if (!eraseKeyLocked(ShortKey("sn", gateway_id, scene_id))) { return false; } } } for (const auto& [gateway_id, store] : groups_) { for (uint8_t group_id = 0; group_id < store.size(); ++group_id) { const auto& entry = store[group_id]; if (!IsDefaultGroup(entry)) { if (!writeStringLocked(ShortKey("gr", gateway_id, group_id), BuildGroupPayload(entry))) { return false; } } else if (!eraseKeyLocked(ShortKey("gr", gateway_id, group_id))) { return false; } if (!entry.name.empty()) { if (!writeStringLocked(ShortKey("gn", gateway_id, group_id), entry.name)) { return false; } } else if (!eraseKeyLocked(ShortKey("gn", gateway_id, group_id))) { 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; } dirty_ = false; return true; } bool GatewayCache::openStorageLocked() { if (storage_ != 0) { return true; } const esp_err_t err = nvs_open(config_.storage_namespace.c_str(), NVS_READWRITE, &storage_); if (err != ESP_OK) { ESP_LOGE(kTag, "failed to open cache storage: %s", esp_err_to_name(err)); return false; } return true; } void GatewayCache::closeStorageLocked() { if (storage_ != 0) { nvs_close(storage_); storage_ = 0; } } 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]; } GatewayCacheDaliRuntimeStatus& GatewayCache::ensureDaliGroupStatusLocked(uint8_t gateway_id, uint8_t group_id) { auto [it, inserted] = dali_group_status_.try_emplace(gateway_id); (void)inserted; return it->second[group_id]; } GatewayCacheDaliRuntimeStatus& GatewayCache::ensureDaliBroadcastStatusLocked(uint8_t gateway_id) { return dali_broadcast_status_[gateway_id]; } GatewayCache::SceneStore& GatewayCache::ensureSceneStoreLocked(uint8_t gateway_id) { auto [it, inserted] = scenes_.try_emplace(gateway_id); if (inserted) { loadSceneStoreLocked(gateway_id, it->second); } return it->second; } GatewayCache::GroupStore& GatewayCache::ensureGroupStoreLocked(uint8_t gateway_id) { auto [it, inserted] = groups_.try_emplace(gateway_id); if (inserted) { loadGroupStoreLocked(gateway_id, it->second); } return it->second; } void GatewayCache::loadSceneStoreLocked(uint8_t gateway_id, SceneStore& scenes) { for (uint8_t scene_id = 0; scene_id < scenes.size(); ++scene_id) { const auto raw = readStringLocked(ShortKey("sc", gateway_id, scene_id)); const auto values = ParseCsv(raw); if (values.size() >= 6) { scenes[scene_id].enabled = values[0] != 0; scenes[scene_id].brightness = static_cast(std::clamp(values[1], 0, 254)); scenes[scene_id].color_mode = static_cast(std::clamp(values[2], 0, 2)); scenes[scene_id].data1 = static_cast(std::clamp(values[3], 0, 255)); scenes[scene_id].data2 = static_cast(std::clamp(values[4], 0, 255)); scenes[scene_id].data3 = static_cast(std::clamp(values[5], 0, 255)); } scenes[scene_id].name = NormalizeName(readStringLocked(ShortKey("sn", gateway_id, scene_id))); } } void GatewayCache::loadGroupStoreLocked(uint8_t gateway_id, GroupStore& groups) { for (uint8_t group_id = 0; group_id < groups.size(); ++group_id) { const auto raw = readStringLocked(ShortKey("gr", gateway_id, group_id)); const auto values = ParseCsv(raw); if (values.size() >= 2) { groups[group_id].enabled = values[0] != 0; uint8_t target_type = static_cast(std::clamp(values[1], 0, 2)); uint8_t target_value = values.size() >= 3 ? static_cast(std::clamp(values[2], 0, 255)) : target_type; if (values.size() < 3) { target_type = 2; } groups[group_id].target_type = target_type; groups[group_id].target_value = target_type == 0 ? static_cast(std::min(target_value, 63)) : target_type == 1 ? static_cast(std::min(target_value, 15)) : 0; } groups[group_id].name = NormalizeName(readStringLocked(ShortKey("gn", gateway_id, group_id))); } } std::string GatewayCache::readStringLocked(std::string_view key) { if (!openStorageLocked()) { return {}; } size_t required_size = 0; const esp_err_t err = nvs_get_str(storage_, std::string(key).c_str(), nullptr, &required_size); if (err != ESP_OK || required_size == 0) { return {}; } std::string value(required_size - 1, '\0'); if (nvs_get_str(storage_, std::string(key).c_str(), value.data(), &required_size) != ESP_OK) { return {}; } return value; } bool GatewayCache::writeStringLocked(std::string_view key, std::string_view value) { if (storage_ == 0) { return false; } const esp_err_t err = nvs_set_str(storage_, std::string(key).c_str(), std::string(value).c_str()); if (err != ESP_OK) { ESP_LOGE(kTag, "failed to write cache key=%s err=%s", std::string(key).c_str(), esp_err_to_name(err)); return false; } return true; } bool GatewayCache::eraseKeyLocked(std::string_view key) { if (storage_ == 0) { return false; } const esp_err_t err = nvs_erase_key(storage_, std::string(key).c_str()); if (err == ESP_ERR_NVS_NOT_FOUND) { return true; } if (err != ESP_OK) { ESP_LOGE(kTag, "failed to erase cache key=%s err=%s", std::string(key).c_str(), esp_err_to_name(err)); return false; } return true; } } // namespace gateway