#pragma once #include #include #include #include #include #include #include #include #include // Hardware- and platform-independent DALI gateway application primitives. // // A transport owns frame timing, collision handling, persistence, and the // actual transmission of backward frames. This header only interprets DALI // forward frames and maintains the high-level state that an IoT adapter can // expose to its own protocol. enum class DaliGatewayCachePriorityMode : uint8_t { outsideBusFirst = 0, localGatewayFirst = 1, }; enum class DaliGatewayFrameOrigin : uint8_t { localGateway = 0, outsideBus = 1, }; enum class DaliGatewayTargetKind : uint8_t { shortAddress = 0, group = 1, broadcast = 2, }; enum class DaliGatewayPresence : uint8_t { unknown = 0, online = 1, offline = 2, }; struct DaliGatewayTarget { DaliGatewayTargetKind kind{DaliGatewayTargetKind::shortAddress}; uint8_t value{0}; }; struct DaliGatewayChannelFlags { bool needUpdateGroup{false}; bool needUpdateScene{false}; bool needUpdateSettings{false}; bool any() const { return needUpdateGroup || needUpdateScene || needUpdateSettings; } }; struct DaliGatewaySettingsSnapshot { std::optional powerOnLevel; std::optional systemFailureLevel; std::optional minLevel; std::optional maxLevel; std::optional fadeTime; std::optional fadeRate; bool anyKnown() const { return powerOnLevel.has_value() || systemFailureLevel.has_value() || minLevel.has_value() || maxLevel.has_value() || fadeTime.has_value() || fadeRate.has_value(); } }; struct DaliGatewayRuntimeStatus { std::optional actualLevel; std::optional sceneID; bool useMinLevel{false}; bool stale{false}; uint32_t revision{0}; bool anyKnown() const { return actualLevel.has_value() || sceneID.has_value() || useMinLevel; } }; struct DaliGatewayAddressState { bool groupMaskKnown{false}; uint16_t groupMask{0}; // Capability data is populated by discovery adapters and persisted with the // rest of the address cache so bridge planners do not need a live bus scan // on every boot. std::optional daliDeviceTypeMask; std::optional dt8ColorTypeFeatures; std::array, 16> sceneLevels{}; DaliGatewaySettingsSnapshot settings; DaliGatewayRuntimeStatus status; }; struct DaliGatewayCacheConfig { bool enabled{true}; bool reconciliationEnabled{true}; bool fullStateMirrorEnabled{false}; DaliGatewayCachePriorityMode priorityMode{DaliGatewayCachePriorityMode::outsideBusFirst}; }; struct DaliGatewayStatusUpdate { uint8_t channel{0}; DaliGatewayTarget target; DaliGatewayRuntimeStatus status; std::vector affectedShortAddresses; }; struct DaliGatewayCachePersistenceCallbacks { // The adapter maps a channel/address pair to its own storage. The portable // cache owns the encoded payload and its versioning. std::function(uint8_t channel, uint8_t shortAddress)> load; std::function& payload)> store; std::function commit; }; class DaliGatewayCache { public: using StatusUpdateCallback = std::function; explicit DaliGatewayCache(DaliGatewayCacheConfig config = {}); void configure(DaliGatewayCacheConfig config); bool enabled() const; bool setEnabled(bool enabled); bool reconciliationEnabled() const; bool fullStateMirrorEnabled() const; 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); DaliGatewayAddressState addressState(uint8_t channel, uint8_t shortAddress) const; DaliGatewayPresence addressPresence(uint8_t channel, uint8_t shortAddress) const; DaliGatewayRuntimeStatus groupStatus(uint8_t channel, uint8_t group) const; DaliGatewayRuntimeStatus broadcastStatus(uint8_t channel) const; DaliGatewayChannelFlags channelFlags(uint8_t channel) const; DaliGatewayChannelFlags pendingChannelFlags(uint8_t channel) const; void markAddressPresence(uint8_t channel, uint8_t shortAddress, DaliGatewayPresence presence); bool setGroupMask(uint8_t channel, uint8_t shortAddress, std::optional groupMask); bool setCapabilities(uint8_t channel, uint8_t shortAddress, std::optional daliDeviceTypeMask, std::optional dt8ColorTypeFeatures); bool setSceneLevel(uint8_t channel, uint8_t shortAddress, uint8_t scene, std::optional level); bool setSettings(uint8_t channel, uint8_t shortAddress, std::optional settings); bool setActualLevel(uint8_t channel, uint8_t shortAddress, std::optional level); // Call after a successful locally initiated command, or for an observed // command that should update the state mirror. The result is true only if // a DALI state value changed. bool mirrorForwardFrame(uint8_t channel, uint8_t rawAddress, uint8_t command); bool observeForwardFrame(uint8_t channel, uint8_t rawAddress, uint8_t command, DaliGatewayFrameOrigin origin); std::vector reconciliationAddresses( uint8_t channel, std::optional target = std::nullopt) const; bool clearChannelFlagsIfMatched(uint8_t channel, const DaliGatewayChannelFlags& expected); void markGroupUpdateNeeded(uint8_t channel, bool needed = true); void markSceneUpdateNeeded(uint8_t channel, bool needed = true); void markSettingsUpdateNeeded(uint8_t channel, bool needed = true); // The cache owns persistence encoding and dirty tracking. The adapter only // binds platform storage callbacks and decides when preload/flush run. std::array addressStates(uint8_t channel) const; void restoreAddressStates(uint8_t channel, const std::array& states); bool preloadChannel(uint8_t channel); bool flush(); private: struct DtrState { std::optional dtr0; std::optional dtr1; std::optional dtr2; }; using AddressStates = std::array; using PresenceStates = std::array; using GroupStatuses = std::array; AddressStates& ensureStates(uint8_t channel); PresenceStates& ensurePresence(uint8_t channel); GroupStatuses& ensureGroupStatuses(uint8_t channel); DaliGatewayRuntimeStatus& ensureBroadcastStatus(uint8_t channel); uint32_t nextRevision(); bool mirrorForwardFrameLocked(uint8_t channel, uint8_t rawAddress, uint8_t command, std::optional* statusUpdate); std::optional statusUpdate( uint8_t channel, const DaliGatewayTarget& target) const; void markDirty(uint8_t channel); void clearTarget(uint8_t channel, const DaliGatewayTarget& target, uint32_t revision); void applyRuntimeStatus(uint8_t channel, const DaliGatewayTarget& target, const DaliGatewayRuntimeStatus& status); static void applyRuntimeStatusToAddress(DaliGatewayAddressState& address, const DaliGatewayRuntimeStatus& status); void applyGroupMutation(uint8_t channel, const DaliGatewayTarget& target, uint8_t group, bool add); void applySceneMutation(uint8_t channel, const DaliGatewayTarget& target, uint8_t scene, std::optional level); void applySettingsMutation(uint8_t channel, const DaliGatewayTarget& target, uint8_t command, uint8_t value); void refreshAggregateStatus(uint8_t channel, DaliGatewayAddressState& address); 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_; std::map groupStatuses_; std::map broadcastStatuses_; std::map dtrStates_; std::map flags_; std::map dirtyGenerations_; uint32_t revision_{0}; }; struct DaliApplicationControllerConfig { // nullopt models an unaddressed control device. A commissioning adapter can // persist this value and restore it at boot. std::optional shortAddress; uint32_t randomAddress{0x00D10301U}; uint8_t versionNumber{2}; uint8_t extendedVersionNumber{1}; bool applicationControllerEnabled{true}; bool applicationControllerAlwaysActive{false}; }; struct DaliApplicationControllerResult { std::optional backwardFrame; bool identifyRequested{false}; bool shortAddressChanged{false}; std::optional shortAddress; }; // IEC 62386-103 logical control device with application-controller capability. // The caller supplies whether a command that is specified as "send twice" has // been confirmed by its transport/timing layer; this keeps timing out of the // library while preserving the required command semantics. class DaliApplicationController { public: explicit DaliApplicationController(DaliApplicationControllerConfig config = {}); const DaliApplicationControllerConfig& config() const; std::optional shortAddress() const; void setShortAddress(std::optional shortAddress); DaliApplicationControllerResult handleForwardFrame(const std::array& frame, bool doubleSendConfirmed = false); private: bool isAddressed(const std::array& frame) const; bool isSelectedForCommissioning() const; uint32_t searchAddress() const; std::optional queryResponse(uint8_t opcode) const; void reset(); DaliApplicationControllerConfig config_; bool powerCycleNotificationEnabled_{true}; bool powerCycleSeen_{true}; bool resetState_{false}; bool initialising_{false}; bool withdrawn_{false}; uint8_t operatingMode_{0}; uint8_t dtr0_{0}; uint8_t dtr1_{0}; uint8_t dtr2_{0}; uint32_t searchAddress_{0}; };