feat(gateway): enhance DALI host activity tracking and presence management

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-26 22:21:36 +08:00
parent f922993d2f
commit 865bf8425a
9 changed files with 441 additions and 39 deletions
@@ -5,6 +5,7 @@
#include <cstdint>
#include <functional>
#include <map>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
@@ -36,6 +37,8 @@ struct GatewayControllerConfig {
bool cache_supported{true};
uint32_t cache_refresh_interval_ms{120000};
uint32_t cache_refresh_idle_ms{100};
uint32_t cache_host_snooze_ms{5000};
uint32_t cache_host_echo_ms{250};
};
struct GatewayChannelSnapshot {
@@ -101,7 +104,9 @@ class GatewayController {
GatewayCacheChannelFlags flags{};
Phase phase{Phase::kReloadFlags};
uint8_t short_address{0};
std::optional<GatewayCacheDaliTarget> target;
std::vector<uint8_t> addresses;
size_t address_index{0};
uint8_t scene_id{0};
};
@@ -120,9 +125,11 @@ class GatewayController {
static void TaskEntry(void* arg);
void taskLoop();
void dispatchCommand(const std::vector<uint8_t>& command);
void scheduleReconciliation(uint8_t gateway_id);
void scheduleReconciliation(uint8_t gateway_id,
std::optional<GatewayCacheDaliTarget> target = std::nullopt);
bool hasPendingReconciliation() const;
bool cacheRefreshEnabled() const;
bool cacheMaintenanceSnoozed(uint8_t gateway_id) const;
bool runMaintenanceStep();
bool runReconciliationStep(uint8_t gateway_id, ReconciliationJob& job);
bool runCacheRefreshStep();
@@ -9,6 +9,7 @@
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <optional>
#include <utility>
namespace gateway {
@@ -77,6 +78,43 @@ bool AnyFlagSet(const GatewayCacheChannelFlags& flags) {
return flags.need_update_group || flags.need_update_scene || flags.need_update_settings;
}
bool SameTarget(const std::optional<GatewayCacheDaliTarget>& lhs,
const std::optional<GatewayCacheDaliTarget>& rhs) {
if (lhs.has_value() != rhs.has_value()) {
return false;
}
if (!lhs.has_value()) {
return true;
}
return lhs->kind == rhs->kind && lhs->value == rhs->value;
}
bool IsDaliHostCommandOpcode(uint8_t opcode) {
switch (opcode) {
case 0x07:
case 0x08:
case 0x10:
case 0x11:
case 0x12:
case 0x13:
case 0x14:
case 0x15:
case 0x16:
case 0x17:
case 0x18:
case 0x30:
case 0x32:
case 0x37:
case 0x38:
case 0xA0:
case 0xA2:
case kBridgeTransportRequestOpcode:
return true;
default:
return false;
}
}
std::string NormalizeName(std::string_view name) {
std::string normalized(name);
if (normalized.size() > kMaxNameBytes) {
@@ -375,7 +413,8 @@ void GatewayController::taskLoop() {
}
}
void GatewayController::scheduleReconciliation(uint8_t gateway_id) {
void GatewayController::scheduleReconciliation(uint8_t gateway_id,
std::optional<GatewayCacheDaliTarget> target) {
if (!cache_.reconciliationEnabled()) {
return;
}
@@ -390,7 +429,16 @@ void GatewayController::scheduleReconciliation(uint8_t gateway_id) {
{
LockGuard guard(maintenance_lock_);
reconciliation_jobs_.try_emplace(gateway_id);
auto [it, inserted] = reconciliation_jobs_.try_emplace(gateway_id);
if (inserted) {
it->second.target = target;
} else if (!SameTarget(it->second.target, target)) {
it->second.target.reset();
it->second.phase = ReconciliationJob::Phase::kReloadFlags;
it->second.addresses.clear();
it->second.address_index = 0;
it->second.scene_id = 0;
}
}
if (task_handle_ != nullptr) {
@@ -408,6 +456,11 @@ bool GatewayController::cacheRefreshEnabled() const {
config_.cache_refresh_interval_ms > 0;
}
bool GatewayController::cacheMaintenanceSnoozed(uint8_t gateway_id) const {
return config_.cache_host_snooze_ms > 0 &&
dali_domain_.hasRecentHostActivity(gateway_id, config_.cache_host_snooze_ms);
}
bool GatewayController::runMaintenanceStep() {
if (cache_.reconciliationEnabled()) {
bool has_job = false;
@@ -427,6 +480,9 @@ bool GatewayController::runMaintenanceStep() {
if (runtime_.shouldYieldMaintenance(gateway_id)) {
return false;
}
if (cacheMaintenanceSnoozed(gateway_id)) {
return false;
}
const bool keep_job = runReconciliationStep(gateway_id, job);
@@ -459,8 +515,13 @@ bool GatewayController::runReconciliationStep(uint8_t gateway_id, Reconciliation
return false;
}
job.short_address = 0;
job.addresses = cache_.reconciliationAddresses(gateway_id, job.target);
job.address_index = 0;
job.scene_id = 0;
if (job.addresses.empty()) {
cache_.clearChannelFlagsIfMatched(gateway_id, job.flags);
return false;
}
if (job.flags.need_update_group) {
job.phase = ReconciliationJob::Phase::kGroups;
} else if (job.flags.need_update_scene) {
@@ -472,9 +533,9 @@ bool GatewayController::runReconciliationStep(uint8_t gateway_id, Reconciliation
switch (job.phase) {
case ReconciliationJob::Phase::kGroups:
reconcileGroupStep(gateway_id, job.short_address++);
if (job.short_address >= kDaliShortAddressCount) {
job.short_address = 0;
reconcileGroupStep(gateway_id, job.addresses[job.address_index++]);
if (job.address_index >= job.addresses.size()) {
job.address_index = 0;
if (job.flags.need_update_scene) {
job.phase = ReconciliationJob::Phase::kScenes;
} else if (job.flags.need_update_settings) {
@@ -486,15 +547,22 @@ bool GatewayController::runReconciliationStep(uint8_t gateway_id, Reconciliation
}
}
return true;
case ReconciliationJob::Phase::kScenes:
reconcileSceneStep(gateway_id, job.short_address, job.scene_id);
++job.scene_id;
case ReconciliationJob::Phase::kScenes: {
const uint8_t short_address = job.addresses[job.address_index];
if (cache_.daliAddressPresence(gateway_id, short_address) ==
GatewayCacheDaliPresence::kOffline) {
job.scene_id = 0;
++job.address_index;
} else {
reconcileSceneStep(gateway_id, short_address, job.scene_id);
++job.scene_id;
}
if (job.scene_id >= kDaliSceneCount) {
job.scene_id = 0;
++job.short_address;
++job.address_index;
}
if (job.short_address >= kDaliShortAddressCount) {
job.short_address = 0;
if (job.address_index >= job.addresses.size()) {
job.address_index = 0;
if (job.flags.need_update_settings) {
job.phase = ReconciliationJob::Phase::kSettings;
} else if (!cache_.clearChannelFlagsIfMatched(gateway_id, job.flags)) {
@@ -504,10 +572,15 @@ bool GatewayController::runReconciliationStep(uint8_t gateway_id, Reconciliation
}
}
return true;
case ReconciliationJob::Phase::kSettings:
reconcileSettingsStep(gateway_id, job.short_address++);
if (job.short_address >= kDaliShortAddressCount) {
job.short_address = 0;
}
case ReconciliationJob::Phase::kSettings: {
const uint8_t short_address = job.addresses[job.address_index++];
if (cache_.daliAddressPresence(gateway_id, short_address) !=
GatewayCacheDaliPresence::kOffline) {
reconcileSettingsStep(gateway_id, short_address);
}
if (job.address_index >= job.addresses.size()) {
job.address_index = 0;
if (!cache_.clearChannelFlagsIfMatched(gateway_id, job.flags)) {
job.phase = ReconciliationJob::Phase::kReloadFlags;
} else {
@@ -515,6 +588,7 @@ bool GatewayController::runReconciliationStep(uint8_t gateway_id, Reconciliation
}
}
return true;
}
case ReconciliationJob::Phase::kReloadFlags:
default:
return true;
@@ -542,22 +616,36 @@ bool GatewayController::runCacheRefreshStep() {
}
if (runtime_.shouldYieldMaintenance(channel.gateway_id) ||
dali_domain_.isAllocAddr(channel.gateway_id) ||
cacheMaintenanceSnoozed(channel.gateway_id) ||
!dali_domain_.isBusIdle(channel.gateway_id, config_.cache_refresh_idle_ms)) {
continue;
}
auto advance_job = [&]() {
++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();
}
};
if (cache_.daliAddressPresence(channel.gateway_id, job.short_address) ==
GatewayCacheDaliPresence::kOffline) {
advance_job();
return true;
}
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_.markDaliAddressPresence(channel.gateway_id, job.short_address,
actual_level.has_value()
? GatewayCacheDaliPresence::kOnline
: GatewayCacheDaliPresence::kOffline);
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();
}
advance_job();
return true;
}
@@ -573,6 +661,10 @@ void GatewayController::reconcileGroupStep(uint8_t gateway_id, uint8_t short_add
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_.markDaliAddressPresence(gateway_id, short_address,
verified_mask.has_value()
? GatewayCacheDaliPresence::kOnline
: GatewayCacheDaliPresence::kOffline);
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);
@@ -580,8 +672,11 @@ void GatewayController::reconcileGroupStep(uint8_t gateway_id, uint8_t short_add
return;
}
cache_.setDaliGroupMask(gateway_id, short_address,
dali_domain_.queryGroupMask(gateway_id, short_address));
const auto group_mask = dali_domain_.queryGroupMask(gateway_id, short_address);
cache_.markDaliAddressPresence(gateway_id, short_address,
group_mask.has_value() ? GatewayCacheDaliPresence::kOnline
: GatewayCacheDaliPresence::kOffline);
cache_.setDaliGroupMask(gateway_id, short_address, group_mask);
}
void GatewayController::reconcileSceneStep(uint8_t gateway_id, uint8_t short_address,
@@ -596,8 +691,11 @@ void GatewayController::reconcileSceneStep(uint8_t gateway_id, uint8_t short_add
maintenance_activity_gateway_.store(-1);
}
cache_.setDaliSceneLevel(gateway_id, short_address, scene_id,
dali_domain_.querySceneLevel(gateway_id, short_address, scene_id));
const auto level = dali_domain_.querySceneLevel(gateway_id, short_address, scene_id);
cache_.markDaliAddressPresence(gateway_id, short_address,
level.has_value() ? GatewayCacheDaliPresence::kOnline
: GatewayCacheDaliPresence::kOffline);
cache_.setDaliSceneLevel(gateway_id, short_address, scene_id, level);
}
void GatewayController::reconcileSettingsStep(uint8_t gateway_id, uint8_t short_address) {
@@ -618,6 +716,9 @@ void GatewayController::reconcileSettingsStep(uint8_t gateway_id, uint8_t short_
}
const auto settings = dali_domain_.queryAddressSettings(gateway_id, short_address);
cache_.markDaliAddressPresence(gateway_id, short_address,
settings.has_value() ? GatewayCacheDaliPresence::kOnline
: GatewayCacheDaliPresence::kOffline);
if (settings.has_value()) {
cache_.setDaliSettings(gateway_id, short_address,
GatewayCacheDaliSettingsSnapshot{settings->power_on_level,
@@ -645,6 +746,9 @@ void GatewayController::dispatchCommand(const std::vector<uint8_t>& command) {
ESP_LOGW(kTag, "command for unknown gateway=%u opcode=0x%02x", gateway_id, opcode);
return;
}
if (IsDaliHostCommandOpcode(opcode)) {
dali_domain_.markHostActivity(gateway_id);
}
switch (opcode) {
case 0x00:
@@ -1021,14 +1125,18 @@ void GatewayController::handleDaliRawFrame(const DaliRawFrame& frame) {
}
const bool maintenance_activity = maintenance_activity_gateway_.load() == frame.gateway_id;
const bool host_echo_activity =
dali_domain_.matchesRecentHostCommandFrame(frame.gateway_id, addr, data,
config_.cache_host_echo_ms) ||
dali_domain_.hasRecentHostActivity(frame.gateway_id, config_.cache_host_echo_ms);
const bool local_activity = maintenance_activity || runtime_.hasActiveCommand(frame.gateway_id) ||
dali_domain_.isAllocAddr(frame.gateway_id);
host_echo_activity || 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);
scheduleReconciliation(frame.gateway_id, cache_.decodeDaliTarget(addr));
}
if (setup_mode_ || dali_domain_.isAllocAddr(frame.gateway_id) || maintenance_activity ||
@@ -1057,26 +1165,31 @@ bool GatewayController::sendExtRawAndMirror(uint8_t gateway_id, uint8_t raw_addr
}
bool GatewayController::setBrightAndMirror(uint8_t gateway_id, int dec_address, uint8_t level) {
const uint8_t raw_addr = rawArcAddressFromDec(dec_address);
dali_domain_.markHostCommandFrame(gateway_id, raw_addr, level);
const bool sent = dali_domain_.setBright(gateway_id, dec_address, level);
if (sent) {
cache_.mirrorDaliCommand(gateway_id, rawArcAddressFromDec(dec_address), level);
cache_.mirrorDaliCommand(gateway_id, raw_addr, level);
}
return sent;
}
bool GatewayController::offAndMirror(uint8_t gateway_id, int dec_address) {
const uint8_t raw_addr = rawCommandAddressFromDec(dec_address);
dali_domain_.markHostCommandFrame(gateway_id, raw_addr, kDaliCmdOff);
const bool sent = dali_domain_.off(gateway_id, dec_address);
if (sent) {
cache_.mirrorDaliCommand(gateway_id, rawCommandAddressFromDec(dec_address), kDaliCmdOff);
cache_.mirrorDaliCommand(gateway_id, raw_addr, kDaliCmdOff);
}
return sent;
}
bool GatewayController::onAndMirror(uint8_t gateway_id, int dec_address) {
const uint8_t raw_addr = rawCommandAddressFromDec(dec_address);
dali_domain_.markHostCommandFrame(gateway_id, raw_addr, kDaliCmdRecallMax);
const bool sent = dali_domain_.on(gateway_id, dec_address);
if (sent) {
cache_.mirrorDaliCommand(gateway_id, rawCommandAddressFromDec(dec_address),
kDaliCmdRecallMax);
cache_.mirrorDaliCommand(gateway_id, raw_addr, kDaliCmdRecallMax);
}
return sent;
}