Files
gateway/components/gateway_cache/src/gateway_cache.cpp
T

1425 lines
44 KiB
C++

#include "gateway_cache.hpp"
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <utility>
#include <vector>
#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<int> ParseCsv(std::string_view raw) {
std::vector<int> 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<int>(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<GatewayCacheDaliTarget> DecodeDaliTarget(uint8_t raw_addr) {
if (raw_addr <= 0x7F) {
return GatewayCacheDaliTarget{GatewayCacheDaliTargetKind::kShortAddress,
static_cast<uint8_t>(raw_addr >> 1)};
}
if (raw_addr >= kDaliGroupRawMin && raw_addr <= kDaliGroupRawMax) {
return GatewayCacheDaliTarget{GatewayCacheDaliTargetKind::kGroup,
static_cast<uint8_t>((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<unsigned>(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<uint8_t, uint8_t> 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<uint16_t>(1U << index);
}
}
return {static_cast<uint8_t>(mask & 0xff), static_cast<uint8_t>((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<uint8_t, uint8_t> 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<uint16_t>(1U << index);
}
}
return {static_cast<uint8_t>(mask & 0xff), static_cast<uint8_t>((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<uint16_t> 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<uint8_t> 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<GatewayCacheDaliSettingsSnapshot> 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<uint8_t>(command - kDaliCmdGoToSceneMin);
status.revision = nextDaliRuntimeRevisionLocked();
applyDaliTargetRuntimeStatusLocked(gateway_id, *target, status);
return true;
}
if (command >= kDaliCmdAddToGroupMin && command <= kDaliCmdRemoveFromGroupMax) {
applyDaliTargetGroupMutationLocked(gateway_id, *target,
static_cast<uint8_t>(command & 0x0F),
command < (kDaliCmdAddToGroupMin + 16));
return true;
}
if (command >= kDaliCmdSetSceneMin && command < (kDaliCmdSetSceneMin + 16) &&
dtr_state.dtr0.has_value()) {
applyDaliTargetSceneLevelLocked(gateway_id, *target,
static_cast<uint8_t>(command - kDaliCmdSetSceneMin),
*dtr_state.dtr0);
return true;
}
if (command >= (kDaliCmdSetSceneMin + 16) && command <= kDaliCmdRemoveSceneMax) {
applyDaliTargetSceneLevelLocked(
gateway_id, *target, static_cast<uint8_t>(command - (kDaliCmdSetSceneMin + 16)),
static_cast<uint8_t>(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<uint16_t>(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<uint16_t>(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<uint16_t>(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<uint16_t>(~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<uint16_t>(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<uint8_t> 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<uint16_t>(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<uint16_t>(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<uint16_t>(1U << group_id);
if ((state.group_mask & bit) != 0) {
applyDaliRuntimeStatusToAddressLocked(state, groups->second[group_id]);
}
}
}
void GatewayCache::TaskEntry(void* arg) {
static_cast<GatewayCache*>(arg)->taskLoop();
}
void GatewayCache::taskLoop() {
const TickType_t interval_ticks =
std::max<TickType_t>(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<uint8_t>(std::clamp(values[1], 0, 254));
scenes[scene_id].color_mode = static_cast<uint8_t>(std::clamp(values[2], 0, 2));
scenes[scene_id].data1 = static_cast<uint8_t>(std::clamp(values[3], 0, 255));
scenes[scene_id].data2 = static_cast<uint8_t>(std::clamp(values[4], 0, 255));
scenes[scene_id].data3 = static_cast<uint8_t>(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<uint8_t>(std::clamp(values[1], 0, 2));
uint8_t target_value = values.size() >= 3 ? static_cast<uint8_t>(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<uint8_t>(std::min<int>(target_value, 63))
: target_type == 1
? static_cast<uint8_t>(std::min<int>(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