feat(gateway_cache): enhance DALI state management and caching

- Increased flush interval to 10 seconds and added a refresh interval of 120 seconds in GatewayCacheConfig.
- Introduced a new boolean `stale` in GatewayCacheDaliRuntimeStatus to track stale states.
- Added methods for setting actual DALI levels and persisting DALI address states.
- Implemented functions to build and apply DALI state payloads, including handling scene levels.
- Enhanced the GatewayCache class to manage DALI states more effectively, including loading and persisting states.
- Updated GatewayController to support cache refresh operations, including handling cache commands and reporting cache status.
- Added mechanisms for periodic cache refresh based on idle time and configured intervals.

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-21 15:34:26 +08:00
parent 2b8ef31263
commit 0827befb06
10 changed files with 748 additions and 64 deletions
@@ -24,7 +24,8 @@ struct GatewayCacheConfig {
bool cache_enabled{true};
bool reconciliation_enabled{true};
bool full_state_mirror_enabled{false};
uint32_t flush_interval_ms{5000};
uint32_t flush_interval_ms{10000};
uint32_t refresh_interval_ms{120000};
uint32_t task_stack_size{4096};
UBaseType_t task_priority{3};
GatewayCachePriorityMode default_priority_mode{GatewayCachePriorityMode::kOutsideBusFirst};
@@ -71,6 +72,7 @@ struct GatewayCacheDaliRuntimeStatus {
std::optional<uint8_t> actual_level;
std::optional<uint8_t> scene_id;
bool use_min_level{false};
bool stale{false};
uint32_t revision{0};
bool anyKnown() const {
@@ -144,6 +146,8 @@ class GatewayCache {
std::optional<uint8_t> level);
bool setDaliSettings(uint8_t gateway_id, uint8_t short_address,
std::optional<GatewayCacheDaliSettingsSnapshot> settings);
bool setDaliActualLevel(uint8_t gateway_id, uint8_t short_address,
std::optional<uint8_t> level);
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);
@@ -174,6 +178,8 @@ class GatewayCache {
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 persistDaliAddressStateLocked(uint8_t gateway_id, uint8_t short_address,
const GatewayCacheDaliAddressState& state);
bool commitStorageLocked();
bool shouldTrackUpdateFlagsLocked() const;
uint32_t nextDaliRuntimeRevisionLocked();
@@ -205,6 +211,8 @@ class GatewayCache {
GroupStore& ensureGroupStoreLocked(uint8_t gateway_id);
void loadSceneStoreLocked(uint8_t gateway_id, SceneStore& scenes);
void loadGroupStoreLocked(uint8_t gateway_id, GroupStore& groups);
void loadDaliStateStoreLocked(uint8_t gateway_id,
std::array<GatewayCacheDaliAddressState, 64>& states);
std::string readStringLocked(std::string_view key);
bool writeStringLocked(std::string_view key, std::string_view value);
bool eraseKeyLocked(std::string_view key);
+254 -4
View File
@@ -43,6 +43,18 @@ constexpr uint8_t kDaliCmdDt8StoreDtrAsColorX = 0xE0;
constexpr uint8_t kDaliCmdDt8StoreDtrAsColorY = 0xE1;
constexpr uint8_t kDaliCmdDt8StorePrimaryMin = 0xF0;
constexpr uint8_t kDaliCmdDt8StartAutoCalibration = 0xF6;
constexpr int kDaliStatePayloadVersion = 1;
constexpr uint32_t kDaliStateGroupMaskKnown = 1U << 0;
constexpr uint32_t kDaliStateActualKnown = 1U << 1;
constexpr uint32_t kDaliStateSceneKnown = 1U << 2;
constexpr uint32_t kDaliStateUseMinLevel = 1U << 3;
constexpr uint32_t kDaliStateStatusStale = 1U << 4;
constexpr uint32_t kDaliStatePowerOnKnown = 1U << 5;
constexpr uint32_t kDaliStateSystemFailureKnown = 1U << 6;
constexpr uint32_t kDaliStateMinKnown = 1U << 7;
constexpr uint32_t kDaliStateMaxKnown = 1U << 8;
constexpr uint32_t kDaliStateFadeTimeKnown = 1U << 9;
constexpr uint32_t kDaliStateFadeRateKnown = 1U << 10;
class LockGuard {
public:
@@ -134,6 +146,18 @@ bool ShouldMirrorObservedMutation(GatewayCacheRawFrameOrigin origin,
priority_mode == GatewayCachePriorityMode::kOutsideBusFirst;
}
bool ShouldAlwaysMirrorObservedStatus(uint8_t raw_addr, uint8_t command) {
if (!DecodeDaliTarget(raw_addr).has_value()) {
return false;
}
if ((raw_addr & 0x01) == 0) {
return command <= 254;
}
return command == kDaliCmdOff || command == kDaliCmdRecallMax ||
command == kDaliCmdRecallMin ||
(command >= kDaliCmdGoToSceneMin && command <= kDaliCmdGoToSceneMax);
}
void ClearDaliState(GatewayCacheDaliAddressState& state) {
state.group_mask_known = false;
state.group_mask = 0;
@@ -215,6 +239,139 @@ std::string BuildGroupPayload(const GatewayCache::GroupEntry& group) {
return std::string(payload);
}
uint8_t ByteValue(int value) {
return static_cast<uint8_t>(std::clamp(value, 0, 255));
}
uint16_t WordValue(int value) {
return static_cast<uint16_t>(std::clamp(value, 0, 0xffff));
}
uint16_t SceneKnownMask(const GatewayCacheDaliAddressState& state) {
uint16_t mask = 0;
for (size_t index = 0; index < state.scene_levels.size(); ++index) {
if (state.scene_levels[index].has_value()) {
mask |= static_cast<uint16_t>(1U << index);
}
}
return mask;
}
bool IsDefaultDaliAddressState(const GatewayCacheDaliAddressState& state) {
return !state.group_mask_known && state.group_mask == 0 && SceneKnownMask(state) == 0 &&
!state.settings.anyKnown() && !state.status.anyKnown();
}
uint32_t DaliStateFlags(const GatewayCacheDaliAddressState& state) {
uint32_t flags = 0;
if (state.group_mask_known) {
flags |= kDaliStateGroupMaskKnown;
}
if (state.status.actual_level.has_value()) {
flags |= kDaliStateActualKnown;
}
if (state.status.scene_id.has_value()) {
flags |= kDaliStateSceneKnown;
}
if (state.status.use_min_level) {
flags |= kDaliStateUseMinLevel;
}
if (state.status.stale) {
flags |= kDaliStateStatusStale;
}
if (state.settings.power_on_level.has_value()) {
flags |= kDaliStatePowerOnKnown;
}
if (state.settings.system_failure_level.has_value()) {
flags |= kDaliStateSystemFailureKnown;
}
if (state.settings.min_level.has_value()) {
flags |= kDaliStateMinKnown;
}
if (state.settings.max_level.has_value()) {
flags |= kDaliStateMaxKnown;
}
if (state.settings.fade_time.has_value()) {
flags |= kDaliStateFadeTimeKnown;
}
if (state.settings.fade_rate.has_value()) {
flags |= kDaliStateFadeRateKnown;
}
return flags;
}
std::string BuildDaliStatePayload(const GatewayCacheDaliAddressState& state) {
const uint16_t scene_known_mask = SceneKnownMask(state);
std::string payload = std::to_string(kDaliStatePayloadVersion);
payload += "," + std::to_string(DaliStateFlags(state));
payload += "," + std::to_string(state.status.revision);
payload += "," + std::to_string(state.group_mask);
payload += "," + std::to_string(state.status.actual_level.value_or(0));
payload += "," + std::to_string(state.status.scene_id.value_or(0));
payload += "," + std::to_string(state.settings.power_on_level.value_or(0));
payload += "," + std::to_string(state.settings.system_failure_level.value_or(0));
payload += "," + std::to_string(state.settings.min_level.value_or(0));
payload += "," + std::to_string(state.settings.max_level.value_or(0));
payload += "," + std::to_string(state.settings.fade_time.value_or(0));
payload += "," + std::to_string(state.settings.fade_rate.value_or(0));
payload += "," + std::to_string(scene_known_mask);
for (const auto& scene_level : state.scene_levels) {
payload += "," + std::to_string(scene_level.value_or(255));
}
return payload;
}
void ApplyDaliStatePayload(std::string_view raw, GatewayCacheDaliAddressState& state) {
const auto values = ParseCsv(raw);
if (values.size() < 13 || values[0] != kDaliStatePayloadVersion) {
return;
}
const uint32_t flags = static_cast<uint32_t>(std::max(values[1], 0));
state.group_mask_known = (flags & kDaliStateGroupMaskKnown) != 0;
state.group_mask = state.group_mask_known ? WordValue(values[3]) : 0;
state.status = {};
state.status.revision = static_cast<uint32_t>(std::max(values[2], 0));
state.status.stale = (flags & kDaliStateStatusStale) != 0;
state.status.use_min_level = (flags & kDaliStateUseMinLevel) != 0;
if ((flags & kDaliStateActualKnown) != 0) {
state.status.actual_level = ByteValue(values[4]);
}
if ((flags & kDaliStateSceneKnown) != 0) {
state.status.scene_id = static_cast<uint8_t>(std::min<int>(ByteValue(values[5]), 15));
}
state.settings = {};
if ((flags & kDaliStatePowerOnKnown) != 0) {
state.settings.power_on_level = ByteValue(values[6]);
}
if ((flags & kDaliStateSystemFailureKnown) != 0) {
state.settings.system_failure_level = ByteValue(values[7]);
}
if ((flags & kDaliStateMinKnown) != 0) {
state.settings.min_level = ByteValue(values[8]);
}
if ((flags & kDaliStateMaxKnown) != 0) {
state.settings.max_level = ByteValue(values[9]);
}
if ((flags & kDaliStateFadeTimeKnown) != 0) {
state.settings.fade_time = ByteValue(values[10]);
}
if ((flags & kDaliStateFadeRateKnown) != 0) {
state.settings.fade_rate = ByteValue(values[11]);
}
state.scene_levels.fill(std::nullopt);
const uint16_t scene_known_mask = WordValue(values[12]);
for (uint8_t scene_id = 0; scene_id < state.scene_levels.size(); ++scene_id) {
const size_t value_index = 13 + scene_id;
if ((scene_known_mask & (1U << scene_id)) != 0 && value_index < values.size()) {
state.scene_levels[scene_id] = ByteValue(values[value_index]);
}
}
}
} // namespace
GatewayCache::GatewayCache(GatewayCacheConfig config)
@@ -269,8 +426,11 @@ esp_err_t GatewayCache::start() {
return ESP_ERR_NO_MEM;
}
ESP_LOGI(kTag, "cache started namespace=%s flush_interval_ms=%u reconciliation=%d full_mirror=%d",
ESP_LOGI(kTag,
"cache started namespace=%s flush_interval_ms=%u refresh_interval_ms=%u "
"reconciliation=%d full_mirror=%d",
config_.storage_namespace.c_str(), static_cast<unsigned>(config_.flush_interval_ms),
static_cast<unsigned>(config_.refresh_interval_ms),
config_.reconciliation_enabled, config_.full_state_mirror_enabled);
return ESP_OK;
}
@@ -282,6 +442,10 @@ void GatewayCache::preloadChannel(uint8_t gateway_id) {
}
ensureSceneStoreLocked(gateway_id);
ensureGroupStoreLocked(gateway_id);
auto [it, inserted] = dali_states_.try_emplace(gateway_id);
if (inserted) {
loadDaliStateStoreLocked(gateway_id, it->second);
}
}
GatewayCache::SceneStore GatewayCache::scenes(uint8_t gateway_id) {
@@ -620,6 +784,7 @@ bool GatewayCache::setDaliGroupMask(uint8_t gateway_id, uint8_t short_address,
state.group_mask_known = group_mask.has_value();
state.group_mask = group_mask.value_or(0);
refreshDaliAddressAggregateStatusLocked(gateway_id, state);
dirty_ = true;
return true;
}
@@ -632,6 +797,7 @@ bool GatewayCache::setDaliSceneLevel(uint8_t gateway_id, uint8_t short_address,
auto& state = ensureDaliAddressStateLocked(gateway_id, short_address);
state.scene_levels[scene_id] = level;
dirty_ = true;
return true;
}
@@ -644,6 +810,31 @@ bool GatewayCache::setDaliSettings(uint8_t gateway_id, uint8_t short_address,
auto& state = ensureDaliAddressStateLocked(gateway_id, short_address);
state.settings = settings.value_or(GatewayCacheDaliSettingsSnapshot{});
dirty_ = true;
return true;
}
bool GatewayCache::setDaliActualLevel(uint8_t gateway_id, uint8_t short_address,
std::optional<uint8_t> level) {
LockGuard guard(lock_);
if (short_address >= 64) {
return false;
}
GatewayCacheDaliRuntimeStatus status;
status.actual_level = level;
status.revision = nextDaliRuntimeRevisionLocked();
status.stale = false;
auto& state = ensureDaliAddressStateLocked(gateway_id, short_address);
state.status.scene_id.reset();
state.status.use_min_level = false;
applyDaliRuntimeStatusToAddressLocked(state, status);
if (!level.has_value()) {
state.status.actual_level.reset();
state.status.revision = status.revision;
state.status.stale = false;
}
dirty_ = true;
return true;
}
@@ -712,7 +903,8 @@ bool GatewayCache::observeDaliCommand(uint8_t gateway_id, uint8_t raw_addr, uint
return false;
}
if (ShouldMirrorObservedMutation(origin, priority_mode_)) {
if (ShouldAlwaysMirrorObservedStatus(raw_addr, command) ||
ShouldMirrorObservedMutation(origin, priority_mode_)) {
mirrorDaliCommandLocked(gateway_id, raw_addr, command);
}
@@ -791,12 +983,15 @@ bool GatewayCache::mirrorDaliCommandLocked(uint8_t gateway_id, uint8_t raw_addr,
GatewayCacheDaliRuntimeStatus status;
status.actual_level = command;
status.revision = nextDaliRuntimeRevisionLocked();
status.stale = false;
applyDaliTargetRuntimeStatusLocked(gateway_id, *target, status);
dirty_ = true;
return true;
}
if (command == kDaliCmdReset) {
clearDaliTargetStateLocked(gateway_id, *target, nextDaliRuntimeRevisionLocked());
dirty_ = true;
return true;
}
@@ -804,7 +999,9 @@ bool GatewayCache::mirrorDaliCommandLocked(uint8_t gateway_id, uint8_t raw_addr,
GatewayCacheDaliRuntimeStatus status;
status.actual_level = command == kDaliCmdOff ? 0 : 254;
status.revision = nextDaliRuntimeRevisionLocked();
status.stale = false;
applyDaliTargetRuntimeStatusLocked(gateway_id, *target, status);
dirty_ = true;
return true;
}
@@ -812,7 +1009,9 @@ bool GatewayCache::mirrorDaliCommandLocked(uint8_t gateway_id, uint8_t raw_addr,
GatewayCacheDaliRuntimeStatus status;
status.use_min_level = true;
status.revision = nextDaliRuntimeRevisionLocked();
status.stale = false;
applyDaliTargetRuntimeStatusLocked(gateway_id, *target, status);
dirty_ = true;
return true;
}
@@ -820,7 +1019,9 @@ bool GatewayCache::mirrorDaliCommandLocked(uint8_t gateway_id, uint8_t raw_addr,
GatewayCacheDaliRuntimeStatus status;
status.scene_id = static_cast<uint8_t>(command - kDaliCmdGoToSceneMin);
status.revision = nextDaliRuntimeRevisionLocked();
status.stale = false;
applyDaliTargetRuntimeStatusLocked(gateway_id, *target, status);
dirty_ = true;
return true;
}
@@ -828,6 +1029,7 @@ bool GatewayCache::mirrorDaliCommandLocked(uint8_t gateway_id, uint8_t raw_addr,
applyDaliTargetGroupMutationLocked(gateway_id, *target,
static_cast<uint8_t>(command & 0x0F),
command < (kDaliCmdAddToGroupMin + 16));
dirty_ = true;
return true;
}
@@ -836,6 +1038,7 @@ bool GatewayCache::mirrorDaliCommandLocked(uint8_t gateway_id, uint8_t raw_addr,
applyDaliTargetSceneLevelLocked(gateway_id, *target,
static_cast<uint8_t>(command - kDaliCmdSetSceneMin),
*dtr_state.dtr0);
dirty_ = true;
return true;
}
@@ -843,12 +1046,14 @@ bool GatewayCache::mirrorDaliCommandLocked(uint8_t gateway_id, uint8_t raw_addr,
applyDaliTargetSceneLevelLocked(
gateway_id, *target, static_cast<uint8_t>(command - (kDaliCmdSetSceneMin + 16)),
static_cast<uint8_t>(255U));
dirty_ = true;
return true;
}
if (command >= kDaliCmdStoreDtrAsMaxLevel && command <= kDaliCmdStoreDtrAsFadeRate &&
dtr_state.dtr0.has_value()) {
applyDaliTargetSettingsLocked(gateway_id, *target, command, *dtr_state.dtr0);
dirty_ = true;
return true;
}
@@ -977,6 +1182,7 @@ void GatewayCache::applyDaliRuntimeStatusToAddressLocked(
}
}
state.status.revision = status.revision;
state.status.stale = status.stale;
}
void GatewayCache::applyDaliTargetGroupMutationLocked(uint8_t gateway_id,
@@ -1127,7 +1333,9 @@ void GatewayCache::refreshDaliAddressAggregateStatusLocked(uint8_t gateway_id,
if (const auto broadcast = dali_broadcast_status_.find(gateway_id);
broadcast != dali_broadcast_status_.end()) {
applyDaliRuntimeStatusToAddressLocked(state, broadcast->second);
if (!broadcast->second.stale) {
applyDaliRuntimeStatusToAddressLocked(state, broadcast->second);
}
}
const auto groups = dali_group_status_.find(gateway_id);
@@ -1137,6 +1345,9 @@ void GatewayCache::refreshDaliAddressAggregateStatusLocked(uint8_t gateway_id,
for (uint8_t group_id = 0; group_id < groups->second.size(); ++group_id) {
const uint16_t bit = static_cast<uint16_t>(1U << group_id);
if ((state.group_mask & bit) != 0) {
if (groups->second[group_id].stale) {
continue;
}
applyDaliRuntimeStatusToAddressLocked(state, groups->second[group_id]);
}
}
@@ -1209,6 +1420,14 @@ bool GatewayCache::flushDirty() {
}
}
for (const auto& [gateway_id, states] : dali_states_) {
for (uint8_t short_address = 0; short_address < states.size(); ++short_address) {
if (!persistDaliAddressStateLocked(gateway_id, short_address, states[short_address])) {
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));
@@ -1288,6 +1507,18 @@ bool GatewayCache::persistGroupLocked(uint8_t gateway_id, uint8_t group_id,
return commitStorageLocked();
}
bool GatewayCache::persistDaliAddressStateLocked(
uint8_t gateway_id, uint8_t short_address, const GatewayCacheDaliAddressState& state) {
if (short_address >= 64) {
return false;
}
if (!IsDefaultDaliAddressState(state)) {
return writeStringLocked(ShortKey("ds", gateway_id, short_address),
BuildDaliStatePayload(state));
}
return eraseKeyLocked(ShortKey("ds", gateway_id, short_address));
}
bool GatewayCache::commitStorageLocked() {
if (storage_ == 0) {
return false;
@@ -1307,7 +1538,9 @@ bool GatewayCache::shouldTrackUpdateFlagsLocked() const {
GatewayCacheDaliAddressState& GatewayCache::ensureDaliAddressStateLocked(uint8_t gateway_id,
uint8_t short_address) {
auto [it, inserted] = dali_states_.try_emplace(gateway_id);
(void)inserted;
if (inserted) {
loadDaliStateStoreLocked(gateway_id, it->second);
}
return it->second[short_address];
}
@@ -1377,6 +1610,23 @@ void GatewayCache::loadGroupStoreLocked(uint8_t gateway_id, GroupStore& groups)
}
}
void GatewayCache::loadDaliStateStoreLocked(
uint8_t gateway_id, std::array<GatewayCacheDaliAddressState, 64>& states) {
for (uint8_t short_address = 0; short_address < states.size(); ++short_address) {
ClearDaliState(states[short_address]);
const auto raw = readStringLocked(ShortKey("ds", gateway_id, short_address));
if (!raw.empty()) {
ApplyDaliStatePayload(raw, states[short_address]);
if (states[short_address].status.anyKnown()) {
states[short_address].status.stale = true;
}
if (states[short_address].status.revision > dali_runtime_revision_) {
dali_runtime_revision_ = states[short_address].status.revision;
}
}
}
}
std::string GatewayCache::readStringLocked(std::string_view key) {
if (!openStorageLocked()) {
return {};