Implement DALI Gateway Cache with State Management and Reconciliation
- Added DaliGatewayCache class for managing DALI device states, including address states, presence, and group statuses. - Implemented methods for configuring cache, enabling/disabling features, and setting status update callbacks. - Introduced state encoding/decoding for persistence and added support for CSV parsing. - Created mutation handling for runtime status updates, group masks, scene levels, and settings. - Developed reconciliation logic to handle incoming frames and classify mutations. - Added tests to validate cache functionality, persistence, and DALI protocol handling. Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
+3
-1
@@ -15,6 +15,7 @@
|
||||
#include "dt6.hpp"
|
||||
#include "dt8.hpp"
|
||||
#include "addr.hpp"
|
||||
#include "dali_gateway.hpp"
|
||||
#include "gateway_cloud.hpp"
|
||||
#include "gateway_provisioning.hpp"
|
||||
#include "color.hpp"
|
||||
@@ -27,7 +28,8 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
// Convenience umbrella header for the ESP-IDF DALI component.
|
||||
// Convenience umbrella header for the complete ESP-IDF DALI component.
|
||||
// Platform-neutral gateway adapters can include dali_gateway.hpp directly.
|
||||
|
||||
class Dali {
|
||||
public:
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
#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};
|
||||
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);
|
||||
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 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);
|
||||
|
||||
mutable std::recursive_mutex mutex_;
|
||||
DaliGatewayCacheConfig config_;
|
||||
StatusUpdateCallback statusUpdateCallback_;
|
||||
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};
|
||||
};
|
||||
Reference in New Issue
Block a user