feat(gateway_cache): enhance DALI state management and caching
- Increased flush interval to 10 seconds and added a refresh interval of 120 seconds in GatewayCacheConfig. - Introduced a new boolean `stale` in GatewayCacheDaliRuntimeStatus to track stale states. - Added methods for setting actual DALI levels and persisting DALI address states. - Implemented functions to build and apply DALI state payloads, including handling scene levels. - Enhanced the GatewayCache class to manage DALI states more effectively, including loading and persisting states. - Updated GatewayController to support cache refresh operations, including handling cache commands and reporting cache status. - Added mechanisms for periodic cache refresh based on idle time and configured intervals. Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
@@ -26,6 +26,32 @@ update as the project evolves:
|
||||
- `knx`: The forked OpenKNX cEMI programming support, ESP-IDF port, and KNX security storage used by the gateway. You can edit this code when necessary to support missing ETS programming features or to implement the secure-session transport path.
|
||||
- `knx_dali_gw`: The forked OpenKNX DALI-GW function-property support used by the gateway. You can edit this code when necessary to support missing DALI-GW features or to fix bugs.
|
||||
|
||||
## Gateway DALI cache
|
||||
|
||||
The gateway owns the shared DALI cache for multi-user deployments. App, BLE,
|
||||
IP, Modbus, BACnet, KNX, and local control paths should treat the gateway cache
|
||||
as the shared read surface instead of maintaining separate app-side device-state
|
||||
caches. Transparent/setup raw forwarding paths remain bypass-oriented, but raw
|
||||
DALI bus observation still feeds the passive decoder when frames are visible to
|
||||
the gateway.
|
||||
|
||||
`gateway_cache` stores internal scene/group data and per-short-address DALI
|
||||
state. Device settings, group masks, scene levels, known flags, and the last
|
||||
runtime status are batched to NVS using `GATEWAY_CACHE_FLUSH_INTERVAL_MS`, which
|
||||
defaults to 10000 ms. Runtime status loaded from disk is marked stale until the
|
||||
gateway observes a bus command or the background refresher verifies it again.
|
||||
|
||||
`GATEWAY_CACHE_REFRESH_INTERVAL_MS` defaults to 120000 ms. When nonzero, the
|
||||
controller maintenance loop refreshes direct short-address actual levels one
|
||||
small step at a time. The refresh is low priority: it yields to queued gateway
|
||||
commands, address allocation, live management reads, bridge traffic, and any raw
|
||||
DALI bus activity from another master until the bus has been idle. Group and
|
||||
broadcast targets are never queried for refresh.
|
||||
|
||||
Gateway feature opcode `0x06` advertises cache support with bit `0x40`. Gateway
|
||||
opcode `0x39` returns cache summary and target snapshots so frontend clients can
|
||||
read cached state without issuing live DALI queries on supported gateways.
|
||||
|
||||
## Current status
|
||||
|
||||
The native rewrite now wires a shared `gateway_core` bootstrap component, a multi-channel `dali_domain` wrapper over `dali_cpp`, a local vendored `dali` hardware backend from the LuatOS ESP-IDF port with raw receive fan-out, an initial `gateway_runtime` service that provides persistent settings, device info, Lua-compatible command framing helpers, and Lua-style query command deduplication, plus a `gateway_controller` service that starts the gateway command task, dispatches core Lua gateway opcodes, and owns internal scene/group state. The gateway app also includes a `gateway_ble` NimBLE bridge that advertises a Lua-compatible GATT service and forwards `FFF3` framed notifications, incoming `FFF1`/`FFF2`/`FFF3` writes, and native raw DALI frame notifications into the matching raw channel, a `gateway_network` service that provides the native HTTP `/info`, `GET`/`POST /dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP control-plane router on port `2020`, W5500 SPI Ethernet with DHCP, Wi-Fi STA lifecycle, ESP-Touch smartconfig credential provisioning, the Lua-style `LAMMIN_Gateway` setup AP on `192.168.3.1`, ESP-NOW setup ingress for Lua-compatible `connReq`/`connAck`/`echo`/`cmd`/`data`/`uart` packets, native raw DALI frame forwarding back to connected setup peers, setup AP GPIO entry, and optional Wi-Fi credential reset GPIO handling, and an optional `gateway_485_control` bridge that claims UART0 for Lua-compatible framed command ingress plus `0x22` notification egress when the console is moved off UART0. Startup behavior is configured in `main/Kconfig.projbuild`: BLE and wired Ethernet are enabled by default, W5500 initialization and startup probe failures are ignored by default for boards without populated Ethernet hardware by fully disabling Ethernet for that boot, Wi-Fi STA, smartconfig, and ESP-NOW setup mode are disabled by default, the built-in USB Serial/JTAG interface stays in debug mode unless the optional USB setup bridge mode is selected, and the UART0 control bridge stays disabled unless the deployment explicitly repurposes UART0 away from the ESP-IDF console. Runtime settings and internal scene/group data are cached in RAM after load, skip unchanged flash writes, and batch Wi-Fi credential commits to reduce flash stalls on ESP32-S3 boards where flash and PSRAM share the SPI bus. The gateway app exposes per-channel PHY selection through `main/Kconfig.projbuild`; each channel can be disabled, bound to the native DALI GPIO HAL, or bound to a UART1/UART2 serial PHY. The checked-in `sdkconfig` is aligned with the app's custom 16 MB partition table so the Wi-Fi/BLE/network-enabled image fits the OTA app slots.
|
||||
|
||||
@@ -259,9 +259,18 @@ config GATEWAY_CACHE_FLUSH_INTERVAL_MS
|
||||
int "Cache flush interval ms"
|
||||
depends on GATEWAY_CACHE_SUPPORTED && GATEWAY_CACHE_START_ENABLED
|
||||
range 100 600000
|
||||
default 5000
|
||||
default 10000
|
||||
help
|
||||
Interval used to batch scene and group cache writes to flash.
|
||||
Interval used to batch gateway cache writes to flash.
|
||||
|
||||
config GATEWAY_CACHE_REFRESH_INTERVAL_MS
|
||||
int "Cache status refresh interval ms"
|
||||
depends on GATEWAY_CACHE_SUPPORTED && GATEWAY_CACHE_START_ENABLED
|
||||
range 0 3600000
|
||||
default 120000
|
||||
help
|
||||
Interval used by the low-priority DALI status refresh scheduler. Set to
|
||||
0 to disable scheduled refresh while keeping command and bus mirroring.
|
||||
|
||||
choice GATEWAY_CACHE_CONFLICT_PRIORITY
|
||||
prompt "Cache conflict priority default"
|
||||
|
||||
@@ -268,7 +268,11 @@
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS
|
||||
#define CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS 5000
|
||||
#define CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS 10000
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_GATEWAY_CACHE_REFRESH_INTERVAL_MS
|
||||
#define CONFIG_GATEWAY_CACHE_REFRESH_INTERVAL_MS 120000
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
@@ -785,6 +789,8 @@ extern "C" void app_main(void) {
|
||||
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.refresh_interval_ms =
|
||||
static_cast<uint32_t>(CONFIG_GATEWAY_CACHE_REFRESH_INTERVAL_MS);
|
||||
cache_config.default_priority_mode = kCachePriorityMode;
|
||||
s_cache = std::make_unique<gateway::GatewayCache>(cache_config);
|
||||
ESP_ERROR_CHECK(s_cache->start());
|
||||
@@ -797,6 +803,9 @@ extern "C" void app_main(void) {
|
||||
controller_config.ip_router_supported = network_transport_supported;
|
||||
controller_config.internal_scene_supported = true;
|
||||
controller_config.internal_group_supported = true;
|
||||
controller_config.cache_supported = kCacheSupported;
|
||||
controller_config.cache_refresh_interval_ms =
|
||||
static_cast<uint32_t>(CONFIG_GATEWAY_CACHE_REFRESH_INTERVAL_MS);
|
||||
|
||||
s_controller = std::make_unique<gateway::GatewayController>(*s_runtime, *s_dali_domain,
|
||||
*s_cache,
|
||||
|
||||
@@ -623,7 +623,8 @@ CONFIG_GATEWAY_CHANNEL1_NATIVE_BAUDRATE=1200
|
||||
CONFIG_GATEWAY_CACHE_SUPPORTED=y
|
||||
CONFIG_GATEWAY_CACHE_START_ENABLED=y
|
||||
# CONFIG_GATEWAY_CACHE_RECONCILIATION_ENABLED is not set
|
||||
CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS=60000
|
||||
CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS=10000
|
||||
CONFIG_GATEWAY_CACHE_REFRESH_INTERVAL_MS=120000
|
||||
# end of Gateway Cache
|
||||
|
||||
# CONFIG_GATEWAY_ENABLE_DALI_BUS is not set
|
||||
|
||||
@@ -126,6 +126,7 @@ class DaliDomainService {
|
||||
void addRawFrameSink(std::function<void(const DaliRawFrame& frame)> sink);
|
||||
|
||||
bool resetBus(uint8_t gateway_id) const;
|
||||
bool isBusIdle(uint8_t gateway_id, uint32_t quiet_ms) const;
|
||||
bool writeBridgeFrame(uint8_t gateway_id, const uint8_t* data, size_t len) const;
|
||||
std::vector<uint8_t> transactBridgeFrame(uint8_t gateway_id, const uint8_t* data,
|
||||
size_t len) const;
|
||||
@@ -163,6 +164,7 @@ class DaliDomainService {
|
||||
bool on(uint8_t gateway_id, int short_address) const;
|
||||
bool off(uint8_t gateway_id, int short_address) const;
|
||||
bool off(int short_address) const;
|
||||
std::optional<uint8_t> queryActualLevel(uint8_t gateway_id, 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,
|
||||
@@ -195,10 +197,13 @@ class DaliDomainService {
|
||||
static void RawFrameTaskEntry(void* arg);
|
||||
void rawFrameTaskLoop();
|
||||
void notifyRawFrameSinks(const DaliRawFrame& frame);
|
||||
void markBusActivity(uint8_t gateway_id) const;
|
||||
|
||||
std::vector<std::unique_ptr<DaliChannel>> channels_;
|
||||
std::vector<std::function<void(const DaliRawFrame& frame)>> raw_frame_sinks_;
|
||||
SemaphoreHandle_t raw_frame_sink_lock_{nullptr};
|
||||
mutable SemaphoreHandle_t bus_activity_lock_{nullptr};
|
||||
mutable std::map<uint8_t, TickType_t> last_bus_activity_ticks_;
|
||||
TaskHandle_t raw_frame_task_handle_{nullptr};
|
||||
};
|
||||
|
||||
|
||||
@@ -314,11 +314,21 @@ struct DaliDomainService::DaliChannel {
|
||||
};
|
||||
|
||||
DaliDomainService::DaliDomainService()
|
||||
: raw_frame_sink_lock_(xSemaphoreCreateMutex()) {
|
||||
: raw_frame_sink_lock_(xSemaphoreCreateMutex()),
|
||||
bus_activity_lock_(xSemaphoreCreateMutex()) {
|
||||
esp_log_level_set(TAG, (esp_log_level_t)CONFIG_DALI_LOG_LEVEL);
|
||||
}
|
||||
|
||||
DaliDomainService::~DaliDomainService() = default;
|
||||
DaliDomainService::~DaliDomainService() {
|
||||
if (raw_frame_sink_lock_ != nullptr) {
|
||||
vSemaphoreDelete(raw_frame_sink_lock_);
|
||||
raw_frame_sink_lock_ = nullptr;
|
||||
}
|
||||
if (bus_activity_lock_ != nullptr) {
|
||||
vSemaphoreDelete(bus_activity_lock_);
|
||||
bus_activity_lock_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool DaliDomainService::bindTransport(const DaliChannelConfig& config, DaliTransportHooks hooks) {
|
||||
if (!hooks.send) {
|
||||
@@ -533,12 +543,38 @@ void DaliDomainService::addRawFrameSink(std::function<void(const DaliRawFrame& f
|
||||
|
||||
bool DaliDomainService::resetBus(uint8_t gateway_id) const {
|
||||
const auto* channel = findChannelByGateway(gateway_id);
|
||||
return channel != nullptr && channel->comm != nullptr && channel->comm->resetBus();
|
||||
if (channel == nullptr || channel->comm == nullptr) {
|
||||
return false;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
return channel->comm->resetBus();
|
||||
}
|
||||
|
||||
bool DaliDomainService::isBusIdle(uint8_t gateway_id, uint32_t quiet_ms) const {
|
||||
TickType_t last_activity = 0;
|
||||
if (bus_activity_lock_ != nullptr) {
|
||||
xSemaphoreTake(bus_activity_lock_, portMAX_DELAY);
|
||||
}
|
||||
if (const auto it = last_bus_activity_ticks_.find(gateway_id);
|
||||
it != last_bus_activity_ticks_.end()) {
|
||||
last_activity = it->second;
|
||||
}
|
||||
if (bus_activity_lock_ != nullptr) {
|
||||
xSemaphoreGive(bus_activity_lock_);
|
||||
}
|
||||
if (last_activity == 0) {
|
||||
return true;
|
||||
}
|
||||
return (xTaskGetTickCount() - last_activity) >= pdMS_TO_TICKS(quiet_ms);
|
||||
}
|
||||
|
||||
bool DaliDomainService::writeBridgeFrame(uint8_t gateway_id, const uint8_t* data, size_t len) const {
|
||||
const auto* channel = findChannelByGateway(gateway_id);
|
||||
return channel != nullptr && channel->hooks.send && channel->hooks.send(data, len);
|
||||
if (channel == nullptr || !channel->hooks.send) {
|
||||
return false;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
return channel->hooks.send(data, len);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> DaliDomainService::transactBridgeFrame(uint8_t gateway_id,
|
||||
@@ -548,17 +584,26 @@ std::vector<uint8_t> DaliDomainService::transactBridgeFrame(uint8_t gateway_id,
|
||||
if (channel == nullptr || !channel->hooks.transact) {
|
||||
return {};
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
return channel->hooks.transact(data, len);
|
||||
}
|
||||
|
||||
bool DaliDomainService::sendRaw(uint8_t gateway_id, uint8_t raw_addr, uint8_t command) const {
|
||||
const auto* channel = findChannelByGateway(gateway_id);
|
||||
return channel != nullptr && channel->comm != nullptr && channel->comm->sendRawNew(raw_addr, command);
|
||||
if (channel == nullptr || channel->comm == nullptr) {
|
||||
return false;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
return channel->comm->sendRawNew(raw_addr, command);
|
||||
}
|
||||
|
||||
bool DaliDomainService::sendExtRaw(uint8_t gateway_id, uint8_t raw_addr, uint8_t command) const {
|
||||
const auto* channel = findChannelByGateway(gateway_id);
|
||||
return channel != nullptr && channel->comm != nullptr && channel->comm->sendExtRawNew(raw_addr, command);
|
||||
if (channel == nullptr || channel->comm == nullptr) {
|
||||
return false;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
return channel->comm->sendExtRawNew(raw_addr, command);
|
||||
}
|
||||
|
||||
std::optional<uint8_t> DaliDomainService::queryRaw(uint8_t gateway_id, uint8_t raw_addr,
|
||||
@@ -567,6 +612,7 @@ std::optional<uint8_t> DaliDomainService::queryRaw(uint8_t gateway_id, uint8_t r
|
||||
if (channel == nullptr || channel->comm == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
return channel->comm->queryRawNew(raw_addr, command);
|
||||
}
|
||||
|
||||
@@ -577,6 +623,7 @@ std::optional<DaliDomainSnapshot> DaliDomainService::discoverDeviceTypes(
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
const std::vector<int> fallback = fallback_types.empty() ? std::vector<int>{1, 4, 5, 6, 8}
|
||||
: fallback_types;
|
||||
auto discovery = channel->dali->base.discoverDeviceTypes(short_address, fallback,
|
||||
@@ -598,6 +645,7 @@ std::optional<DaliDomainSnapshot> DaliDomainService::baseStatusSnapshot(
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
const auto raw_status = channel->dali->base.getStatus(short_address);
|
||||
if (!raw_status.has_value()) {
|
||||
return std::nullopt;
|
||||
@@ -623,6 +671,7 @@ std::optional<DaliDomainSnapshot> DaliDomainService::dt1Snapshot(uint8_t gateway
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
const auto detailed = channel->dali->dt1.getDT1TestStatusDetailed(short_address);
|
||||
if (!detailed.has_value()) {
|
||||
return std::nullopt;
|
||||
@@ -705,6 +754,7 @@ std::optional<DaliDomainSnapshot> DaliDomainService::dt4Snapshot(uint8_t gateway
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
auto snapshot = MakeSnapshot(gateway_id, short_address, "dt4");
|
||||
auto& dt4 = channel->dali->dt4;
|
||||
PutOptionalInt(snapshot, "extendedVersion", dt4.getExtendedVersion(short_address));
|
||||
@@ -787,6 +837,7 @@ std::optional<DaliDomainSnapshot> DaliDomainService::dt5Snapshot(uint8_t gateway
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
auto snapshot = MakeSnapshot(gateway_id, short_address, "dt5");
|
||||
auto& dt5 = channel->dali->dt5;
|
||||
PutOptionalInt(snapshot, "extendedVersion", dt5.getExtendedVersion(short_address));
|
||||
@@ -828,6 +879,7 @@ std::optional<DaliDomainSnapshot> DaliDomainService::dt6Snapshot(uint8_t gateway
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
auto snapshot = MakeSnapshot(gateway_id, short_address, "dt6");
|
||||
auto& dt6 = channel->dali->dt6;
|
||||
PutOptionalInt(snapshot, "extendedVersion", dt6.getExtendedVersion(short_address));
|
||||
@@ -891,6 +943,7 @@ std::optional<DaliDomainSnapshot> DaliDomainService::dt8StatusSnapshot(
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
|
||||
auto snapshot = MakeSnapshot(gateway_id, short_address, "dt8_status");
|
||||
bool has_data = false;
|
||||
@@ -925,6 +978,7 @@ std::optional<DaliDomainSnapshot> DaliDomainService::dt8SceneColorReport(
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
const auto report = channel->dali->dt8.getSceneColorReport(short_address, scene);
|
||||
if (!report.has_value()) {
|
||||
return std::nullopt;
|
||||
@@ -948,6 +1002,7 @@ std::optional<DaliDomainSnapshot> DaliDomainService::dt8PowerOnLevelColorReport(
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
const auto report = channel->dali->dt8.getPowerOnLevelColorReport(short_address);
|
||||
if (!report.has_value()) {
|
||||
return std::nullopt;
|
||||
@@ -970,6 +1025,7 @@ std::optional<DaliDomainSnapshot> DaliDomainService::dt8SystemFailureLevelColorR
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
const auto report = channel->dali->dt8.getSystemFailureLevelColorReport(short_address);
|
||||
if (!report.has_value()) {
|
||||
return std::nullopt;
|
||||
@@ -992,8 +1048,11 @@ bool DaliDomainService::storeDt8SceneSnapshot(uint8_t gateway_id, int short_addr
|
||||
int color_temperature, int red, int green,
|
||||
int blue) const {
|
||||
const auto* channel = findChannelByGateway(gateway_id);
|
||||
return channel != nullptr && channel->dali != nullptr &&
|
||||
channel->dali->dt8.storeSceneSnapshot(short_address, scene, brightness,
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return false;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
return channel->dali->dt8.storeSceneSnapshot(short_address, scene, brightness,
|
||||
ToDaliCppColorMode(color_mode), color_temperature,
|
||||
red, green, blue);
|
||||
}
|
||||
@@ -1001,56 +1060,85 @@ bool DaliDomainService::storeDt8SceneSnapshot(uint8_t gateway_id, int short_addr
|
||||
bool DaliDomainService::storeDt8PowerOnLevelSnapshot(uint8_t gateway_id, int short_address,
|
||||
int level) const {
|
||||
const auto* channel = findChannelByGateway(gateway_id);
|
||||
return channel != nullptr && channel->dali != nullptr &&
|
||||
channel->dali->dt8.storePowerOnLevelSnapshot(short_address, level);
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return false;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
return channel->dali->dt8.storePowerOnLevelSnapshot(short_address, level);
|
||||
}
|
||||
|
||||
bool DaliDomainService::storeDt8SystemFailureLevelSnapshot(uint8_t gateway_id,
|
||||
int short_address, int level) const {
|
||||
const auto* channel = findChannelByGateway(gateway_id);
|
||||
return channel != nullptr && channel->dali != nullptr &&
|
||||
channel->dali->dt8.storeSystemFailureLevelSnapshot(short_address, level);
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return false;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
return channel->dali->dt8.storeSystemFailureLevelSnapshot(short_address, level);
|
||||
}
|
||||
|
||||
bool DaliDomainService::setBright(uint8_t gateway_id, int short_address, int brightness) const {
|
||||
const auto* channel = findChannelByGateway(gateway_id);
|
||||
return channel != nullptr && channel->dali != nullptr &&
|
||||
channel->dali->base.setBright(short_address, brightness);
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return false;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
return channel->dali->base.setBright(short_address, brightness);
|
||||
}
|
||||
|
||||
bool DaliDomainService::setColTempRaw(uint8_t gateway_id, int short_address, int mirek) const {
|
||||
const auto* channel = findChannelByGateway(gateway_id);
|
||||
return channel != nullptr && channel->dali != nullptr &&
|
||||
channel->dali->dt8.setColTempRaw(short_address, mirek);
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return false;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
return channel->dali->dt8.setColTempRaw(short_address, mirek);
|
||||
}
|
||||
|
||||
bool DaliDomainService::setColTemp(uint8_t gateway_id, int short_address, int kelvin) const {
|
||||
const auto* channel = findChannelByGateway(gateway_id);
|
||||
return channel != nullptr && channel->dali != nullptr &&
|
||||
channel->dali->dt8.setColorTemperature(short_address, kelvin);
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return false;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
return channel->dali->dt8.setColorTemperature(short_address, kelvin);
|
||||
}
|
||||
|
||||
bool DaliDomainService::setColourRaw(uint8_t gateway_id, int raw_addr, int x, int y) const {
|
||||
const auto* channel = findChannelByGateway(gateway_id);
|
||||
return channel != nullptr && channel->dali != nullptr &&
|
||||
channel->dali->dt8.setColourRaw(raw_addr, x, y);
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return false;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
return channel->dali->dt8.setColourRaw(raw_addr, x, y);
|
||||
}
|
||||
|
||||
bool DaliDomainService::setColourRGB(uint8_t gateway_id, int short_address, int r, int g,
|
||||
int b) const {
|
||||
const auto* channel = findChannelByGateway(gateway_id);
|
||||
return channel != nullptr && channel->dali != nullptr &&
|
||||
channel->dali->dt8.setColourRGB(short_address, r, g, b);
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return false;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
return channel->dali->dt8.setColourRGB(short_address, r, g, b);
|
||||
}
|
||||
|
||||
bool DaliDomainService::on(uint8_t gateway_id, int short_address) const {
|
||||
const auto* channel = findChannelByGateway(gateway_id);
|
||||
return channel != nullptr && channel->dali != nullptr && channel->dali->base.on(short_address);
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return false;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
return channel->dali->base.on(short_address);
|
||||
}
|
||||
|
||||
bool DaliDomainService::off(uint8_t gateway_id, int short_address) const {
|
||||
const auto* channel = findChannelByGateway(gateway_id);
|
||||
return channel != nullptr && channel->dali != nullptr && channel->dali->base.off(short_address);
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return false;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
return channel->dali->base.off(short_address);
|
||||
}
|
||||
|
||||
bool DaliDomainService::off(int short_address) const {
|
||||
@@ -1061,6 +1149,20 @@ bool DaliDomainService::off(int short_address) const {
|
||||
return off(channels_.front()->config.gateway_id, short_address);
|
||||
}
|
||||
|
||||
std::optional<uint8_t> DaliDomainService::queryActualLevel(uint8_t gateway_id,
|
||||
int short_address) const {
|
||||
const auto* channel = findChannelByGateway(gateway_id);
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
const auto level = channel->dali->base.getBright(short_address);
|
||||
if (!level.has_value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return static_cast<uint8_t>(std::clamp(*level, 0, 254));
|
||||
}
|
||||
|
||||
std::optional<uint16_t> DaliDomainService::queryGroupMask(uint8_t gateway_id,
|
||||
int short_address) const {
|
||||
const auto* channel = findChannelByGateway(gateway_id);
|
||||
@@ -1068,6 +1170,8 @@ std::optional<uint16_t> DaliDomainService::queryGroupMask(uint8_t gateway_id,
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
markBusActivity(gateway_id);
|
||||
|
||||
const auto group_mask = channel->dali->base.getGroup(short_address);
|
||||
if (!group_mask.has_value()) {
|
||||
return std::nullopt;
|
||||
@@ -1083,6 +1187,8 @@ std::optional<uint8_t> DaliDomainService::querySceneLevel(uint8_t gateway_id, in
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
markBusActivity(gateway_id);
|
||||
|
||||
const auto level = channel->dali->base.getScene(short_address, scene);
|
||||
if (!level.has_value()) {
|
||||
return std::nullopt;
|
||||
@@ -1098,6 +1204,8 @@ std::optional<DaliAddressSettingsSnapshot> DaliDomainService::queryAddressSettin
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
markBusActivity(gateway_id);
|
||||
|
||||
DaliAddressSettingsSnapshot settings{};
|
||||
|
||||
if (const auto value = channel->dali->base.getPowerOnLevel(short_address); value.has_value()) {
|
||||
@@ -1130,8 +1238,11 @@ std::optional<DaliAddressSettingsSnapshot> DaliDomainService::queryAddressSettin
|
||||
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);
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return false;
|
||||
}
|
||||
markBusActivity(gateway_id);
|
||||
return channel->dali->base.setGroup(short_address, group_mask);
|
||||
}
|
||||
|
||||
bool DaliDomainService::applySceneLevel(uint8_t gateway_id, int short_address, int scene,
|
||||
@@ -1141,6 +1252,8 @@ bool DaliDomainService::applySceneLevel(uint8_t gateway_id, int short_address, i
|
||||
return false;
|
||||
}
|
||||
|
||||
markBusActivity(gateway_id);
|
||||
|
||||
if (*level == 255U) {
|
||||
return channel->dali->base.removeScene(short_address, scene);
|
||||
}
|
||||
@@ -1156,6 +1269,8 @@ bool DaliDomainService::applyAddressSettings(uint8_t gateway_id, int short_addre
|
||||
return false;
|
||||
}
|
||||
|
||||
markBusActivity(gateway_id);
|
||||
|
||||
bool ok = true;
|
||||
if (settings.power_on_level.has_value()) {
|
||||
ok = ok && channel->dali->base.setPowerOnLevel(short_address, *settings.power_on_level);
|
||||
@@ -1375,6 +1490,7 @@ void DaliDomainService::rawFrameTaskLoop() {
|
||||
}
|
||||
|
||||
void DaliDomainService::notifyRawFrameSinks(const DaliRawFrame& frame) {
|
||||
markBusActivity(frame.gateway_id);
|
||||
if (raw_frame_sink_lock_ != nullptr) {
|
||||
xSemaphoreTake(raw_frame_sink_lock_, portMAX_DELAY);
|
||||
}
|
||||
@@ -1387,6 +1503,16 @@ void DaliDomainService::notifyRawFrameSinks(const DaliRawFrame& frame) {
|
||||
}
|
||||
}
|
||||
|
||||
void DaliDomainService::markBusActivity(uint8_t gateway_id) const {
|
||||
if (bus_activity_lock_ != nullptr) {
|
||||
xSemaphoreTake(bus_activity_lock_, portMAX_DELAY);
|
||||
}
|
||||
last_bus_activity_ticks_[gateway_id] = xTaskGetTickCount();
|
||||
if (bus_activity_lock_ != nullptr) {
|
||||
xSemaphoreGive(bus_activity_lock_);
|
||||
}
|
||||
}
|
||||
|
||||
bool DaliDomainService::hasSerialPort(int uart_port) const {
|
||||
return std::any_of(channels_.begin(), channels_.end(), [uart_port](const auto& channel) {
|
||||
return channel->serial_bus.has_value() && channel->serial_bus->uart_port == uart_port;
|
||||
|
||||
@@ -24,7 +24,8 @@ struct GatewayCacheConfig {
|
||||
bool cache_enabled{true};
|
||||
bool reconciliation_enabled{true};
|
||||
bool full_state_mirror_enabled{false};
|
||||
uint32_t flush_interval_ms{5000};
|
||||
uint32_t flush_interval_ms{10000};
|
||||
uint32_t refresh_interval_ms{120000};
|
||||
uint32_t task_stack_size{4096};
|
||||
UBaseType_t task_priority{3};
|
||||
GatewayCachePriorityMode default_priority_mode{GatewayCachePriorityMode::kOutsideBusFirst};
|
||||
@@ -71,6 +72,7 @@ struct GatewayCacheDaliRuntimeStatus {
|
||||
std::optional<uint8_t> actual_level;
|
||||
std::optional<uint8_t> scene_id;
|
||||
bool use_min_level{false};
|
||||
bool stale{false};
|
||||
uint32_t revision{0};
|
||||
|
||||
bool anyKnown() const {
|
||||
@@ -144,6 +146,8 @@ class GatewayCache {
|
||||
std::optional<uint8_t> level);
|
||||
bool setDaliSettings(uint8_t gateway_id, uint8_t short_address,
|
||||
std::optional<GatewayCacheDaliSettingsSnapshot> settings);
|
||||
bool setDaliActualLevel(uint8_t gateway_id, uint8_t short_address,
|
||||
std::optional<uint8_t> level);
|
||||
bool clearChannelFlagsIfMatched(uint8_t gateway_id, const GatewayCacheChannelFlags& flags);
|
||||
void markGroupUpdateNeeded(uint8_t gateway_id, bool needed = true);
|
||||
void markSceneUpdateNeeded(uint8_t gateway_id, bool needed = true);
|
||||
@@ -174,6 +178,8 @@ class GatewayCache {
|
||||
void closeStorageLocked();
|
||||
bool persistSceneLocked(uint8_t gateway_id, uint8_t scene_id, const SceneEntry& scene);
|
||||
bool persistGroupLocked(uint8_t gateway_id, uint8_t group_id, const GroupEntry& group);
|
||||
bool persistDaliAddressStateLocked(uint8_t gateway_id, uint8_t short_address,
|
||||
const GatewayCacheDaliAddressState& state);
|
||||
bool commitStorageLocked();
|
||||
bool shouldTrackUpdateFlagsLocked() const;
|
||||
uint32_t nextDaliRuntimeRevisionLocked();
|
||||
@@ -205,6 +211,8 @@ class GatewayCache {
|
||||
GroupStore& ensureGroupStoreLocked(uint8_t gateway_id);
|
||||
void loadSceneStoreLocked(uint8_t gateway_id, SceneStore& scenes);
|
||||
void loadGroupStoreLocked(uint8_t gateway_id, GroupStore& groups);
|
||||
void loadDaliStateStoreLocked(uint8_t gateway_id,
|
||||
std::array<GatewayCacheDaliAddressState, 64>& states);
|
||||
std::string readStringLocked(std::string_view key);
|
||||
bool writeStringLocked(std::string_view key, std::string_view value);
|
||||
bool eraseKeyLocked(std::string_view key);
|
||||
|
||||
@@ -43,6 +43,18 @@ constexpr uint8_t kDaliCmdDt8StoreDtrAsColorX = 0xE0;
|
||||
constexpr uint8_t kDaliCmdDt8StoreDtrAsColorY = 0xE1;
|
||||
constexpr uint8_t kDaliCmdDt8StorePrimaryMin = 0xF0;
|
||||
constexpr uint8_t kDaliCmdDt8StartAutoCalibration = 0xF6;
|
||||
constexpr int kDaliStatePayloadVersion = 1;
|
||||
constexpr uint32_t kDaliStateGroupMaskKnown = 1U << 0;
|
||||
constexpr uint32_t kDaliStateActualKnown = 1U << 1;
|
||||
constexpr uint32_t kDaliStateSceneKnown = 1U << 2;
|
||||
constexpr uint32_t kDaliStateUseMinLevel = 1U << 3;
|
||||
constexpr uint32_t kDaliStateStatusStale = 1U << 4;
|
||||
constexpr uint32_t kDaliStatePowerOnKnown = 1U << 5;
|
||||
constexpr uint32_t kDaliStateSystemFailureKnown = 1U << 6;
|
||||
constexpr uint32_t kDaliStateMinKnown = 1U << 7;
|
||||
constexpr uint32_t kDaliStateMaxKnown = 1U << 8;
|
||||
constexpr uint32_t kDaliStateFadeTimeKnown = 1U << 9;
|
||||
constexpr uint32_t kDaliStateFadeRateKnown = 1U << 10;
|
||||
|
||||
class LockGuard {
|
||||
public:
|
||||
@@ -134,6 +146,18 @@ bool ShouldMirrorObservedMutation(GatewayCacheRawFrameOrigin origin,
|
||||
priority_mode == GatewayCachePriorityMode::kOutsideBusFirst;
|
||||
}
|
||||
|
||||
bool ShouldAlwaysMirrorObservedStatus(uint8_t raw_addr, uint8_t command) {
|
||||
if (!DecodeDaliTarget(raw_addr).has_value()) {
|
||||
return false;
|
||||
}
|
||||
if ((raw_addr & 0x01) == 0) {
|
||||
return command <= 254;
|
||||
}
|
||||
return command == kDaliCmdOff || command == kDaliCmdRecallMax ||
|
||||
command == kDaliCmdRecallMin ||
|
||||
(command >= kDaliCmdGoToSceneMin && command <= kDaliCmdGoToSceneMax);
|
||||
}
|
||||
|
||||
void ClearDaliState(GatewayCacheDaliAddressState& state) {
|
||||
state.group_mask_known = false;
|
||||
state.group_mask = 0;
|
||||
@@ -215,6 +239,139 @@ std::string BuildGroupPayload(const GatewayCache::GroupEntry& group) {
|
||||
return std::string(payload);
|
||||
}
|
||||
|
||||
uint8_t ByteValue(int value) {
|
||||
return static_cast<uint8_t>(std::clamp(value, 0, 255));
|
||||
}
|
||||
|
||||
uint16_t WordValue(int value) {
|
||||
return static_cast<uint16_t>(std::clamp(value, 0, 0xffff));
|
||||
}
|
||||
|
||||
uint16_t SceneKnownMask(const GatewayCacheDaliAddressState& state) {
|
||||
uint16_t mask = 0;
|
||||
for (size_t index = 0; index < state.scene_levels.size(); ++index) {
|
||||
if (state.scene_levels[index].has_value()) {
|
||||
mask |= static_cast<uint16_t>(1U << index);
|
||||
}
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
bool IsDefaultDaliAddressState(const GatewayCacheDaliAddressState& state) {
|
||||
return !state.group_mask_known && state.group_mask == 0 && SceneKnownMask(state) == 0 &&
|
||||
!state.settings.anyKnown() && !state.status.anyKnown();
|
||||
}
|
||||
|
||||
uint32_t DaliStateFlags(const GatewayCacheDaliAddressState& state) {
|
||||
uint32_t flags = 0;
|
||||
if (state.group_mask_known) {
|
||||
flags |= kDaliStateGroupMaskKnown;
|
||||
}
|
||||
if (state.status.actual_level.has_value()) {
|
||||
flags |= kDaliStateActualKnown;
|
||||
}
|
||||
if (state.status.scene_id.has_value()) {
|
||||
flags |= kDaliStateSceneKnown;
|
||||
}
|
||||
if (state.status.use_min_level) {
|
||||
flags |= kDaliStateUseMinLevel;
|
||||
}
|
||||
if (state.status.stale) {
|
||||
flags |= kDaliStateStatusStale;
|
||||
}
|
||||
if (state.settings.power_on_level.has_value()) {
|
||||
flags |= kDaliStatePowerOnKnown;
|
||||
}
|
||||
if (state.settings.system_failure_level.has_value()) {
|
||||
flags |= kDaliStateSystemFailureKnown;
|
||||
}
|
||||
if (state.settings.min_level.has_value()) {
|
||||
flags |= kDaliStateMinKnown;
|
||||
}
|
||||
if (state.settings.max_level.has_value()) {
|
||||
flags |= kDaliStateMaxKnown;
|
||||
}
|
||||
if (state.settings.fade_time.has_value()) {
|
||||
flags |= kDaliStateFadeTimeKnown;
|
||||
}
|
||||
if (state.settings.fade_rate.has_value()) {
|
||||
flags |= kDaliStateFadeRateKnown;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
std::string BuildDaliStatePayload(const GatewayCacheDaliAddressState& state) {
|
||||
const uint16_t scene_known_mask = SceneKnownMask(state);
|
||||
std::string payload = std::to_string(kDaliStatePayloadVersion);
|
||||
payload += "," + std::to_string(DaliStateFlags(state));
|
||||
payload += "," + std::to_string(state.status.revision);
|
||||
payload += "," + std::to_string(state.group_mask);
|
||||
payload += "," + std::to_string(state.status.actual_level.value_or(0));
|
||||
payload += "," + std::to_string(state.status.scene_id.value_or(0));
|
||||
payload += "," + std::to_string(state.settings.power_on_level.value_or(0));
|
||||
payload += "," + std::to_string(state.settings.system_failure_level.value_or(0));
|
||||
payload += "," + std::to_string(state.settings.min_level.value_or(0));
|
||||
payload += "," + std::to_string(state.settings.max_level.value_or(0));
|
||||
payload += "," + std::to_string(state.settings.fade_time.value_or(0));
|
||||
payload += "," + std::to_string(state.settings.fade_rate.value_or(0));
|
||||
payload += "," + std::to_string(scene_known_mask);
|
||||
for (const auto& scene_level : state.scene_levels) {
|
||||
payload += "," + std::to_string(scene_level.value_or(255));
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
void ApplyDaliStatePayload(std::string_view raw, GatewayCacheDaliAddressState& state) {
|
||||
const auto values = ParseCsv(raw);
|
||||
if (values.size() < 13 || values[0] != kDaliStatePayloadVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t flags = static_cast<uint32_t>(std::max(values[1], 0));
|
||||
state.group_mask_known = (flags & kDaliStateGroupMaskKnown) != 0;
|
||||
state.group_mask = state.group_mask_known ? WordValue(values[3]) : 0;
|
||||
|
||||
state.status = {};
|
||||
state.status.revision = static_cast<uint32_t>(std::max(values[2], 0));
|
||||
state.status.stale = (flags & kDaliStateStatusStale) != 0;
|
||||
state.status.use_min_level = (flags & kDaliStateUseMinLevel) != 0;
|
||||
if ((flags & kDaliStateActualKnown) != 0) {
|
||||
state.status.actual_level = ByteValue(values[4]);
|
||||
}
|
||||
if ((flags & kDaliStateSceneKnown) != 0) {
|
||||
state.status.scene_id = static_cast<uint8_t>(std::min<int>(ByteValue(values[5]), 15));
|
||||
}
|
||||
|
||||
state.settings = {};
|
||||
if ((flags & kDaliStatePowerOnKnown) != 0) {
|
||||
state.settings.power_on_level = ByteValue(values[6]);
|
||||
}
|
||||
if ((flags & kDaliStateSystemFailureKnown) != 0) {
|
||||
state.settings.system_failure_level = ByteValue(values[7]);
|
||||
}
|
||||
if ((flags & kDaliStateMinKnown) != 0) {
|
||||
state.settings.min_level = ByteValue(values[8]);
|
||||
}
|
||||
if ((flags & kDaliStateMaxKnown) != 0) {
|
||||
state.settings.max_level = ByteValue(values[9]);
|
||||
}
|
||||
if ((flags & kDaliStateFadeTimeKnown) != 0) {
|
||||
state.settings.fade_time = ByteValue(values[10]);
|
||||
}
|
||||
if ((flags & kDaliStateFadeRateKnown) != 0) {
|
||||
state.settings.fade_rate = ByteValue(values[11]);
|
||||
}
|
||||
|
||||
state.scene_levels.fill(std::nullopt);
|
||||
const uint16_t scene_known_mask = WordValue(values[12]);
|
||||
for (uint8_t scene_id = 0; scene_id < state.scene_levels.size(); ++scene_id) {
|
||||
const size_t value_index = 13 + scene_id;
|
||||
if ((scene_known_mask & (1U << scene_id)) != 0 && value_index < values.size()) {
|
||||
state.scene_levels[scene_id] = ByteValue(values[value_index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
GatewayCache::GatewayCache(GatewayCacheConfig config)
|
||||
@@ -269,8 +426,11 @@ esp_err_t GatewayCache::start() {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
ESP_LOGI(kTag, "cache started namespace=%s flush_interval_ms=%u reconciliation=%d full_mirror=%d",
|
||||
ESP_LOGI(kTag,
|
||||
"cache started namespace=%s flush_interval_ms=%u refresh_interval_ms=%u "
|
||||
"reconciliation=%d full_mirror=%d",
|
||||
config_.storage_namespace.c_str(), static_cast<unsigned>(config_.flush_interval_ms),
|
||||
static_cast<unsigned>(config_.refresh_interval_ms),
|
||||
config_.reconciliation_enabled, config_.full_state_mirror_enabled);
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -282,6 +442,10 @@ void GatewayCache::preloadChannel(uint8_t gateway_id) {
|
||||
}
|
||||
ensureSceneStoreLocked(gateway_id);
|
||||
ensureGroupStoreLocked(gateway_id);
|
||||
auto [it, inserted] = dali_states_.try_emplace(gateway_id);
|
||||
if (inserted) {
|
||||
loadDaliStateStoreLocked(gateway_id, it->second);
|
||||
}
|
||||
}
|
||||
|
||||
GatewayCache::SceneStore GatewayCache::scenes(uint8_t gateway_id) {
|
||||
@@ -620,6 +784,7 @@ bool GatewayCache::setDaliGroupMask(uint8_t gateway_id, uint8_t short_address,
|
||||
state.group_mask_known = group_mask.has_value();
|
||||
state.group_mask = group_mask.value_or(0);
|
||||
refreshDaliAddressAggregateStatusLocked(gateway_id, state);
|
||||
dirty_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -632,6 +797,7 @@ bool GatewayCache::setDaliSceneLevel(uint8_t gateway_id, uint8_t short_address,
|
||||
|
||||
auto& state = ensureDaliAddressStateLocked(gateway_id, short_address);
|
||||
state.scene_levels[scene_id] = level;
|
||||
dirty_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -644,6 +810,31 @@ bool GatewayCache::setDaliSettings(uint8_t gateway_id, uint8_t short_address,
|
||||
|
||||
auto& state = ensureDaliAddressStateLocked(gateway_id, short_address);
|
||||
state.settings = settings.value_or(GatewayCacheDaliSettingsSnapshot{});
|
||||
dirty_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GatewayCache::setDaliActualLevel(uint8_t gateway_id, uint8_t short_address,
|
||||
std::optional<uint8_t> level) {
|
||||
LockGuard guard(lock_);
|
||||
if (short_address >= 64) {
|
||||
return false;
|
||||
}
|
||||
|
||||
GatewayCacheDaliRuntimeStatus status;
|
||||
status.actual_level = level;
|
||||
status.revision = nextDaliRuntimeRevisionLocked();
|
||||
status.stale = false;
|
||||
auto& state = ensureDaliAddressStateLocked(gateway_id, short_address);
|
||||
state.status.scene_id.reset();
|
||||
state.status.use_min_level = false;
|
||||
applyDaliRuntimeStatusToAddressLocked(state, status);
|
||||
if (!level.has_value()) {
|
||||
state.status.actual_level.reset();
|
||||
state.status.revision = status.revision;
|
||||
state.status.stale = false;
|
||||
}
|
||||
dirty_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -712,7 +903,8 @@ bool GatewayCache::observeDaliCommand(uint8_t gateway_id, uint8_t raw_addr, uint
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ShouldMirrorObservedMutation(origin, priority_mode_)) {
|
||||
if (ShouldAlwaysMirrorObservedStatus(raw_addr, command) ||
|
||||
ShouldMirrorObservedMutation(origin, priority_mode_)) {
|
||||
mirrorDaliCommandLocked(gateway_id, raw_addr, command);
|
||||
}
|
||||
|
||||
@@ -791,12 +983,15 @@ bool GatewayCache::mirrorDaliCommandLocked(uint8_t gateway_id, uint8_t raw_addr,
|
||||
GatewayCacheDaliRuntimeStatus status;
|
||||
status.actual_level = command;
|
||||
status.revision = nextDaliRuntimeRevisionLocked();
|
||||
status.stale = false;
|
||||
applyDaliTargetRuntimeStatusLocked(gateway_id, *target, status);
|
||||
dirty_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (command == kDaliCmdReset) {
|
||||
clearDaliTargetStateLocked(gateway_id, *target, nextDaliRuntimeRevisionLocked());
|
||||
dirty_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -804,7 +999,9 @@ bool GatewayCache::mirrorDaliCommandLocked(uint8_t gateway_id, uint8_t raw_addr,
|
||||
GatewayCacheDaliRuntimeStatus status;
|
||||
status.actual_level = command == kDaliCmdOff ? 0 : 254;
|
||||
status.revision = nextDaliRuntimeRevisionLocked();
|
||||
status.stale = false;
|
||||
applyDaliTargetRuntimeStatusLocked(gateway_id, *target, status);
|
||||
dirty_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -812,7 +1009,9 @@ bool GatewayCache::mirrorDaliCommandLocked(uint8_t gateway_id, uint8_t raw_addr,
|
||||
GatewayCacheDaliRuntimeStatus status;
|
||||
status.use_min_level = true;
|
||||
status.revision = nextDaliRuntimeRevisionLocked();
|
||||
status.stale = false;
|
||||
applyDaliTargetRuntimeStatusLocked(gateway_id, *target, status);
|
||||
dirty_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -820,7 +1019,9 @@ bool GatewayCache::mirrorDaliCommandLocked(uint8_t gateway_id, uint8_t raw_addr,
|
||||
GatewayCacheDaliRuntimeStatus status;
|
||||
status.scene_id = static_cast<uint8_t>(command - kDaliCmdGoToSceneMin);
|
||||
status.revision = nextDaliRuntimeRevisionLocked();
|
||||
status.stale = false;
|
||||
applyDaliTargetRuntimeStatusLocked(gateway_id, *target, status);
|
||||
dirty_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -828,6 +1029,7 @@ bool GatewayCache::mirrorDaliCommandLocked(uint8_t gateway_id, uint8_t raw_addr,
|
||||
applyDaliTargetGroupMutationLocked(gateway_id, *target,
|
||||
static_cast<uint8_t>(command & 0x0F),
|
||||
command < (kDaliCmdAddToGroupMin + 16));
|
||||
dirty_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -836,6 +1038,7 @@ bool GatewayCache::mirrorDaliCommandLocked(uint8_t gateway_id, uint8_t raw_addr,
|
||||
applyDaliTargetSceneLevelLocked(gateway_id, *target,
|
||||
static_cast<uint8_t>(command - kDaliCmdSetSceneMin),
|
||||
*dtr_state.dtr0);
|
||||
dirty_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -843,12 +1046,14 @@ bool GatewayCache::mirrorDaliCommandLocked(uint8_t gateway_id, uint8_t raw_addr,
|
||||
applyDaliTargetSceneLevelLocked(
|
||||
gateway_id, *target, static_cast<uint8_t>(command - (kDaliCmdSetSceneMin + 16)),
|
||||
static_cast<uint8_t>(255U));
|
||||
dirty_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (command >= kDaliCmdStoreDtrAsMaxLevel && command <= kDaliCmdStoreDtrAsFadeRate &&
|
||||
dtr_state.dtr0.has_value()) {
|
||||
applyDaliTargetSettingsLocked(gateway_id, *target, command, *dtr_state.dtr0);
|
||||
dirty_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -977,6 +1182,7 @@ void GatewayCache::applyDaliRuntimeStatusToAddressLocked(
|
||||
}
|
||||
}
|
||||
state.status.revision = status.revision;
|
||||
state.status.stale = status.stale;
|
||||
}
|
||||
|
||||
void GatewayCache::applyDaliTargetGroupMutationLocked(uint8_t gateway_id,
|
||||
@@ -1127,7 +1333,9 @@ void GatewayCache::refreshDaliAddressAggregateStatusLocked(uint8_t gateway_id,
|
||||
|
||||
if (const auto broadcast = dali_broadcast_status_.find(gateway_id);
|
||||
broadcast != dali_broadcast_status_.end()) {
|
||||
applyDaliRuntimeStatusToAddressLocked(state, broadcast->second);
|
||||
if (!broadcast->second.stale) {
|
||||
applyDaliRuntimeStatusToAddressLocked(state, broadcast->second);
|
||||
}
|
||||
}
|
||||
|
||||
const auto groups = dali_group_status_.find(gateway_id);
|
||||
@@ -1137,6 +1345,9 @@ void GatewayCache::refreshDaliAddressAggregateStatusLocked(uint8_t gateway_id,
|
||||
for (uint8_t group_id = 0; group_id < groups->second.size(); ++group_id) {
|
||||
const uint16_t bit = static_cast<uint16_t>(1U << group_id);
|
||||
if ((state.group_mask & bit) != 0) {
|
||||
if (groups->second[group_id].stale) {
|
||||
continue;
|
||||
}
|
||||
applyDaliRuntimeStatusToAddressLocked(state, groups->second[group_id]);
|
||||
}
|
||||
}
|
||||
@@ -1209,6 +1420,14 @@ bool GatewayCache::flushDirty() {
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& [gateway_id, states] : dali_states_) {
|
||||
for (uint8_t short_address = 0; short_address < states.size(); ++short_address) {
|
||||
if (!persistDaliAddressStateLocked(gateway_id, short_address, states[short_address])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const esp_err_t commit_err = nvs_commit(storage_);
|
||||
if (commit_err != ESP_OK) {
|
||||
ESP_LOGE(kTag, "cache commit failed: %s", esp_err_to_name(commit_err));
|
||||
@@ -1288,6 +1507,18 @@ bool GatewayCache::persistGroupLocked(uint8_t gateway_id, uint8_t group_id,
|
||||
return commitStorageLocked();
|
||||
}
|
||||
|
||||
bool GatewayCache::persistDaliAddressStateLocked(
|
||||
uint8_t gateway_id, uint8_t short_address, const GatewayCacheDaliAddressState& state) {
|
||||
if (short_address >= 64) {
|
||||
return false;
|
||||
}
|
||||
if (!IsDefaultDaliAddressState(state)) {
|
||||
return writeStringLocked(ShortKey("ds", gateway_id, short_address),
|
||||
BuildDaliStatePayload(state));
|
||||
}
|
||||
return eraseKeyLocked(ShortKey("ds", gateway_id, short_address));
|
||||
}
|
||||
|
||||
bool GatewayCache::commitStorageLocked() {
|
||||
if (storage_ == 0) {
|
||||
return false;
|
||||
@@ -1307,7 +1538,9 @@ bool GatewayCache::shouldTrackUpdateFlagsLocked() const {
|
||||
GatewayCacheDaliAddressState& GatewayCache::ensureDaliAddressStateLocked(uint8_t gateway_id,
|
||||
uint8_t short_address) {
|
||||
auto [it, inserted] = dali_states_.try_emplace(gateway_id);
|
||||
(void)inserted;
|
||||
if (inserted) {
|
||||
loadDaliStateStoreLocked(gateway_id, it->second);
|
||||
}
|
||||
return it->second[short_address];
|
||||
}
|
||||
|
||||
@@ -1377,6 +1610,23 @@ void GatewayCache::loadGroupStoreLocked(uint8_t gateway_id, GroupStore& groups)
|
||||
}
|
||||
}
|
||||
|
||||
void GatewayCache::loadDaliStateStoreLocked(
|
||||
uint8_t gateway_id, std::array<GatewayCacheDaliAddressState, 64>& states) {
|
||||
for (uint8_t short_address = 0; short_address < states.size(); ++short_address) {
|
||||
ClearDaliState(states[short_address]);
|
||||
const auto raw = readStringLocked(ShortKey("ds", gateway_id, short_address));
|
||||
if (!raw.empty()) {
|
||||
ApplyDaliStatePayload(raw, states[short_address]);
|
||||
if (states[short_address].status.anyKnown()) {
|
||||
states[short_address].status.stale = true;
|
||||
}
|
||||
if (states[short_address].status.revision > dali_runtime_revision_) {
|
||||
dali_runtime_revision_ = states[short_address].status.revision;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string GatewayCache::readStringLocked(std::string_view key) {
|
||||
if (!openStorageLocked()) {
|
||||
return {};
|
||||
|
||||
@@ -33,6 +33,9 @@ struct GatewayControllerConfig {
|
||||
bool ip_router_supported{true};
|
||||
bool internal_scene_supported{true};
|
||||
bool internal_group_supported{true};
|
||||
bool cache_supported{true};
|
||||
uint32_t cache_refresh_interval_ms{120000};
|
||||
uint32_t cache_refresh_idle_ms{100};
|
||||
};
|
||||
|
||||
struct GatewayChannelSnapshot {
|
||||
@@ -109,13 +112,20 @@ class GatewayController {
|
||||
std::map<uint8_t, std::vector<uint8_t>> chunks;
|
||||
};
|
||||
|
||||
struct CacheRefreshJob {
|
||||
TickType_t next_due_tick{0};
|
||||
uint8_t short_address{0};
|
||||
};
|
||||
|
||||
static void TaskEntry(void* arg);
|
||||
void taskLoop();
|
||||
void dispatchCommand(const std::vector<uint8_t>& command);
|
||||
void scheduleReconciliation(uint8_t gateway_id);
|
||||
bool hasPendingReconciliation() const;
|
||||
bool cacheRefreshEnabled() const;
|
||||
bool runMaintenanceStep();
|
||||
bool runReconciliationStep(uint8_t gateway_id, ReconciliationJob& job);
|
||||
bool runCacheRefreshStep();
|
||||
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);
|
||||
@@ -165,6 +175,7 @@ class GatewayController {
|
||||
void handleAllocationCommand(uint8_t gateway_id, const std::vector<uint8_t>& command);
|
||||
void handleInternalSceneCommand(uint8_t gateway_id, const std::vector<uint8_t>& command);
|
||||
void handleInternalGroupCommand(uint8_t gateway_id, const std::vector<uint8_t>& command);
|
||||
void handleGatewayCacheCommand(uint8_t gateway_id, const std::vector<uint8_t>& command);
|
||||
void handleBridgeTransportCommand(uint8_t gateway_id, const std::vector<uint8_t>& command);
|
||||
void publishBridgeTransportResponse(uint8_t gateway_id, uint8_t version, uint8_t sequence,
|
||||
std::string_view response);
|
||||
@@ -182,6 +193,7 @@ class GatewayController {
|
||||
std::vector<GatewayNameSink> gateway_name_sinks_;
|
||||
std::map<uint16_t, BridgeTransportRequestState> bridge_transport_requests_;
|
||||
std::map<uint8_t, ReconciliationJob> reconciliation_jobs_;
|
||||
std::map<uint8_t, CacheRefreshJob> cache_refresh_jobs_;
|
||||
std::atomic<int> maintenance_activity_gateway_{-1};
|
||||
bool setup_mode_{false};
|
||||
bool wireless_setup_mode_{false};
|
||||
|
||||
@@ -22,10 +22,32 @@ constexpr uint8_t kDaliSceneCount = 16;
|
||||
constexpr uint8_t kDaliCmdOff = 0x00;
|
||||
constexpr uint8_t kDaliCmdRecallMax = 0x05;
|
||||
constexpr TickType_t kMaintenancePollTicks = pdMS_TO_TICKS(20);
|
||||
constexpr TickType_t kIdleMaintenancePollTicks = pdMS_TO_TICKS(1000);
|
||||
constexpr uint8_t kBridgeTransportRequestOpcode = 0xB0;
|
||||
constexpr uint8_t kBridgeTransportResponseOpcode = 0xB1;
|
||||
constexpr uint8_t kBridgeTransportVersion = 1;
|
||||
constexpr size_t kBridgeTransportMaxChunkBytes = 120;
|
||||
constexpr uint8_t kGatewayFeatureCache = 0x40;
|
||||
constexpr uint8_t kGatewayCacheOpcode = 0x39;
|
||||
constexpr uint8_t kGatewayCacheProtocolVersion = 1;
|
||||
constexpr uint8_t kGatewayCacheOpSummary = 0x00;
|
||||
constexpr uint8_t kGatewayCacheOpShortAddress = 0x01;
|
||||
constexpr uint8_t kGatewayCacheOpGroup = 0x02;
|
||||
constexpr uint8_t kGatewayCacheOpBroadcast = 0x03;
|
||||
constexpr uint8_t kGatewayCacheStatusOk = 0x00;
|
||||
constexpr uint8_t kGatewayCacheStatusDisabled = 0x01;
|
||||
constexpr uint8_t kGatewayCacheStatusInvalidArgument = 0x02;
|
||||
constexpr uint16_t kCacheFlagActualKnown = 1U << 0;
|
||||
constexpr uint16_t kCacheFlagSceneKnown = 1U << 1;
|
||||
constexpr uint16_t kCacheFlagUseMinLevel = 1U << 2;
|
||||
constexpr uint16_t kCacheFlagStatusStale = 1U << 3;
|
||||
constexpr uint16_t kCacheFlagGroupMaskKnown = 1U << 4;
|
||||
constexpr uint16_t kCacheFlagPowerOnKnown = 1U << 5;
|
||||
constexpr uint16_t kCacheFlagSystemFailureKnown = 1U << 6;
|
||||
constexpr uint16_t kCacheFlagMinKnown = 1U << 7;
|
||||
constexpr uint16_t kCacheFlagMaxKnown = 1U << 8;
|
||||
constexpr uint16_t kCacheFlagFadeTimeKnown = 1U << 9;
|
||||
constexpr uint16_t kCacheFlagFadeRateKnown = 1U << 10;
|
||||
constexpr const char* kBridgeTransportInvalidFrameResponse =
|
||||
"{\"statusCode\":400,\"error\":\"invalid bridge transport frame\","
|
||||
"\"message\":\"invalid bridge transport frame\"}";
|
||||
@@ -84,6 +106,75 @@ void AppendStringBytes(std::vector<uint8_t>& out, std::string_view value) {
|
||||
}
|
||||
}
|
||||
|
||||
void AppendLe16(std::vector<uint8_t>& out, uint16_t value) {
|
||||
out.push_back(static_cast<uint8_t>(value & 0xFF));
|
||||
out.push_back(static_cast<uint8_t>((value >> 8) & 0xFF));
|
||||
}
|
||||
|
||||
void AppendLe32(std::vector<uint8_t>& out, uint32_t value) {
|
||||
out.push_back(static_cast<uint8_t>(value & 0xFF));
|
||||
out.push_back(static_cast<uint8_t>((value >> 8) & 0xFF));
|
||||
out.push_back(static_cast<uint8_t>((value >> 16) & 0xFF));
|
||||
out.push_back(static_cast<uint8_t>((value >> 24) & 0xFF));
|
||||
}
|
||||
|
||||
uint8_t CacheByte(std::optional<uint8_t> value) {
|
||||
return value.value_or(0xFF);
|
||||
}
|
||||
|
||||
uint16_t CacheRuntimeFlags(const GatewayCacheDaliRuntimeStatus& status) {
|
||||
uint16_t flags = 0;
|
||||
if (status.actual_level.has_value()) {
|
||||
flags |= kCacheFlagActualKnown;
|
||||
}
|
||||
if (status.scene_id.has_value()) {
|
||||
flags |= kCacheFlagSceneKnown;
|
||||
}
|
||||
if (status.use_min_level) {
|
||||
flags |= kCacheFlagUseMinLevel;
|
||||
}
|
||||
if (status.stale) {
|
||||
flags |= kCacheFlagStatusStale;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
uint16_t CacheAddressFlags(const GatewayCacheDaliAddressState& state) {
|
||||
uint16_t flags = CacheRuntimeFlags(state.status);
|
||||
if (state.group_mask_known) {
|
||||
flags |= kCacheFlagGroupMaskKnown;
|
||||
}
|
||||
if (state.settings.power_on_level.has_value()) {
|
||||
flags |= kCacheFlagPowerOnKnown;
|
||||
}
|
||||
if (state.settings.system_failure_level.has_value()) {
|
||||
flags |= kCacheFlagSystemFailureKnown;
|
||||
}
|
||||
if (state.settings.min_level.has_value()) {
|
||||
flags |= kCacheFlagMinKnown;
|
||||
}
|
||||
if (state.settings.max_level.has_value()) {
|
||||
flags |= kCacheFlagMaxKnown;
|
||||
}
|
||||
if (state.settings.fade_time.has_value()) {
|
||||
flags |= kCacheFlagFadeTimeKnown;
|
||||
}
|
||||
if (state.settings.fade_rate.has_value()) {
|
||||
flags |= kCacheFlagFadeRateKnown;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
uint16_t CacheSceneKnownMask(const GatewayCacheDaliAddressState& state) {
|
||||
uint16_t mask = 0;
|
||||
for (size_t index = 0; index < state.scene_levels.size(); ++index) {
|
||||
if (state.scene_levels[index].has_value()) {
|
||||
mask |= static_cast<uint16_t>(1U << index);
|
||||
}
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
uint16_t BridgeTransportRequestKey(uint8_t gateway_id, uint8_t sequence) {
|
||||
return static_cast<uint16_t>((static_cast<uint16_t>(gateway_id) << 8) | sequence);
|
||||
}
|
||||
@@ -275,7 +366,11 @@ void GatewayController::taskLoop() {
|
||||
worked = runMaintenanceStep();
|
||||
}
|
||||
if (!worked) {
|
||||
ulTaskNotifyTake(pdTRUE, hasPendingReconciliation() ? kMaintenancePollTicks : portMAX_DELAY);
|
||||
const TickType_t wait_ticks = hasPendingReconciliation()
|
||||
? kMaintenancePollTicks
|
||||
: cacheRefreshEnabled() ? kIdleMaintenancePollTicks
|
||||
: portMAX_DELAY;
|
||||
ulTaskNotifyTake(pdTRUE, wait_ticks);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,42 +403,50 @@ bool GatewayController::hasPendingReconciliation() const {
|
||||
return !reconciliation_jobs_.empty();
|
||||
}
|
||||
|
||||
bool GatewayController::cacheRefreshEnabled() const {
|
||||
return config_.cache_supported && cache_.cacheEnabled() &&
|
||||
config_.cache_refresh_interval_ms > 0;
|
||||
}
|
||||
|
||||
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;
|
||||
if (cache_.reconciliationEnabled()) {
|
||||
bool has_job = false;
|
||||
uint8_t gateway_id = 0;
|
||||
ReconciliationJob job;
|
||||
{
|
||||
LockGuard guard(maintenance_lock_);
|
||||
if (!reconciliation_jobs_.empty()) {
|
||||
const auto it = reconciliation_jobs_.begin();
|
||||
has_job = true;
|
||||
gateway_id = it->first;
|
||||
job = it->second;
|
||||
}
|
||||
}
|
||||
const auto it = reconciliation_jobs_.begin();
|
||||
gateway_id = it->first;
|
||||
job = it->second;
|
||||
}
|
||||
|
||||
if (runtime_.shouldYieldMaintenance(gateway_id)) {
|
||||
return false;
|
||||
}
|
||||
if (has_job) {
|
||||
if (runtime_.shouldYieldMaintenance(gateway_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool keep_job = runReconciliationStep(gateway_id, job);
|
||||
const bool keep_job = runReconciliationStep(gateway_id, job);
|
||||
|
||||
{
|
||||
LockGuard guard(maintenance_lock_);
|
||||
auto it = reconciliation_jobs_.find(gateway_id);
|
||||
if (it == reconciliation_jobs_.end()) {
|
||||
{
|
||||
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;
|
||||
}
|
||||
if (keep_job) {
|
||||
it->second = job;
|
||||
} else {
|
||||
reconciliation_jobs_.erase(it);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
return runCacheRefreshStep();
|
||||
}
|
||||
|
||||
bool GatewayController::runReconciliationStep(uint8_t gateway_id, ReconciliationJob& job) {
|
||||
@@ -418,6 +521,49 @@ bool GatewayController::runReconciliationStep(uint8_t gateway_id, Reconciliation
|
||||
}
|
||||
}
|
||||
|
||||
bool GatewayController::runCacheRefreshStep() {
|
||||
if (!cacheRefreshEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const TickType_t interval_ticks =
|
||||
std::max<TickType_t>(1, pdMS_TO_TICKS(config_.cache_refresh_interval_ms));
|
||||
const auto channels = dali_domain_.channelInfo();
|
||||
const TickType_t now = xTaskGetTickCount();
|
||||
|
||||
for (const auto& channel : channels) {
|
||||
auto& job = cache_refresh_jobs_[channel.gateway_id];
|
||||
if (job.next_due_tick == 0) {
|
||||
job.next_due_tick = now + interval_ticks;
|
||||
continue;
|
||||
}
|
||||
if (now < job.next_due_tick) {
|
||||
continue;
|
||||
}
|
||||
if (runtime_.shouldYieldMaintenance(channel.gateway_id) ||
|
||||
dali_domain_.isAllocAddr(channel.gateway_id) ||
|
||||
!dali_domain_.isBusIdle(channel.gateway_id, config_.cache_refresh_idle_ms)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
maintenance_activity_gateway_.store(channel.gateway_id);
|
||||
const auto actual_level = dali_domain_.queryActualLevel(channel.gateway_id, job.short_address);
|
||||
maintenance_activity_gateway_.store(-1);
|
||||
cache_.setDaliActualLevel(channel.gateway_id, job.short_address, actual_level);
|
||||
|
||||
++job.short_address;
|
||||
if (job.short_address >= kDaliShortAddressCount) {
|
||||
job.short_address = 0;
|
||||
job.next_due_tick = xTaskGetTickCount() + interval_ticks;
|
||||
} else {
|
||||
job.next_due_tick = xTaskGetTickCount();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -574,6 +720,9 @@ void GatewayController::dispatchCommand(const std::vector<uint8_t>& command) {
|
||||
if (config_.internal_group_supported) {
|
||||
feature |= 0x20;
|
||||
}
|
||||
if (config_.cache_supported) {
|
||||
feature |= kGatewayFeatureCache;
|
||||
}
|
||||
publishPayload(gateway_id, {0x03, gateway_id, feature});
|
||||
break;
|
||||
}
|
||||
@@ -672,6 +821,9 @@ void GatewayController::dispatchCommand(const std::vector<uint8_t>& command) {
|
||||
}
|
||||
}
|
||||
break;
|
||||
case kGatewayCacheOpcode:
|
||||
handleGatewayCacheCommand(gateway_id, command);
|
||||
break;
|
||||
case 0xA0:
|
||||
handleInternalSceneCommand(gateway_id, command);
|
||||
break;
|
||||
@@ -1360,4 +1512,90 @@ void GatewayController::handleInternalGroupCommand(uint8_t gateway_id,
|
||||
}
|
||||
}
|
||||
|
||||
void GatewayController::handleGatewayCacheCommand(uint8_t gateway_id,
|
||||
const std::vector<uint8_t>& command) {
|
||||
const uint8_t op = command.size() > 4 ? command[4] : kGatewayCacheOpSummary;
|
||||
const uint8_t arg = command.size() > 5 ? command[5] : 0;
|
||||
const bool enabled = config_.cache_supported && cache_.cacheEnabled();
|
||||
|
||||
if (op == kGatewayCacheOpSummary) {
|
||||
const uint8_t flags = static_cast<uint8_t>((config_.cache_supported ? 0x01 : 0x00) |
|
||||
(cache_.cacheEnabled() ? 0x02 : 0x00) |
|
||||
(cacheRefreshEnabled() ? 0x04 : 0x00));
|
||||
std::vector<uint8_t> payload{kGatewayCacheOpcode,
|
||||
gateway_id,
|
||||
op,
|
||||
kGatewayCacheStatusOk,
|
||||
kGatewayCacheProtocolVersion,
|
||||
flags};
|
||||
AppendLe16(payload, static_cast<uint16_t>(std::min<uint32_t>(
|
||||
config_.cache_refresh_interval_ms / 1000U, 0xffffU)));
|
||||
publishPayload(gateway_id, payload);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
publishPayload(gateway_id, {kGatewayCacheOpcode, gateway_id, op,
|
||||
kGatewayCacheStatusDisabled, arg});
|
||||
return;
|
||||
}
|
||||
|
||||
if (op == kGatewayCacheOpShortAddress) {
|
||||
if (arg >= kDaliShortAddressCount) {
|
||||
publishPayload(gateway_id, {kGatewayCacheOpcode, gateway_id, op,
|
||||
kGatewayCacheStatusInvalidArgument, arg});
|
||||
return;
|
||||
}
|
||||
const auto state = cache_.daliAddressState(gateway_id, arg);
|
||||
std::vector<uint8_t> payload{kGatewayCacheOpcode, gateway_id, op, kGatewayCacheStatusOk, arg};
|
||||
AppendLe16(payload, CacheAddressFlags(state));
|
||||
payload.push_back(CacheByte(state.status.actual_level));
|
||||
payload.push_back(CacheByte(state.status.scene_id));
|
||||
AppendLe16(payload, state.group_mask_known ? state.group_mask : 0);
|
||||
payload.push_back(CacheByte(state.settings.power_on_level));
|
||||
payload.push_back(CacheByte(state.settings.system_failure_level));
|
||||
payload.push_back(CacheByte(state.settings.min_level));
|
||||
payload.push_back(CacheByte(state.settings.max_level));
|
||||
payload.push_back(CacheByte(state.settings.fade_time));
|
||||
payload.push_back(CacheByte(state.settings.fade_rate));
|
||||
AppendLe32(payload, state.status.revision);
|
||||
AppendLe16(payload, CacheSceneKnownMask(state));
|
||||
for (const auto& level : state.scene_levels) {
|
||||
payload.push_back(CacheByte(level));
|
||||
}
|
||||
publishPayload(gateway_id, payload);
|
||||
return;
|
||||
}
|
||||
|
||||
if (op == kGatewayCacheOpGroup) {
|
||||
if (arg >= kDaliSceneCount) {
|
||||
publishPayload(gateway_id, {kGatewayCacheOpcode, gateway_id, op,
|
||||
kGatewayCacheStatusInvalidArgument, arg});
|
||||
return;
|
||||
}
|
||||
const auto status = cache_.daliGroupStatus(gateway_id, arg);
|
||||
std::vector<uint8_t> payload{kGatewayCacheOpcode, gateway_id, op, kGatewayCacheStatusOk, arg};
|
||||
AppendLe16(payload, CacheRuntimeFlags(status));
|
||||
payload.push_back(CacheByte(status.actual_level));
|
||||
payload.push_back(CacheByte(status.scene_id));
|
||||
AppendLe32(payload, status.revision);
|
||||
publishPayload(gateway_id, payload);
|
||||
return;
|
||||
}
|
||||
|
||||
if (op == kGatewayCacheOpBroadcast) {
|
||||
const auto status = cache_.daliBroadcastStatus(gateway_id);
|
||||
std::vector<uint8_t> payload{kGatewayCacheOpcode, gateway_id, op, kGatewayCacheStatusOk, 0};
|
||||
AppendLe16(payload, CacheRuntimeFlags(status));
|
||||
payload.push_back(CacheByte(status.actual_level));
|
||||
payload.push_back(CacheByte(status.scene_id));
|
||||
AppendLe32(payload, status.revision);
|
||||
publishPayload(gateway_id, payload);
|
||||
return;
|
||||
}
|
||||
|
||||
publishPayload(gateway_id, {kGatewayCacheOpcode, gateway_id, op,
|
||||
kGatewayCacheStatusInvalidArgument, arg});
|
||||
}
|
||||
|
||||
} // namespace gateway
|
||||
|
||||
Reference in New Issue
Block a user