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