Files

287 lines
11 KiB
C++

#pragma once
#include <array>
#include <cstdint>
#include <functional>
#include <map>
#include <mutex>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
// 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<uint8_t> powerOnLevel;
std::optional<uint8_t> systemFailureLevel;
std::optional<uint8_t> minLevel;
std::optional<uint8_t> maxLevel;
std::optional<uint8_t> fadeTime;
std::optional<uint8_t> 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<uint8_t> actualLevel;
std::optional<uint8_t> 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<uint16_t> daliDeviceTypeMask;
std::optional<uint8_t> dt8ColorTypeFeatures;
std::array<std::optional<uint8_t>, 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<uint8_t> 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<std::optional<std::string>(uint8_t channel, uint8_t shortAddress)> load;
std::function<bool(uint8_t channel, uint8_t shortAddress,
const std::optional<std::string>& payload)>
store;
std::function<bool()> commit;
};
class DaliGatewayCache {
public:
using StatusUpdateCallback = std::function<void(const DaliGatewayStatusUpdate&)>;
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<DaliGatewayTarget> 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<uint16_t> groupMask);
bool setCapabilities(uint8_t channel, uint8_t shortAddress,
std::optional<uint16_t> daliDeviceTypeMask,
std::optional<uint8_t> dt8ColorTypeFeatures);
bool setSceneLevel(uint8_t channel, uint8_t shortAddress, uint8_t scene,
std::optional<uint8_t> level);
bool setSettings(uint8_t channel, uint8_t shortAddress,
std::optional<DaliGatewaySettingsSnapshot> settings);
bool setActualLevel(uint8_t channel, uint8_t shortAddress, std::optional<uint8_t> 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<uint8_t> reconciliationAddresses(
uint8_t channel, std::optional<DaliGatewayTarget> 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<DaliGatewayAddressState, 64> addressStates(uint8_t channel) const;
void restoreAddressStates(uint8_t channel,
const std::array<DaliGatewayAddressState, 64>& states);
bool preloadChannel(uint8_t channel);
bool flush();
private:
struct DtrState {
std::optional<uint8_t> dtr0;
std::optional<uint8_t> dtr1;
std::optional<uint8_t> dtr2;
};
using AddressStates = std::array<DaliGatewayAddressState, 64>;
using PresenceStates = std::array<DaliGatewayPresence, 64>;
using GroupStatuses = std::array<DaliGatewayRuntimeStatus, 16>;
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<DaliGatewayStatusUpdate>* statusUpdate);
std::optional<DaliGatewayStatusUpdate> 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<uint8_t> 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<std::string> encodeAddressState(
const DaliGatewayAddressState& state);
static std::optional<DaliGatewayAddressState> decodeAddressState(std::string_view payload);
StatusUpdateCallback statusUpdateCallbackLocked() const;
mutable std::recursive_mutex mutex_;
DaliGatewayCacheConfig config_;
StatusUpdateCallback statusUpdateCallback_;
std::vector<StatusUpdateCallback> statusUpdateCallbacks_;
DaliGatewayCachePersistenceCallbacks persistence_;
std::map<uint8_t, AddressStates> states_;
std::map<uint8_t, PresenceStates> presence_;
std::map<uint8_t, GroupStatuses> groupStatuses_;
std::map<uint8_t, DaliGatewayRuntimeStatus> broadcastStatuses_;
std::map<uint8_t, DtrState> dtrStates_;
std::map<uint8_t, DaliGatewayChannelFlags> flags_;
std::map<uint8_t, uint64_t> 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<uint8_t> shortAddress;
uint32_t randomAddress{0x00D10301U};
uint8_t versionNumber{2};
uint8_t extendedVersionNumber{1};
bool applicationControllerEnabled{true};
bool applicationControllerAlwaysActive{false};
};
struct DaliApplicationControllerResult {
std::optional<uint8_t> backwardFrame;
bool identifyRequested{false};
bool shortAddressChanged{false};
std::optional<uint8_t> 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<uint8_t> shortAddress() const;
void setShortAddress(std::optional<uint8_t> shortAddress);
DaliApplicationControllerResult handleForwardFrame(const std::array<uint8_t, 3>& frame,
bool doubleSendConfirmed = false);
private:
bool isAddressed(const std::array<uint8_t, 3>& frame) const;
bool isSelectedForCommissioning() const;
uint32_t searchAddress() const;
std::optional<uint8_t> 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};
};