diff --git a/include/dali_gateway.hpp b/include/dali_gateway.hpp index c322499..daf5c8c 100644 --- a/include/dali_gateway.hpp +++ b/include/dali_gateway.hpp @@ -126,6 +126,10 @@ class DaliGatewayCache { DaliGatewayCachePriorityMode priorityMode() const; void setPriorityMode(DaliGatewayCachePriorityMode mode); void setStatusUpdateCallback(StatusUpdateCallback callback); + // Register an additional semantic status observer without replacing the + // protocol bridge callback installed through setStatusUpdateCallback(). + // Adapters registered here must outlive the cache. + void addStatusUpdateCallback(StatusUpdateCallback callback); void setPersistenceCallbacks(DaliGatewayCachePersistenceCallbacks callbacks); static std::optional decodeTarget(uint8_t rawAddress); @@ -203,10 +207,12 @@ class DaliGatewayCache { static std::optional encodeAddressState( const DaliGatewayAddressState& state); static std::optional decodeAddressState(std::string_view payload); + StatusUpdateCallback statusUpdateCallbackLocked() const; mutable std::recursive_mutex mutex_; DaliGatewayCacheConfig config_; StatusUpdateCallback statusUpdateCallback_; + std::vector statusUpdateCallbacks_; DaliGatewayCachePersistenceCallbacks persistence_; std::map states_; std::map presence_; diff --git a/src/dali_gateway_cache.cpp b/src/dali_gateway_cache.cpp index 6ffcba4..e22a6c0 100644 --- a/src/dali_gateway_cache.cpp +++ b/src/dali_gateway_cache.cpp @@ -118,6 +118,25 @@ void DaliGatewayCache::setStatusUpdateCallback(StatusUpdateCallback callback) { statusUpdateCallback_ = std::move(callback); } +void DaliGatewayCache::addStatusUpdateCallback(StatusUpdateCallback callback) { + if (!callback) return; + std::lock_guard guard(mutex_); + statusUpdateCallbacks_.push_back(std::move(callback)); +} + +DaliGatewayCache::StatusUpdateCallback DaliGatewayCache::statusUpdateCallbackLocked() const { + std::vector callbacks; + callbacks.reserve(statusUpdateCallbacks_.size() + (statusUpdateCallback_ ? 1U : 0U)); + if (statusUpdateCallback_) callbacks.push_back(statusUpdateCallback_); + callbacks.insert(callbacks.end(), statusUpdateCallbacks_.begin(), statusUpdateCallbacks_.end()); + if (callbacks.empty()) return {}; + return [callbacks = std::move(callbacks)](const DaliGatewayStatusUpdate& update) { + for (const auto& callback : callbacks) { + if (callback) callback(update); + } + }; +} + void DaliGatewayCache::setPersistenceCallbacks( DaliGatewayCachePersistenceCallbacks callbacks) { std::lock_guard guard(mutex_); diff --git a/src/dali_gateway_cache_mutation.cpp b/src/dali_gateway_cache_mutation.cpp index 4eb5aac..a8c05d9 100644 --- a/src/dali_gateway_cache_mutation.cpp +++ b/src/dali_gateway_cache_mutation.cpp @@ -140,7 +140,7 @@ bool DaliGatewayCache::setGroupMask(uint8_t channel, uint8_t shortAddress, markDirty(channel); update = statusUpdate( channel, DaliGatewayTarget{DaliGatewayTargetKind::shortAddress, shortAddress}); - callback = statusUpdateCallback_; + callback = statusUpdateCallbackLocked(); } if (callback && update.has_value()) callback(*update); return true; @@ -162,7 +162,7 @@ bool DaliGatewayCache::setSceneLevel(uint8_t channel, uint8_t shortAddress, uint markDirty(channel); update = statusUpdate( channel, DaliGatewayTarget{DaliGatewayTargetKind::shortAddress, shortAddress}); - callback = statusUpdateCallback_; + callback = statusUpdateCallbackLocked(); } if (callback && update.has_value()) callback(*update); return true; @@ -181,7 +181,7 @@ bool DaliGatewayCache::setSettings(uint8_t channel, uint8_t shortAddress, markDirty(channel); update = statusUpdate( channel, DaliGatewayTarget{DaliGatewayTargetKind::shortAddress, shortAddress}); - callback = statusUpdateCallback_; + callback = statusUpdateCallbackLocked(); } if (callback && update.has_value()) callback(*update); return true; @@ -203,7 +203,7 @@ bool DaliGatewayCache::setActualLevel(uint8_t channel, uint8_t shortAddress, markDirty(channel); update = statusUpdate( channel, DaliGatewayTarget{DaliGatewayTargetKind::shortAddress, shortAddress}); - callback = statusUpdateCallback_; + callback = statusUpdateCallbackLocked(); } if (callback && update.has_value()) callback(*update); return true; @@ -394,7 +394,7 @@ bool DaliGatewayCache::mirrorForwardFrame(uint8_t channel, uint8_t rawAddress, { std::lock_guard guard(mutex_); changed = mirrorForwardFrameLocked(channel, rawAddress, command, &update); - callback = statusUpdateCallback_; + callback = statusUpdateCallbackLocked(); } if (callback && update.has_value()) callback(*update); return changed; diff --git a/src/dali_gateway_reconciliation.cpp b/src/dali_gateway_reconciliation.cpp index e7d182b..1f431fc 100644 --- a/src/dali_gateway_reconciliation.cpp +++ b/src/dali_gateway_reconciliation.cpp @@ -80,7 +80,7 @@ bool DaliGatewayCache::observeForwardFrame(uint8_t channel, uint8_t rawAddress, flags.needUpdateSettings = flags.needUpdateSettings || mutation.needUpdateSettings; flagged = true; } - callback = statusUpdateCallback_; + callback = statusUpdateCallbackLocked(); } if (callback && update.has_value()) callback(*update); return flagged; diff --git a/tests/dali_gateway_test.cpp b/tests/dali_gateway_test.cpp index afd30d5..a5fbb7d 100644 --- a/tests/dali_gateway_test.cpp +++ b/tests/dali_gateway_test.cpp @@ -30,15 +30,19 @@ int main() { }); int status_updates = 0; + int observer_updates = 0; DaliGatewayStatusUpdate last_update; cache.setStatusUpdateCallback([&](const DaliGatewayStatusUpdate& update) { ++status_updates; last_update = update; }); + cache.addStatusUpdateCallback( + [&](const DaliGatewayStatusUpdate&) { ++observer_updates; }); assert(cache.setGroupMask(7, 4, 0x0001)); assert(cache.mirrorForwardFrame(7, 0x80, 42)); assert(status_updates == 1); + assert(observer_updates == status_updates); assert(last_update.channel == 7); assert(last_update.target.kind == DaliGatewayTargetKind::group); assert(last_update.target.value == 0); @@ -47,6 +51,12 @@ int main() { assert(cache.groupStatus(7, 0).actualLevel == 42); assert(cache.addressState(7, 4).status.actualLevel == 42); + // Replacing the owning callback must not remove additive adapter observers. + cache.setStatusUpdateCallback({}); + assert(cache.setActualLevel(7, 4, 43)); + assert(status_updates == 1); + assert(observer_updates == 2); + // The command-state cache tracks DTR-dependent settings and applies a // broadcast command to every state known by the adapter. cache.mirrorForwardFrame(7, 0xA3, 12);