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:
@@ -1,6 +1,21 @@
|
||||
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND "$ENV{IDF_PATH}" STREQUAL "")
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(dali_cpp_gateway LANGUAGES CXX)
|
||||
endif()
|
||||
|
||||
set(DALI_CPP_GATEWAY_SOURCES
|
||||
"src/dali_protocol.cpp"
|
||||
"src/dali_gateway_cache.cpp"
|
||||
"src/dali_gateway_cache_mutation.cpp"
|
||||
"src/dali_gateway_reconciliation.cpp"
|
||||
"src/dali_application_controller.cpp"
|
||||
)
|
||||
|
||||
if(COMMAND idf_component_register)
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"src/dali_comm.cpp"
|
||||
${DALI_CPP_GATEWAY_SOURCES}
|
||||
"src/base.cpp"
|
||||
"src/addr.cpp"
|
||||
"src/bridge.cpp"
|
||||
@@ -29,3 +44,17 @@ idf_component_register(
|
||||
)
|
||||
|
||||
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
|
||||
else()
|
||||
# Standalone target for IoT adapters that only need the platform-neutral
|
||||
# DALI gateway application behavior.
|
||||
add_library(dali_cpp_gateway STATIC ${DALI_CPP_GATEWAY_SOURCES})
|
||||
target_include_directories(dali_cpp_gateway PUBLIC "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(dali_cpp_gateway PUBLIC cxx_std_17)
|
||||
|
||||
include(CTest)
|
||||
if(BUILD_TESTING)
|
||||
add_executable(dali_cpp_gateway_test "tests/dali_gateway_test.cpp")
|
||||
target_link_libraries(dali_cpp_gateway_test PRIVATE dali_cpp_gateway)
|
||||
add_test(NAME dali_cpp_gateway_test COMMAND dali_cpp_gateway_test)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -1,6 +1,105 @@
|
||||
# DALI ESP-IDF Component
|
||||
# DALI C++ Library
|
||||
|
||||
A lightweight C++ implementation of the DALI stack used in `lib/dali/*.dart` for gateway type 1 (USB UART) traffic. The component mirrors Dart method names so it can be used as an embedded replacement target.
|
||||
A C++ implementation of the DALI stack used in `lib/dali/*.dart`. The
|
||||
portable DALI application layer mirrors Dart method names where practical, so
|
||||
it can become an embedded replacement target without starting that migration
|
||||
yet.
|
||||
|
||||
## Portable Gateway Application Layer
|
||||
|
||||
`include/dali_gateway.hpp` is the hardware- and platform-independent gateway
|
||||
API. It uses only the C++17 standard library and deliberately owns neither a
|
||||
transceiver, a clock, a task, nor a platform storage API:
|
||||
|
||||
- `DaliGatewayCache` interprets direct, group, and broadcast forward frames;
|
||||
owns cached DALI state, persistence encoding, dirty tracking, and
|
||||
reconciliation work.
|
||||
- `DaliApplicationController` implements the Part 103 logical application
|
||||
controller: standard control-device queries, `IDENTIFY DEVICE`, and control
|
||||
device short-address commissioning (`INITIALISE`, `RANDOMISE`, search,
|
||||
`COMPARE`, `PROGRAM SHORT ADDRESS`, `VERIFY`, and `QUERY SHORT ADDRESS`).
|
||||
- The transport/adapter supplies the `doubleSendConfirmed` argument for
|
||||
commands that must be sent twice, sends returned backward frames, persists
|
||||
address state through registered key/value callbacks, and maps
|
||||
`identifyRequested` to its own physical indication.
|
||||
|
||||
This lets another IoT protocol use the DALI behavior without pulling in
|
||||
ESP-IDF. The current ESP-IDF gateway is one such adapter: its NVS wrapper and
|
||||
FreeRTOS/PHY timing stay under `gateway/components/`. Existing cloud and
|
||||
provisioning helpers remain compatibility integrations for the ESP-IDF
|
||||
component; they are not used by the portable gateway API.
|
||||
|
||||
For a non-ESP project, add this directory with CMake and link the focused
|
||||
`dali_cpp_gateway` target:
|
||||
|
||||
```cmake
|
||||
add_subdirectory(path/to/dali_cpp)
|
||||
target_link_libraries(my_iot_adapter PRIVATE dali_cpp_gateway)
|
||||
```
|
||||
|
||||
### Bind a transceiver and an IoT protocol
|
||||
|
||||
The portable cache reports semantic status changes, so an adapter does not
|
||||
need to decode DALI opcodes before updating KNX, Modbus, BACnet, MQTT, or
|
||||
another IoT protocol:
|
||||
|
||||
```cpp
|
||||
DaliGatewayCache cache({
|
||||
true, // cache enabled
|
||||
true, // reconcile changes observed from another DALI master
|
||||
false, // only reconcile the affected state category
|
||||
DaliGatewayCachePriorityMode::outsideBusFirst,
|
||||
});
|
||||
|
||||
cache.setStatusUpdateCallback([](const DaliGatewayStatusUpdate& update) {
|
||||
// update.target is direct, group, or broadcast.
|
||||
// update.status contains the interpreted level/scene state.
|
||||
// update.affectedShortAddresses is ready for protocols that expose
|
||||
// per-device status.
|
||||
my_iot_adapter.publishDaliStatus(update);
|
||||
});
|
||||
|
||||
cache.setPersistenceCallbacks({
|
||||
[](uint8_t channel, uint8_t address) {
|
||||
return my_storage.load(channel, address);
|
||||
},
|
||||
[](uint8_t channel, uint8_t address,
|
||||
const std::optional<std::string>& payload) {
|
||||
return my_storage.storeOrErase(channel, address, payload);
|
||||
},
|
||||
[]() { return my_storage.commit(); },
|
||||
});
|
||||
|
||||
cache.preloadChannel(0);
|
||||
|
||||
my_transceiver.onForwardFrame([&cache](uint8_t address, uint8_t data) {
|
||||
cache.observeForwardFrame(0, address, data,
|
||||
DaliGatewayFrameOrigin::outsideBus);
|
||||
});
|
||||
|
||||
// After a locally requested transmission succeeds, mirror it once. The cache
|
||||
// performs all direct/group/broadcast and DTR-dependent interpretation.
|
||||
if (my_transceiver.send(address, data)) {
|
||||
cache.mirrorForwardFrame(0, address, data);
|
||||
}
|
||||
|
||||
// Call from the adapter's low-priority worker or shutdown path.
|
||||
cache.flush();
|
||||
```
|
||||
|
||||
The callbacks contain no ESP-IDF, RTOS, NVS, network, or transceiver types.
|
||||
An adapter chooses its own tasking, storage, bus timing, and IoT protocol.
|
||||
|
||||
The portable implementation is deliberately split by responsibility:
|
||||
|
||||
- `dali_protocol.cpp` decodes DALI target addressing.
|
||||
- `dali_gateway_cache.cpp` owns cache state and adapter-facing persistence
|
||||
snapshots.
|
||||
- `dali_gateway_cache_mutation.cpp` applies direct, group, broadcast, scene,
|
||||
DTR, and settings feedback.
|
||||
- `dali_gateway_reconciliation.cpp` derives external-bus reconciliation work.
|
||||
- `dali_application_controller.cpp` implements Part 103 control-device
|
||||
queries, commissioning, and identity semantics.
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -220,7 +319,7 @@ Source the helper script from your shell so the exported ESP-IDF variables stay
|
||||
. ./scripts/export_esp_idf.sh
|
||||
```
|
||||
|
||||
The helper script sources `~/esp/v5.5.2/esp-idf/export.sh`.
|
||||
The helper script sources the installed ESP-IDF v5.5.4 `export.sh`.
|
||||
|
||||
### Build the Example
|
||||
|
||||
|
||||
+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};
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
ESP_IDF_EXPORT_PATH="${HOME}/esp/v5.5.2/esp-idf/export.sh"
|
||||
ESP_IDF_EXPORT_PATH="${HOME}/.espressif/v5.5.4/esp-idf/export.sh"
|
||||
|
||||
if [ ! -f "$ESP_IDF_EXPORT_PATH" ]; then
|
||||
echo "ESP-IDF export script not found at $ESP_IDF_EXPORT_PATH" >&2
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
#include "dali_gateway.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsDoubleSendCommand(const std::array<uint8_t, 3>& frame) {
|
||||
if (frame[0] == 0xC1) {
|
||||
return frame[1] == 0x01 || frame[1] == 0x02; // INITIALISE, RANDOMISE
|
||||
}
|
||||
if ((frame[0] & 1U) == 0 || frame[1] != 0xFE) return false;
|
||||
switch (frame[2]) {
|
||||
case 0x00: // IDENTIFY DEVICE
|
||||
case 0x01: // RESET POWER CYCLE SEEN
|
||||
case 0x10: // RESET
|
||||
case 0x16: // ENABLE APPLICATION CONTROLLER
|
||||
case 0x17: // DISABLE APPLICATION CONTROLLER
|
||||
case 0x18: // SET OPERATING MODE
|
||||
case 0x1F: // ENABLE POWER CYCLE NOTIFICATION
|
||||
case 0x20: // DISABLE POWER CYCLE NOTIFICATION
|
||||
case 0x21: // SAVE PERSISTENT VARIABLES
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
DaliApplicationController::DaliApplicationController(DaliApplicationControllerConfig config)
|
||||
: config_(config), searchAddress_(config.randomAddress & 0xFFFFFFU) {
|
||||
if (config_.shortAddress.has_value() && *config_.shortAddress > 63) {
|
||||
config_.shortAddress.reset();
|
||||
}
|
||||
config_.randomAddress &= 0xFFFFFFU;
|
||||
}
|
||||
|
||||
const DaliApplicationControllerConfig& DaliApplicationController::config() const {
|
||||
return config_;
|
||||
}
|
||||
|
||||
std::optional<uint8_t> DaliApplicationController::shortAddress() const {
|
||||
return config_.shortAddress;
|
||||
}
|
||||
|
||||
void DaliApplicationController::setShortAddress(std::optional<uint8_t> shortAddress) {
|
||||
config_.shortAddress = shortAddress.has_value() && *shortAddress <= 63 ? shortAddress
|
||||
: std::nullopt;
|
||||
}
|
||||
|
||||
bool DaliApplicationController::isAddressed(const std::array<uint8_t, 3>& frame) const {
|
||||
return (frame[0] & 1U) != 0 &&
|
||||
(frame[0] == 0xFF || (config_.shortAddress.has_value() &&
|
||||
frame[0] == static_cast<uint8_t>(
|
||||
(*config_.shortAddress << 1) | 1U)));
|
||||
}
|
||||
|
||||
bool DaliApplicationController::isSelectedForCommissioning() const {
|
||||
return initialising_ && !withdrawn_ && config_.randomAddress == searchAddress();
|
||||
}
|
||||
|
||||
uint32_t DaliApplicationController::searchAddress() const {
|
||||
return searchAddress_ & 0xFFFFFFU;
|
||||
}
|
||||
|
||||
void DaliApplicationController::reset() {
|
||||
config_.applicationControllerEnabled = true;
|
||||
powerCycleNotificationEnabled_ = true;
|
||||
resetState_ = true;
|
||||
operatingMode_ = 0;
|
||||
dtr0_ = dtr1_ = dtr2_ = 0;
|
||||
}
|
||||
|
||||
std::optional<uint8_t> DaliApplicationController::queryResponse(uint8_t opcode) const {
|
||||
switch (opcode) {
|
||||
case 0x30: {
|
||||
uint8_t status = 0;
|
||||
// Bit 2 reports that the device currently has a short-address *mask*,
|
||||
// rather than a commissioned short address.
|
||||
if (!config_.shortAddress.has_value()) status |= 0x04;
|
||||
if (config_.applicationControllerEnabled) status |= 0x08;
|
||||
if (powerCycleSeen_) status |= 0x20;
|
||||
if (resetState_) status |= 0x40;
|
||||
return status;
|
||||
}
|
||||
case 0x31:
|
||||
case 0x32:
|
||||
return 0x00;
|
||||
case 0x33:
|
||||
return static_cast<uint8_t>(config_.shortAddress.has_value() ? 0x00 : 0xFF);
|
||||
case 0x34:
|
||||
return config_.versionNumber;
|
||||
case 0x35:
|
||||
return 0x00;
|
||||
case 0x36:
|
||||
return dtr0_;
|
||||
case 0x37:
|
||||
return dtr1_;
|
||||
case 0x38:
|
||||
return dtr2_;
|
||||
case 0x39:
|
||||
return static_cast<uint8_t>((config_.randomAddress >> 16) & 0xFFU);
|
||||
case 0x3A:
|
||||
return static_cast<uint8_t>((config_.randomAddress >> 8) & 0xFFU);
|
||||
case 0x3B:
|
||||
return static_cast<uint8_t>(config_.randomAddress & 0xFFU);
|
||||
case 0x3C:
|
||||
return 0xFF;
|
||||
case 0x3D:
|
||||
return static_cast<uint8_t>(config_.applicationControllerEnabled ? 0xFF : 0x00);
|
||||
case 0x3E:
|
||||
return operatingMode_;
|
||||
case 0x3F:
|
||||
case 0x40:
|
||||
case 0x41:
|
||||
case 0x42:
|
||||
case 0x43:
|
||||
case 0x44:
|
||||
return 0x00;
|
||||
case 0x45:
|
||||
return static_cast<uint8_t>(powerCycleNotificationEnabled_ ? 0xFF : 0x00);
|
||||
case 0x46: {
|
||||
uint8_t capabilities = 0x01;
|
||||
if (config_.applicationControllerAlwaysActive) capabilities |= 0x04;
|
||||
return capabilities;
|
||||
}
|
||||
case 0x47:
|
||||
return config_.extendedVersionNumber;
|
||||
case 0x48:
|
||||
return static_cast<uint8_t>(resetState_ ? 0xFF : 0x00);
|
||||
default:
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
DaliApplicationControllerResult DaliApplicationController::handleForwardFrame(
|
||||
const std::array<uint8_t, 3>& frame, bool doubleSendConfirmed) {
|
||||
DaliApplicationControllerResult result;
|
||||
const uint8_t address = frame[0];
|
||||
const uint8_t instance = frame[1];
|
||||
const uint8_t opcode = frame[2];
|
||||
if (IsDoubleSendCommand(frame) && !doubleSendConfirmed) return result;
|
||||
|
||||
if (address == 0xC1) {
|
||||
switch (instance) {
|
||||
case 0x00:
|
||||
initialising_ = false;
|
||||
withdrawn_ = false;
|
||||
break;
|
||||
case 0x01:
|
||||
if (opcode == 0xFF ||
|
||||
(config_.shortAddress.has_value() &&
|
||||
opcode == static_cast<uint8_t>((*config_.shortAddress << 1) | 1U))) {
|
||||
initialising_ = true;
|
||||
withdrawn_ = false;
|
||||
}
|
||||
break;
|
||||
case 0x02:
|
||||
if (initialising_) {
|
||||
// Deterministic re-randomisation lets a host provide/persist the
|
||||
// seed without this library depending on an RNG or a clock.
|
||||
config_.randomAddress =
|
||||
(config_.randomAddress * 1103515245U + 12345U) & 0xFFFFFFU;
|
||||
searchAddress_ = config_.randomAddress;
|
||||
withdrawn_ = false;
|
||||
}
|
||||
break;
|
||||
case 0x03:
|
||||
if (initialising_ && !withdrawn_ && config_.randomAddress <= searchAddress()) {
|
||||
result.backwardFrame = 0xFF;
|
||||
}
|
||||
break;
|
||||
case 0x04:
|
||||
if (isSelectedForCommissioning()) withdrawn_ = true;
|
||||
break;
|
||||
case 0x05:
|
||||
searchAddress_ =
|
||||
(searchAddress_ & 0x00FFFFU) | (static_cast<uint32_t>(opcode) << 16);
|
||||
break;
|
||||
case 0x06:
|
||||
searchAddress_ =
|
||||
(searchAddress_ & 0xFF00FFU) | (static_cast<uint32_t>(opcode) << 8);
|
||||
break;
|
||||
case 0x07:
|
||||
searchAddress_ = (searchAddress_ & 0xFFFF00U) | opcode;
|
||||
break;
|
||||
case 0x08:
|
||||
if (isSelectedForCommissioning()) {
|
||||
const std::optional<uint8_t> next =
|
||||
opcode == 0xFF
|
||||
? std::nullopt
|
||||
: std::optional<uint8_t>(static_cast<uint8_t>((opcode >> 1) & 0x3FU));
|
||||
if (next != config_.shortAddress) {
|
||||
setShortAddress(next);
|
||||
result.shortAddressChanged = true;
|
||||
result.shortAddress = config_.shortAddress;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 0x09:
|
||||
if (initialising_ && config_.shortAddress.has_value() &&
|
||||
opcode == static_cast<uint8_t>((*config_.shortAddress << 1) | 1U)) {
|
||||
result.backwardFrame = 0xFF;
|
||||
}
|
||||
break;
|
||||
case 0x0A:
|
||||
if (initialising_ && isSelectedForCommissioning() && config_.shortAddress.has_value()) {
|
||||
result.backwardFrame = static_cast<uint8_t>((*config_.shortAddress << 1) | 1U);
|
||||
}
|
||||
break;
|
||||
case 0x30:
|
||||
dtr0_ = opcode;
|
||||
break;
|
||||
case 0x31:
|
||||
dtr1_ = opcode;
|
||||
break;
|
||||
case 0x32:
|
||||
dtr2_ = opcode;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!isAddressed(frame) || instance != 0xFE) return result;
|
||||
switch (opcode) {
|
||||
case 0x00:
|
||||
result.identifyRequested = true;
|
||||
break;
|
||||
case 0x01:
|
||||
powerCycleSeen_ = false;
|
||||
resetState_ = false;
|
||||
break;
|
||||
case 0x10:
|
||||
reset();
|
||||
break;
|
||||
case 0x16:
|
||||
config_.applicationControllerEnabled = true;
|
||||
resetState_ = false;
|
||||
break;
|
||||
case 0x17:
|
||||
if (!config_.applicationControllerAlwaysActive) {
|
||||
config_.applicationControllerEnabled = false;
|
||||
}
|
||||
resetState_ = false;
|
||||
break;
|
||||
case 0x18:
|
||||
operatingMode_ = dtr0_;
|
||||
resetState_ = false;
|
||||
break;
|
||||
case 0x1F:
|
||||
powerCycleNotificationEnabled_ = true;
|
||||
resetState_ = false;
|
||||
break;
|
||||
case 0x20:
|
||||
powerCycleNotificationEnabled_ = false;
|
||||
resetState_ = false;
|
||||
break;
|
||||
case 0x21:
|
||||
resetState_ = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
result.backwardFrame = queryResponse(opcode);
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
#include "dali_gateway.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kAddressStatePayloadVersion = 1;
|
||||
constexpr uint32_t kGroupMaskKnown = 1U << 0;
|
||||
constexpr uint32_t kActualLevelKnown = 1U << 1;
|
||||
constexpr uint32_t kSceneKnown = 1U << 2;
|
||||
constexpr uint32_t kUseMinLevel = 1U << 3;
|
||||
constexpr uint32_t kStatusStale = 1U << 4;
|
||||
constexpr uint32_t kPowerOnKnown = 1U << 5;
|
||||
constexpr uint32_t kSystemFailureKnown = 1U << 6;
|
||||
constexpr uint32_t kMinKnown = 1U << 7;
|
||||
constexpr uint32_t kMaxKnown = 1U << 8;
|
||||
constexpr uint32_t kFadeTimeKnown = 1U << 9;
|
||||
constexpr uint32_t kFadeRateKnown = 1U << 10;
|
||||
|
||||
std::vector<int> ParseCsv(std::string_view raw) {
|
||||
std::vector<int> values;
|
||||
size_t start = 0;
|
||||
while (start < raw.size()) {
|
||||
const size_t comma = raw.find(',', start);
|
||||
const size_t end = comma == std::string_view::npos ? raw.size() : comma;
|
||||
if (end > start) {
|
||||
values.push_back(static_cast<int>(
|
||||
std::strtol(std::string(raw.substr(start, end - start)).c_str(), nullptr, 10)));
|
||||
}
|
||||
if (comma == std::string_view::npos) break;
|
||||
start = comma + 1;
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
uint8_t ByteValue(int value) {
|
||||
return static_cast<uint8_t>(std::clamp(value, 0, 255));
|
||||
}
|
||||
|
||||
uint16_t WordValue(int value) {
|
||||
return static_cast<uint16_t>(std::clamp(value, 0, 0xffff));
|
||||
}
|
||||
|
||||
uint16_t SceneKnownMask(const DaliGatewayAddressState& state) {
|
||||
uint16_t mask = 0;
|
||||
for (size_t index = 0; index < state.sceneLevels.size(); ++index) {
|
||||
if (state.sceneLevels[index].has_value()) {
|
||||
mask |= static_cast<uint16_t>(1U << index);
|
||||
}
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
uint32_t StateFlags(const DaliGatewayAddressState& state) {
|
||||
uint32_t flags = 0;
|
||||
if (state.groupMaskKnown) flags |= kGroupMaskKnown;
|
||||
if (state.status.actualLevel.has_value()) flags |= kActualLevelKnown;
|
||||
if (state.status.sceneID.has_value()) flags |= kSceneKnown;
|
||||
if (state.status.useMinLevel) flags |= kUseMinLevel;
|
||||
if (state.status.stale) flags |= kStatusStale;
|
||||
if (state.settings.powerOnLevel.has_value()) flags |= kPowerOnKnown;
|
||||
if (state.settings.systemFailureLevel.has_value()) flags |= kSystemFailureKnown;
|
||||
if (state.settings.minLevel.has_value()) flags |= kMinKnown;
|
||||
if (state.settings.maxLevel.has_value()) flags |= kMaxKnown;
|
||||
if (state.settings.fadeTime.has_value()) flags |= kFadeTimeKnown;
|
||||
if (state.settings.fadeRate.has_value()) flags |= kFadeRateKnown;
|
||||
return flags;
|
||||
}
|
||||
|
||||
bool IsDefaultState(const DaliGatewayAddressState& state) {
|
||||
return !state.groupMaskKnown && state.groupMask == 0 && SceneKnownMask(state) == 0 &&
|
||||
!state.settings.anyKnown() && !state.status.anyKnown();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
DaliGatewayCache::DaliGatewayCache(DaliGatewayCacheConfig config) : config_(config) {}
|
||||
|
||||
void DaliGatewayCache::configure(DaliGatewayCacheConfig config) {
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
config_ = config;
|
||||
}
|
||||
|
||||
bool DaliGatewayCache::enabled() const {
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
return config_.enabled;
|
||||
}
|
||||
|
||||
bool DaliGatewayCache::setEnabled(bool enabled) {
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
config_.enabled = enabled;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DaliGatewayCache::reconciliationEnabled() const {
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
return config_.enabled && config_.reconciliationEnabled;
|
||||
}
|
||||
|
||||
bool DaliGatewayCache::fullStateMirrorEnabled() const {
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
return config_.enabled && config_.reconciliationEnabled && config_.fullStateMirrorEnabled;
|
||||
}
|
||||
|
||||
DaliGatewayCachePriorityMode DaliGatewayCache::priorityMode() const {
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
return config_.priorityMode;
|
||||
}
|
||||
|
||||
void DaliGatewayCache::setPriorityMode(DaliGatewayCachePriorityMode mode) {
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
config_.priorityMode = mode;
|
||||
}
|
||||
|
||||
void DaliGatewayCache::setStatusUpdateCallback(StatusUpdateCallback callback) {
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
statusUpdateCallback_ = std::move(callback);
|
||||
}
|
||||
|
||||
void DaliGatewayCache::setPersistenceCallbacks(
|
||||
DaliGatewayCachePersistenceCallbacks callbacks) {
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
persistence_ = std::move(callbacks);
|
||||
}
|
||||
|
||||
DaliGatewayCache::AddressStates& DaliGatewayCache::ensureStates(uint8_t channel) {
|
||||
return states_[channel];
|
||||
}
|
||||
|
||||
DaliGatewayCache::PresenceStates& DaliGatewayCache::ensurePresence(uint8_t channel) {
|
||||
return presence_[channel];
|
||||
}
|
||||
|
||||
DaliGatewayCache::GroupStatuses& DaliGatewayCache::ensureGroupStatuses(uint8_t channel) {
|
||||
return groupStatuses_[channel];
|
||||
}
|
||||
|
||||
DaliGatewayRuntimeStatus& DaliGatewayCache::ensureBroadcastStatus(uint8_t channel) {
|
||||
return broadcastStatuses_[channel];
|
||||
}
|
||||
|
||||
uint32_t DaliGatewayCache::nextRevision() {
|
||||
++revision_;
|
||||
if (revision_ == 0) ++revision_;
|
||||
return revision_;
|
||||
}
|
||||
|
||||
void DaliGatewayCache::markDirty(uint8_t channel) {
|
||||
auto& generation = dirtyGenerations_[channel];
|
||||
++generation;
|
||||
if (generation == 0) ++generation;
|
||||
}
|
||||
|
||||
DaliGatewayAddressState DaliGatewayCache::addressState(uint8_t channel,
|
||||
uint8_t shortAddress) const {
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
if (shortAddress >= 64) return {};
|
||||
const auto it = states_.find(channel);
|
||||
return it == states_.end() ? DaliGatewayAddressState{} : it->second[shortAddress];
|
||||
}
|
||||
|
||||
DaliGatewayPresence DaliGatewayCache::addressPresence(uint8_t channel,
|
||||
uint8_t shortAddress) const {
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
if (shortAddress >= 64) return DaliGatewayPresence::unknown;
|
||||
const auto it = presence_.find(channel);
|
||||
return it == presence_.end() ? DaliGatewayPresence::unknown : it->second[shortAddress];
|
||||
}
|
||||
|
||||
DaliGatewayRuntimeStatus DaliGatewayCache::groupStatus(uint8_t channel, uint8_t group) const {
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
if (group >= 16) return {};
|
||||
const auto it = groupStatuses_.find(channel);
|
||||
return it == groupStatuses_.end() ? DaliGatewayRuntimeStatus{} : it->second[group];
|
||||
}
|
||||
|
||||
DaliGatewayRuntimeStatus DaliGatewayCache::broadcastStatus(uint8_t channel) const {
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
const auto it = broadcastStatuses_.find(channel);
|
||||
return it == broadcastStatuses_.end() ? DaliGatewayRuntimeStatus{} : it->second;
|
||||
}
|
||||
|
||||
DaliGatewayChannelFlags DaliGatewayCache::channelFlags(uint8_t channel) const {
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
const auto it = flags_.find(channel);
|
||||
return it == flags_.end() ? DaliGatewayChannelFlags{} : it->second;
|
||||
}
|
||||
|
||||
DaliGatewayChannelFlags DaliGatewayCache::pendingChannelFlags(uint8_t channel) const {
|
||||
return channelFlags(channel);
|
||||
}
|
||||
|
||||
void DaliGatewayCache::markAddressPresence(uint8_t channel, uint8_t shortAddress,
|
||||
DaliGatewayPresence presence) {
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
if (shortAddress >= 64) return;
|
||||
ensurePresence(channel)[shortAddress] = presence;
|
||||
}
|
||||
|
||||
std::array<DaliGatewayAddressState, 64> DaliGatewayCache::addressStates(uint8_t channel) const {
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
const auto it = states_.find(channel);
|
||||
return it == states_.end() ? std::array<DaliGatewayAddressState, 64>{} : it->second;
|
||||
}
|
||||
|
||||
void DaliGatewayCache::restoreAddressStates(
|
||||
uint8_t channel, const std::array<DaliGatewayAddressState, 64>& states) {
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
states_[channel] = states;
|
||||
for (auto& state : states_[channel]) {
|
||||
if (state.status.anyKnown()) state.status.stale = true;
|
||||
revision_ = std::max(revision_, state.status.revision);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<DaliGatewayStatusUpdate> DaliGatewayCache::statusUpdate(
|
||||
uint8_t channel, const DaliGatewayTarget& target) const {
|
||||
DaliGatewayStatusUpdate update;
|
||||
update.channel = channel;
|
||||
update.target = target;
|
||||
const auto states = states_.find(channel);
|
||||
if (target.kind == DaliGatewayTargetKind::shortAddress) {
|
||||
if (target.value >= 64 || states == states_.end()) return std::nullopt;
|
||||
update.status = states->second[target.value].status;
|
||||
update.affectedShortAddresses.push_back(target.value);
|
||||
} else if (target.kind == DaliGatewayTargetKind::group) {
|
||||
if (target.value >= 16) return std::nullopt;
|
||||
const auto groups = groupStatuses_.find(channel);
|
||||
if (groups != groupStatuses_.end()) update.status = groups->second[target.value];
|
||||
if (states != states_.end()) {
|
||||
const uint16_t bit = static_cast<uint16_t>(1U << target.value);
|
||||
for (uint8_t address = 0; address < states->second.size(); ++address) {
|
||||
const auto& state = states->second[address];
|
||||
if (state.groupMaskKnown && (state.groupMask & bit) != 0) {
|
||||
update.affectedShortAddresses.push_back(address);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const auto broadcast = broadcastStatuses_.find(channel);
|
||||
if (broadcast != broadcastStatuses_.end()) update.status = broadcast->second;
|
||||
for (uint8_t address = 0; address < 64; ++address) {
|
||||
update.affectedShortAddresses.push_back(address);
|
||||
}
|
||||
}
|
||||
return update.status.anyKnown() ? std::optional<DaliGatewayStatusUpdate>(std::move(update))
|
||||
: std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::string> DaliGatewayCache::encodeAddressState(
|
||||
const DaliGatewayAddressState& state) {
|
||||
if (IsDefaultState(state)) return std::nullopt;
|
||||
std::string payload = std::to_string(kAddressStatePayloadVersion);
|
||||
payload += "," + std::to_string(StateFlags(state));
|
||||
payload += "," + std::to_string(state.status.revision);
|
||||
payload += "," + std::to_string(state.groupMask);
|
||||
payload += "," + std::to_string(state.status.actualLevel.value_or(0));
|
||||
payload += "," + std::to_string(state.status.sceneID.value_or(0));
|
||||
payload += "," + std::to_string(state.settings.powerOnLevel.value_or(0));
|
||||
payload += "," + std::to_string(state.settings.systemFailureLevel.value_or(0));
|
||||
payload += "," + std::to_string(state.settings.minLevel.value_or(0));
|
||||
payload += "," + std::to_string(state.settings.maxLevel.value_or(0));
|
||||
payload += "," + std::to_string(state.settings.fadeTime.value_or(0));
|
||||
payload += "," + std::to_string(state.settings.fadeRate.value_or(0));
|
||||
payload += "," + std::to_string(SceneKnownMask(state));
|
||||
for (const auto& level : state.sceneLevels) {
|
||||
payload += "," + std::to_string(level.value_or(255));
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
std::optional<DaliGatewayAddressState> DaliGatewayCache::decodeAddressState(
|
||||
std::string_view payload) {
|
||||
const auto values = ParseCsv(payload);
|
||||
if (values.size() < 13 || values[0] != kAddressStatePayloadVersion) return std::nullopt;
|
||||
|
||||
DaliGatewayAddressState state;
|
||||
const uint32_t flags = static_cast<uint32_t>(std::max(values[1], 0));
|
||||
state.groupMaskKnown = (flags & kGroupMaskKnown) != 0;
|
||||
state.groupMask = state.groupMaskKnown ? WordValue(values[3]) : 0;
|
||||
state.status.revision = static_cast<uint32_t>(std::max(values[2], 0));
|
||||
state.status.stale = (flags & kStatusStale) != 0;
|
||||
state.status.useMinLevel = (flags & kUseMinLevel) != 0;
|
||||
if ((flags & kActualLevelKnown) != 0) state.status.actualLevel = ByteValue(values[4]);
|
||||
if ((flags & kSceneKnown) != 0) {
|
||||
state.status.sceneID = static_cast<uint8_t>(std::min<int>(ByteValue(values[5]), 15));
|
||||
}
|
||||
if ((flags & kPowerOnKnown) != 0) state.settings.powerOnLevel = ByteValue(values[6]);
|
||||
if ((flags & kSystemFailureKnown) != 0) {
|
||||
state.settings.systemFailureLevel = ByteValue(values[7]);
|
||||
}
|
||||
if ((flags & kMinKnown) != 0) state.settings.minLevel = ByteValue(values[8]);
|
||||
if ((flags & kMaxKnown) != 0) state.settings.maxLevel = ByteValue(values[9]);
|
||||
if ((flags & kFadeTimeKnown) != 0) state.settings.fadeTime = ByteValue(values[10]);
|
||||
if ((flags & kFadeRateKnown) != 0) state.settings.fadeRate = ByteValue(values[11]);
|
||||
|
||||
const uint16_t knownScenes = WordValue(values[12]);
|
||||
for (uint8_t scene = 0; scene < state.sceneLevels.size(); ++scene) {
|
||||
const size_t valueIndex = 13 + scene;
|
||||
if ((knownScenes & (1U << scene)) != 0 && valueIndex < values.size()) {
|
||||
state.sceneLevels[scene] = ByteValue(values[valueIndex]);
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
bool DaliGatewayCache::preloadChannel(uint8_t channel) {
|
||||
DaliGatewayCachePersistenceCallbacks persistence;
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
persistence = persistence_;
|
||||
}
|
||||
if (!persistence.load) return false;
|
||||
|
||||
AddressStates restored{};
|
||||
for (uint8_t address = 0; address < restored.size(); ++address) {
|
||||
const auto payload = persistence.load(channel, address);
|
||||
if (!payload.has_value()) continue;
|
||||
const auto state = decodeAddressState(*payload);
|
||||
if (state.has_value()) restored[address] = *state;
|
||||
}
|
||||
restoreAddressStates(channel, restored);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DaliGatewayCache::flush() {
|
||||
DaliGatewayCachePersistenceCallbacks persistence;
|
||||
std::map<uint8_t, AddressStates> snapshots;
|
||||
std::map<uint8_t, uint64_t> generations;
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
if (dirtyGenerations_.empty()) return true;
|
||||
persistence = persistence_;
|
||||
if (!persistence.store || !persistence.commit) return false;
|
||||
for (const auto& [channel, generation] : dirtyGenerations_) {
|
||||
snapshots[channel] = ensureStates(channel);
|
||||
generations[channel] = generation;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& [channel, states] : snapshots) {
|
||||
for (uint8_t address = 0; address < states.size(); ++address) {
|
||||
if (!persistence.store(channel, address, encodeAddressState(states[address]))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!persistence.commit()) return false;
|
||||
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
for (const auto& [channel, generation] : generations) {
|
||||
const auto current = dirtyGenerations_.find(channel);
|
||||
if (current != dirtyGenerations_.end() && current->second == generation) {
|
||||
dirtyGenerations_.erase(current);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
#include "dali_gateway.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t kCmdOff = 0x00;
|
||||
constexpr uint8_t kCmdRecallMax = 0x05;
|
||||
constexpr uint8_t kCmdRecallMin = 0x06;
|
||||
constexpr uint8_t kCmdGoToSceneMin = 0x10;
|
||||
constexpr uint8_t kCmdGoToSceneMax = 0x1F;
|
||||
constexpr uint8_t kCmdReset = 0x20;
|
||||
constexpr uint8_t kCmdStoreDtrAsMax = 0x2A;
|
||||
constexpr uint8_t kCmdStoreDtrAsMin = 0x2B;
|
||||
constexpr uint8_t kCmdStoreDtrAsFailure = 0x2C;
|
||||
constexpr uint8_t kCmdStoreDtrAsPowerOn = 0x2D;
|
||||
constexpr uint8_t kCmdStoreDtrAsFadeTime = 0x2E;
|
||||
constexpr uint8_t kCmdStoreDtrAsFadeRate = 0x2F;
|
||||
constexpr uint8_t kCmdSetSceneMin = 0x40;
|
||||
constexpr uint8_t kCmdRemoveSceneMax = 0x5F;
|
||||
constexpr uint8_t kCmdAddToGroupMin = 0x60;
|
||||
constexpr uint8_t kCmdRemoveFromGroupMax = 0x7F;
|
||||
constexpr uint8_t kCmdSetDtr0 = 0xA3;
|
||||
constexpr uint8_t kCmdSetDtr1 = 0xC3;
|
||||
constexpr uint8_t kCmdSetDtr2 = 0xC5;
|
||||
|
||||
void ClearAddress(DaliGatewayAddressState& state) {
|
||||
state.groupMaskKnown = false;
|
||||
state.groupMask = 0;
|
||||
state.sceneLevels.fill(std::nullopt);
|
||||
state.settings = {};
|
||||
state.status = {};
|
||||
}
|
||||
|
||||
void ApplySettingsValue(DaliGatewaySettingsSnapshot& settings, uint8_t command, uint8_t value) {
|
||||
switch (command) {
|
||||
case kCmdStoreDtrAsMax:
|
||||
settings.maxLevel = value;
|
||||
break;
|
||||
case kCmdStoreDtrAsMin:
|
||||
settings.minLevel = value;
|
||||
break;
|
||||
case kCmdStoreDtrAsFailure:
|
||||
settings.systemFailureLevel = value;
|
||||
break;
|
||||
case kCmdStoreDtrAsPowerOn:
|
||||
settings.powerOnLevel = value;
|
||||
break;
|
||||
case kCmdStoreDtrAsFadeTime:
|
||||
settings.fadeTime = value;
|
||||
break;
|
||||
case kCmdStoreDtrAsFadeRate:
|
||||
settings.fadeRate = value;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void DaliGatewayCache::applyRuntimeStatusToAddress(DaliGatewayAddressState& address,
|
||||
const DaliGatewayRuntimeStatus& status) {
|
||||
if (!status.anyKnown() || status.revision <= address.status.revision) return;
|
||||
if (status.sceneID.has_value()) {
|
||||
address.status.sceneID = status.sceneID;
|
||||
address.status.useMinLevel = false;
|
||||
const uint8_t scene = *status.sceneID;
|
||||
if (scene < address.sceneLevels.size() && address.sceneLevels[scene].has_value() &&
|
||||
*address.sceneLevels[scene] != 255U) {
|
||||
address.status.actualLevel = *address.sceneLevels[scene];
|
||||
} else if (status.actualLevel.has_value()) {
|
||||
address.status.actualLevel = status.actualLevel;
|
||||
}
|
||||
} else {
|
||||
address.status.sceneID.reset();
|
||||
address.status.useMinLevel = status.useMinLevel;
|
||||
address.status.actualLevel = status.useMinLevel ? address.settings.minLevel : status.actualLevel;
|
||||
}
|
||||
address.status.revision = status.revision;
|
||||
address.status.stale = status.stale;
|
||||
}
|
||||
|
||||
void DaliGatewayCache::applyRuntimeStatus(uint8_t channel, const DaliGatewayTarget& target,
|
||||
const DaliGatewayRuntimeStatus& status) {
|
||||
if (!status.anyKnown()) return;
|
||||
auto& states = ensureStates(channel);
|
||||
switch (target.kind) {
|
||||
case DaliGatewayTargetKind::shortAddress:
|
||||
if (target.value < states.size()) applyRuntimeStatusToAddress(states[target.value], status);
|
||||
break;
|
||||
case DaliGatewayTargetKind::group: {
|
||||
if (target.value >= 16) break;
|
||||
ensureGroupStatuses(channel)[target.value] = status;
|
||||
const uint16_t bit = static_cast<uint16_t>(1U << target.value);
|
||||
for (auto& address : states) {
|
||||
if (address.groupMaskKnown && (address.groupMask & bit) != 0) {
|
||||
applyRuntimeStatusToAddress(address, status);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case DaliGatewayTargetKind::broadcast:
|
||||
ensureBroadcastStatus(channel) = status;
|
||||
for (auto& address : states) applyRuntimeStatusToAddress(address, status);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void DaliGatewayCache::refreshAggregateStatus(uint8_t channel, DaliGatewayAddressState& address) {
|
||||
if (!address.groupMaskKnown) return;
|
||||
if (const auto broadcast = broadcastStatuses_.find(channel);
|
||||
broadcast != broadcastStatuses_.end() && !broadcast->second.stale) {
|
||||
applyRuntimeStatusToAddress(address, broadcast->second);
|
||||
}
|
||||
const auto groups = groupStatuses_.find(channel);
|
||||
if (groups == groupStatuses_.end()) return;
|
||||
for (uint8_t group = 0; group < groups->second.size(); ++group) {
|
||||
if ((address.groupMask & static_cast<uint16_t>(1U << group)) != 0 &&
|
||||
!groups->second[group].stale) {
|
||||
applyRuntimeStatusToAddress(address, groups->second[group]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool DaliGatewayCache::setGroupMask(uint8_t channel, uint8_t shortAddress,
|
||||
std::optional<uint16_t> groupMask) {
|
||||
StatusUpdateCallback callback;
|
||||
std::optional<DaliGatewayStatusUpdate> update;
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
if (!config_.enabled || shortAddress >= 64) return false;
|
||||
auto& address = ensureStates(channel)[shortAddress];
|
||||
if (!groupMask.has_value()) {
|
||||
address.groupMaskKnown = false;
|
||||
address.groupMask = 0;
|
||||
} else {
|
||||
address.groupMaskKnown = true;
|
||||
address.groupMask = *groupMask;
|
||||
refreshAggregateStatus(channel, address);
|
||||
}
|
||||
markDirty(channel);
|
||||
update = statusUpdate(
|
||||
channel, DaliGatewayTarget{DaliGatewayTargetKind::shortAddress, shortAddress});
|
||||
callback = statusUpdateCallback_;
|
||||
}
|
||||
if (callback && update.has_value()) callback(*update);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DaliGatewayCache::setSceneLevel(uint8_t channel, uint8_t shortAddress, uint8_t scene,
|
||||
std::optional<uint8_t> level) {
|
||||
StatusUpdateCallback callback;
|
||||
std::optional<DaliGatewayStatusUpdate> update;
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
if (!config_.enabled || shortAddress >= 64 || scene >= 16) return false;
|
||||
auto& address = ensureStates(channel)[shortAddress];
|
||||
address.sceneLevels[scene] = level;
|
||||
if (address.status.sceneID.has_value() && *address.status.sceneID == scene &&
|
||||
level.has_value() && *level != 255U) {
|
||||
address.status.actualLevel = level;
|
||||
}
|
||||
markDirty(channel);
|
||||
update = statusUpdate(
|
||||
channel, DaliGatewayTarget{DaliGatewayTargetKind::shortAddress, shortAddress});
|
||||
callback = statusUpdateCallback_;
|
||||
}
|
||||
if (callback && update.has_value()) callback(*update);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DaliGatewayCache::setSettings(uint8_t channel, uint8_t shortAddress,
|
||||
std::optional<DaliGatewaySettingsSnapshot> settings) {
|
||||
StatusUpdateCallback callback;
|
||||
std::optional<DaliGatewayStatusUpdate> update;
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
if (!config_.enabled || shortAddress >= 64) return false;
|
||||
auto& address = ensureStates(channel)[shortAddress];
|
||||
address.settings = settings.value_or(DaliGatewaySettingsSnapshot{});
|
||||
if (address.status.useMinLevel) address.status.actualLevel = address.settings.minLevel;
|
||||
markDirty(channel);
|
||||
update = statusUpdate(
|
||||
channel, DaliGatewayTarget{DaliGatewayTargetKind::shortAddress, shortAddress});
|
||||
callback = statusUpdateCallback_;
|
||||
}
|
||||
if (callback && update.has_value()) callback(*update);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DaliGatewayCache::setActualLevel(uint8_t channel, uint8_t shortAddress,
|
||||
std::optional<uint8_t> level) {
|
||||
StatusUpdateCallback callback;
|
||||
std::optional<DaliGatewayStatusUpdate> update;
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
if (!config_.enabled || shortAddress >= 64) return false;
|
||||
auto& address = ensureStates(channel)[shortAddress];
|
||||
address.status.actualLevel = level;
|
||||
address.status.sceneID.reset();
|
||||
address.status.useMinLevel = false;
|
||||
address.status.stale = false;
|
||||
address.status.revision = nextRevision();
|
||||
markDirty(channel);
|
||||
update = statusUpdate(
|
||||
channel, DaliGatewayTarget{DaliGatewayTargetKind::shortAddress, shortAddress});
|
||||
callback = statusUpdateCallback_;
|
||||
}
|
||||
if (callback && update.has_value()) callback(*update);
|
||||
return true;
|
||||
}
|
||||
|
||||
void DaliGatewayCache::clearTarget(uint8_t channel, const DaliGatewayTarget& target,
|
||||
uint32_t revision) {
|
||||
auto& states = ensureStates(channel);
|
||||
const auto clear = [revision](DaliGatewayAddressState& address) {
|
||||
ClearAddress(address);
|
||||
address.status.revision = revision;
|
||||
};
|
||||
switch (target.kind) {
|
||||
case DaliGatewayTargetKind::shortAddress:
|
||||
if (target.value < states.size()) clear(states[target.value]);
|
||||
break;
|
||||
case DaliGatewayTargetKind::group: {
|
||||
if (target.value >= 16) break;
|
||||
ensureGroupStatuses(channel)[target.value] = DaliGatewayRuntimeStatus{};
|
||||
ensureGroupStatuses(channel)[target.value].revision = revision;
|
||||
const uint16_t bit = static_cast<uint16_t>(1U << target.value);
|
||||
for (auto& address : states) {
|
||||
if (address.groupMaskKnown && (address.groupMask & bit) != 0) clear(address);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case DaliGatewayTargetKind::broadcast:
|
||||
ensureBroadcastStatus(channel) = DaliGatewayRuntimeStatus{};
|
||||
ensureBroadcastStatus(channel).revision = revision;
|
||||
for (auto& status : ensureGroupStatuses(channel)) status = DaliGatewayRuntimeStatus{};
|
||||
for (auto& address : states) clear(address);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void DaliGatewayCache::applyGroupMutation(uint8_t channel, const DaliGatewayTarget& target,
|
||||
uint8_t group, bool add) {
|
||||
if (group >= 16) return;
|
||||
auto& states = ensureStates(channel);
|
||||
const uint16_t bit = static_cast<uint16_t>(1U << group);
|
||||
const auto apply = [&](DaliGatewayAddressState& address) {
|
||||
if (!address.groupMaskKnown) return;
|
||||
address.groupMask = add ? static_cast<uint16_t>(address.groupMask | bit)
|
||||
: static_cast<uint16_t>(address.groupMask & ~bit);
|
||||
refreshAggregateStatus(channel, address);
|
||||
};
|
||||
if (target.kind == DaliGatewayTargetKind::shortAddress && target.value < states.size()) {
|
||||
apply(states[target.value]);
|
||||
} else if (target.kind == DaliGatewayTargetKind::group && target.value < 16) {
|
||||
const uint16_t targetBit = static_cast<uint16_t>(1U << target.value);
|
||||
for (auto& address : states) {
|
||||
if (address.groupMaskKnown && (address.groupMask & targetBit) != 0) apply(address);
|
||||
}
|
||||
} else if (target.kind == DaliGatewayTargetKind::broadcast) {
|
||||
for (auto& address : states) apply(address);
|
||||
}
|
||||
}
|
||||
|
||||
void DaliGatewayCache::applySceneMutation(uint8_t channel, const DaliGatewayTarget& target,
|
||||
uint8_t scene, std::optional<uint8_t> level) {
|
||||
if (scene >= 16) return;
|
||||
auto& states = ensureStates(channel);
|
||||
const auto apply = [&](DaliGatewayAddressState& address) {
|
||||
address.sceneLevels[scene] = level;
|
||||
if (address.status.sceneID.has_value() && *address.status.sceneID == scene &&
|
||||
level.has_value() && *level != 255U) {
|
||||
address.status.actualLevel = level;
|
||||
}
|
||||
};
|
||||
if (target.kind == DaliGatewayTargetKind::shortAddress && target.value < states.size()) {
|
||||
apply(states[target.value]);
|
||||
} else if (target.kind == DaliGatewayTargetKind::group && target.value < 16) {
|
||||
const uint16_t bit = static_cast<uint16_t>(1U << target.value);
|
||||
for (auto& address : states) {
|
||||
if (address.groupMaskKnown && (address.groupMask & bit) != 0) apply(address);
|
||||
}
|
||||
} else if (target.kind == DaliGatewayTargetKind::broadcast) {
|
||||
for (auto& address : states) apply(address);
|
||||
}
|
||||
}
|
||||
|
||||
void DaliGatewayCache::applySettingsMutation(uint8_t channel, const DaliGatewayTarget& target,
|
||||
uint8_t command, uint8_t value) {
|
||||
auto& states = ensureStates(channel);
|
||||
const auto apply = [&](DaliGatewayAddressState& address) {
|
||||
ApplySettingsValue(address.settings, command, value);
|
||||
if (command == kCmdStoreDtrAsMin && address.status.useMinLevel) {
|
||||
address.status.actualLevel = value;
|
||||
}
|
||||
};
|
||||
if (target.kind == DaliGatewayTargetKind::shortAddress && target.value < states.size()) {
|
||||
apply(states[target.value]);
|
||||
} else if (target.kind == DaliGatewayTargetKind::group && target.value < 16) {
|
||||
const uint16_t bit = static_cast<uint16_t>(1U << target.value);
|
||||
for (auto& address : states) {
|
||||
if (address.groupMaskKnown && (address.groupMask & bit) != 0) apply(address);
|
||||
}
|
||||
} else if (target.kind == DaliGatewayTargetKind::broadcast) {
|
||||
for (auto& address : states) apply(address);
|
||||
}
|
||||
}
|
||||
|
||||
bool DaliGatewayCache::mirrorForwardFrameLocked(
|
||||
uint8_t channel, uint8_t rawAddress, uint8_t command,
|
||||
std::optional<DaliGatewayStatusUpdate>* statusUpdateResult) {
|
||||
if (!config_.enabled) return false;
|
||||
auto& dtr = dtrStates_[channel];
|
||||
if (rawAddress == kCmdSetDtr0) {
|
||||
dtr.dtr0 = command;
|
||||
return false;
|
||||
}
|
||||
if (rawAddress == kCmdSetDtr1) {
|
||||
dtr.dtr1 = command;
|
||||
return false;
|
||||
}
|
||||
if (rawAddress == kCmdSetDtr2) {
|
||||
dtr.dtr2 = command;
|
||||
return false;
|
||||
}
|
||||
const auto target = decodeTarget(rawAddress);
|
||||
if (!target.has_value()) return false;
|
||||
const auto finish = [&]() {
|
||||
markDirty(channel);
|
||||
if (statusUpdateResult != nullptr) {
|
||||
*statusUpdateResult = statusUpdate(channel, *target);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
if ((rawAddress & 1U) == 0) {
|
||||
if (command > 254) return false;
|
||||
DaliGatewayRuntimeStatus status;
|
||||
status.actualLevel = command;
|
||||
status.revision = nextRevision();
|
||||
applyRuntimeStatus(channel, *target, status);
|
||||
return finish();
|
||||
}
|
||||
if (command == kCmdReset) {
|
||||
clearTarget(channel, *target, nextRevision());
|
||||
return finish();
|
||||
}
|
||||
if (command == kCmdOff || command == kCmdRecallMax) {
|
||||
DaliGatewayRuntimeStatus status;
|
||||
status.actualLevel = command == kCmdOff ? 0 : 254;
|
||||
status.revision = nextRevision();
|
||||
applyRuntimeStatus(channel, *target, status);
|
||||
return finish();
|
||||
}
|
||||
if (command == kCmdRecallMin) {
|
||||
DaliGatewayRuntimeStatus status;
|
||||
status.useMinLevel = true;
|
||||
status.revision = nextRevision();
|
||||
applyRuntimeStatus(channel, *target, status);
|
||||
return finish();
|
||||
}
|
||||
if (command >= kCmdGoToSceneMin && command <= kCmdGoToSceneMax) {
|
||||
DaliGatewayRuntimeStatus status;
|
||||
status.sceneID = static_cast<uint8_t>(command - kCmdGoToSceneMin);
|
||||
status.revision = nextRevision();
|
||||
applyRuntimeStatus(channel, *target, status);
|
||||
return finish();
|
||||
}
|
||||
if (command >= kCmdAddToGroupMin && command <= kCmdRemoveFromGroupMax) {
|
||||
applyGroupMutation(channel, *target, command & 0x0F, command < kCmdAddToGroupMin + 16);
|
||||
return finish();
|
||||
}
|
||||
if (command >= kCmdSetSceneMin && command < kCmdSetSceneMin + 16 && dtr.dtr0.has_value()) {
|
||||
applySceneMutation(channel, *target, command - kCmdSetSceneMin, dtr.dtr0);
|
||||
return finish();
|
||||
}
|
||||
if (command >= kCmdSetSceneMin + 16 && command <= kCmdRemoveSceneMax) {
|
||||
applySceneMutation(channel, *target, command - (kCmdSetSceneMin + 16),
|
||||
static_cast<uint8_t>(255));
|
||||
return finish();
|
||||
}
|
||||
if (command >= kCmdStoreDtrAsMax && command <= kCmdStoreDtrAsFadeRate &&
|
||||
dtr.dtr0.has_value()) {
|
||||
applySettingsMutation(channel, *target, command, *dtr.dtr0);
|
||||
return finish();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DaliGatewayCache::mirrorForwardFrame(uint8_t channel, uint8_t rawAddress,
|
||||
uint8_t command) {
|
||||
StatusUpdateCallback callback;
|
||||
std::optional<DaliGatewayStatusUpdate> update;
|
||||
bool changed = false;
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
changed = mirrorForwardFrameLocked(channel, rawAddress, command, &update);
|
||||
callback = statusUpdateCallback_;
|
||||
}
|
||||
if (callback && update.has_value()) callback(*update);
|
||||
return changed;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
#include "dali_gateway.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t kCmdOff = 0x00;
|
||||
constexpr uint8_t kCmdRecallMax = 0x05;
|
||||
constexpr uint8_t kCmdRecallMin = 0x06;
|
||||
constexpr uint8_t kCmdGoToSceneMin = 0x10;
|
||||
constexpr uint8_t kCmdGoToSceneMax = 0x1F;
|
||||
constexpr uint8_t kCmdReset = 0x20;
|
||||
constexpr uint8_t kCmdStoreDtrAsMax = 0x2A;
|
||||
constexpr uint8_t kCmdStoreDtrAsFadeRate = 0x2F;
|
||||
constexpr uint8_t kCmdSetSceneMin = 0x40;
|
||||
constexpr uint8_t kCmdRemoveSceneMax = 0x5F;
|
||||
constexpr uint8_t kCmdAddToGroupMin = 0x60;
|
||||
constexpr uint8_t kCmdRemoveFromGroupMax = 0x7F;
|
||||
constexpr uint8_t kCmdProgramShortAddress = 0xB7;
|
||||
constexpr uint8_t kCmdDt8StoreColorX = 0xE0;
|
||||
constexpr uint8_t kCmdDt8StoreColorY = 0xE1;
|
||||
constexpr uint8_t kCmdDt8StorePrimaryMin = 0xF0;
|
||||
constexpr uint8_t kCmdDt8StartCalibration = 0xF6;
|
||||
|
||||
DaliGatewayChannelFlags ClassifyMutation(uint8_t rawAddress, uint8_t command) {
|
||||
DaliGatewayChannelFlags flags;
|
||||
if (rawAddress == kCmdProgramShortAddress) {
|
||||
flags.needUpdateSettings = true;
|
||||
return flags;
|
||||
}
|
||||
const bool special = rawAddress >= 0xA1 && rawAddress <= 0xC5 && (rawAddress & 1U) != 0;
|
||||
if (special || (rawAddress & 1U) == 0) return flags;
|
||||
if (command == kCmdReset) {
|
||||
flags = {true, true, true};
|
||||
} else if (command >= kCmdStoreDtrAsMax && command <= kCmdStoreDtrAsFadeRate) {
|
||||
flags.needUpdateSettings = true;
|
||||
} else if (command >= kCmdSetSceneMin && command <= kCmdRemoveSceneMax) {
|
||||
flags.needUpdateScene = true;
|
||||
} else if (command >= kCmdAddToGroupMin && command <= kCmdRemoveFromGroupMax) {
|
||||
flags.needUpdateGroup = true;
|
||||
} else if (command == 0x80 || command == kCmdDt8StoreColorX || command == kCmdDt8StoreColorY ||
|
||||
(command >= kCmdDt8StorePrimaryMin && command <= kCmdDt8StartCalibration)) {
|
||||
flags.needUpdateSettings = true;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
bool SameFlags(const DaliGatewayChannelFlags& lhs, const DaliGatewayChannelFlags& rhs) {
|
||||
return lhs.needUpdateGroup == rhs.needUpdateGroup &&
|
||||
lhs.needUpdateScene == rhs.needUpdateScene &&
|
||||
lhs.needUpdateSettings == rhs.needUpdateSettings;
|
||||
}
|
||||
|
||||
bool IsStatusFeedback(uint8_t rawAddress, uint8_t command) {
|
||||
if (!DaliGatewayCache::decodeTarget(rawAddress).has_value()) return false;
|
||||
if ((rawAddress & 1U) == 0) return command <= 254;
|
||||
return command == kCmdOff || command == kCmdRecallMax || command == kCmdRecallMin ||
|
||||
(command >= kCmdGoToSceneMin && command <= kCmdGoToSceneMax);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool DaliGatewayCache::observeForwardFrame(uint8_t channel, uint8_t rawAddress, uint8_t command,
|
||||
DaliGatewayFrameOrigin origin) {
|
||||
StatusUpdateCallback callback;
|
||||
std::optional<DaliGatewayStatusUpdate> update;
|
||||
bool flagged = false;
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
if (!config_.enabled || !config_.reconciliationEnabled) return false;
|
||||
const auto mutation = ClassifyMutation(rawAddress, command);
|
||||
const bool mirrorMutation =
|
||||
origin == DaliGatewayFrameOrigin::localGateway ||
|
||||
config_.priorityMode == DaliGatewayCachePriorityMode::outsideBusFirst;
|
||||
if (mirrorMutation || IsStatusFeedback(rawAddress, command)) {
|
||||
mirrorForwardFrameLocked(channel, rawAddress, command, &update);
|
||||
}
|
||||
if (mutation.any()) {
|
||||
auto& flags = flags_[channel];
|
||||
flags.needUpdateGroup = flags.needUpdateGroup || mutation.needUpdateGroup;
|
||||
flags.needUpdateScene = flags.needUpdateScene || mutation.needUpdateScene;
|
||||
flags.needUpdateSettings = flags.needUpdateSettings || mutation.needUpdateSettings;
|
||||
flagged = true;
|
||||
}
|
||||
callback = statusUpdateCallback_;
|
||||
}
|
||||
if (callback && update.has_value()) callback(*update);
|
||||
return flagged;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> DaliGatewayCache::reconciliationAddresses(
|
||||
uint8_t channel, std::optional<DaliGatewayTarget> target) const {
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
std::vector<uint8_t> result;
|
||||
const auto stateIt = states_.find(channel);
|
||||
const auto presenceIt = presence_.find(channel);
|
||||
const auto include = [&](uint8_t address) {
|
||||
const DaliGatewayPresence presence =
|
||||
presenceIt == presence_.end() ? DaliGatewayPresence::unknown : presenceIt->second[address];
|
||||
if (!target.has_value()) return presence == DaliGatewayPresence::online;
|
||||
if (target->kind == DaliGatewayTargetKind::shortAddress) {
|
||||
return target->value == address && presence != DaliGatewayPresence::offline;
|
||||
}
|
||||
if (target->kind == DaliGatewayTargetKind::broadcast) {
|
||||
return presence == DaliGatewayPresence::online;
|
||||
}
|
||||
if (target->kind == DaliGatewayTargetKind::group && target->value < 16 &&
|
||||
stateIt != states_.end()) {
|
||||
const auto& state = stateIt->second[address];
|
||||
return presence == DaliGatewayPresence::online && state.groupMaskKnown &&
|
||||
(state.groupMask & static_cast<uint16_t>(1U << target->value)) != 0;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
for (uint8_t address = 0; address < 64; ++address) {
|
||||
if (include(address)) result.push_back(address);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool DaliGatewayCache::clearChannelFlagsIfMatched(uint8_t channel,
|
||||
const DaliGatewayChannelFlags& expected) {
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
auto it = flags_.find(channel);
|
||||
if (it == flags_.end() || !SameFlags(it->second, expected)) return false;
|
||||
it->second = {};
|
||||
return true;
|
||||
}
|
||||
|
||||
void DaliGatewayCache::markGroupUpdateNeeded(uint8_t channel, bool needed) {
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
flags_[channel].needUpdateGroup = needed;
|
||||
}
|
||||
|
||||
void DaliGatewayCache::markSceneUpdateNeeded(uint8_t channel, bool needed) {
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
flags_[channel].needUpdateScene = needed;
|
||||
}
|
||||
|
||||
void DaliGatewayCache::markSettingsUpdateNeeded(uint8_t channel, bool needed) {
|
||||
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
||||
flags_[channel].needUpdateSettings = needed;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#include "dali_gateway.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t kRawBroadcastArc = 0xFE;
|
||||
constexpr uint8_t kRawBroadcastCommand = 0xFF;
|
||||
constexpr uint8_t kRawGroupMin = 0x80;
|
||||
constexpr uint8_t kRawGroupMax = 0x9F;
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<DaliGatewayTarget> DaliGatewayCache::decodeTarget(uint8_t rawAddress) {
|
||||
if (rawAddress < 0x80) {
|
||||
return DaliGatewayTarget{DaliGatewayTargetKind::shortAddress,
|
||||
static_cast<uint8_t>(rawAddress >> 1)};
|
||||
}
|
||||
if (rawAddress >= kRawGroupMin && rawAddress <= kRawGroupMax) {
|
||||
return DaliGatewayTarget{DaliGatewayTargetKind::group,
|
||||
static_cast<uint8_t>((rawAddress - kRawGroupMin) >> 1)};
|
||||
}
|
||||
if (rawAddress == kRawBroadcastArc || rawAddress == kRawBroadcastCommand) {
|
||||
return DaliGatewayTarget{DaliGatewayTargetKind::broadcast, 0};
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
#include "dali_gateway.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <map>
|
||||
#include <utility>
|
||||
|
||||
int main() {
|
||||
DaliGatewayCache cache;
|
||||
std::map<std::pair<uint8_t, uint8_t>, std::string> persisted;
|
||||
int commits = 0;
|
||||
cache.setPersistenceCallbacks({
|
||||
[&](uint8_t channel, uint8_t address) -> std::optional<std::string> {
|
||||
const auto it = persisted.find({channel, address});
|
||||
return it == persisted.end() ? std::nullopt
|
||||
: std::optional<std::string>(it->second);
|
||||
},
|
||||
[&](uint8_t channel, uint8_t address, const std::optional<std::string>& payload) {
|
||||
if (payload.has_value()) {
|
||||
persisted[{channel, address}] = *payload;
|
||||
} else {
|
||||
persisted.erase({channel, address});
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[&]() {
|
||||
++commits;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
int status_updates = 0;
|
||||
DaliGatewayStatusUpdate last_update;
|
||||
cache.setStatusUpdateCallback([&](const DaliGatewayStatusUpdate& update) {
|
||||
++status_updates;
|
||||
last_update = update;
|
||||
});
|
||||
|
||||
assert(cache.setGroupMask(7, 4, 0x0001));
|
||||
assert(cache.mirrorForwardFrame(7, 0x80, 42));
|
||||
assert(status_updates == 1);
|
||||
assert(last_update.channel == 7);
|
||||
assert(last_update.target.kind == DaliGatewayTargetKind::group);
|
||||
assert(last_update.target.value == 0);
|
||||
assert(last_update.status.actualLevel == 42);
|
||||
assert(last_update.affectedShortAddresses == std::vector<uint8_t>{4});
|
||||
assert(cache.groupStatus(7, 0).actualLevel == 42);
|
||||
assert(cache.addressState(7, 4).status.actualLevel == 42);
|
||||
|
||||
// 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);
|
||||
assert(cache.mirrorForwardFrame(7, 0x09, 0x2B));
|
||||
assert(cache.addressState(7, 4).settings.minLevel == 12);
|
||||
assert(cache.mirrorForwardFrame(7, 0xFF, 0x06));
|
||||
assert(cache.addressState(7, 4).status.actualLevel == 12);
|
||||
|
||||
assert(cache.observeForwardFrame(7, 0x09, 0x60, DaliGatewayFrameOrigin::outsideBus));
|
||||
assert(cache.pendingChannelFlags(7).needUpdateGroup);
|
||||
assert(cache.flush());
|
||||
assert(commits == 1);
|
||||
assert(!persisted.empty());
|
||||
|
||||
DaliGatewayCache restored;
|
||||
restored.setPersistenceCallbacks({
|
||||
[&](uint8_t channel, uint8_t address) -> std::optional<std::string> {
|
||||
const auto it = persisted.find({channel, address});
|
||||
return it == persisted.end() ? std::nullopt
|
||||
: std::optional<std::string>(it->second);
|
||||
},
|
||||
{},
|
||||
{},
|
||||
});
|
||||
assert(restored.preloadChannel(7));
|
||||
const auto restored_state = restored.addressState(7, 4);
|
||||
assert(restored_state.groupMaskKnown);
|
||||
assert(restored_state.groupMask == 0x0001);
|
||||
assert(restored_state.settings.minLevel == 12);
|
||||
assert(restored_state.status.stale);
|
||||
|
||||
DaliApplicationControllerConfig config;
|
||||
config.shortAddress = 3;
|
||||
config.randomAddress = 0x123456;
|
||||
DaliApplicationController controller(config);
|
||||
|
||||
const auto capabilities = controller.handleForwardFrame({0x07, 0xFE, 0x46});
|
||||
assert(capabilities.backwardFrame == 0x01);
|
||||
const auto status = controller.handleForwardFrame({0x07, 0xFE, 0x30});
|
||||
assert(status.backwardFrame == 0x28);
|
||||
|
||||
// Commission short address 5 by selecting the controller's random address.
|
||||
controller.handleForwardFrame({0xC1, 0x01, 0xFF}, true);
|
||||
controller.handleForwardFrame({0xC1, 0x05, 0x12});
|
||||
controller.handleForwardFrame({0xC1, 0x06, 0x34});
|
||||
controller.handleForwardFrame({0xC1, 0x07, 0x56});
|
||||
const auto compared = controller.handleForwardFrame({0xC1, 0x03, 0x00});
|
||||
assert(compared.backwardFrame == 0xFF);
|
||||
const auto programmed = controller.handleForwardFrame({0xC1, 0x08, 0x0B});
|
||||
assert(programmed.shortAddressChanged);
|
||||
assert(programmed.shortAddress == 5);
|
||||
const auto verified = controller.handleForwardFrame({0xC1, 0x09, 0x0B});
|
||||
assert(verified.backwardFrame == 0xFF);
|
||||
controller.handleForwardFrame({0xC1, 0x30, 0x5A});
|
||||
const auto dtr0 = controller.handleForwardFrame({0x0B, 0xFE, 0x36});
|
||||
assert(dtr0.backwardFrame == 0x5A);
|
||||
|
||||
const auto before_repeat = controller.handleForwardFrame({0x0B, 0xFE, 0x00});
|
||||
assert(!before_repeat.identifyRequested);
|
||||
const auto identity = controller.handleForwardFrame({0x0B, 0xFE, 0x00}, true);
|
||||
assert(identity.identifyRequested);
|
||||
|
||||
DaliApplicationControllerConfig unaddressed_config;
|
||||
unaddressed_config.shortAddress.reset();
|
||||
DaliApplicationController unaddressed(unaddressed_config);
|
||||
const auto missing_short_address = unaddressed.handleForwardFrame({0xFF, 0xFE, 0x33});
|
||||
assert(missing_short_address.backwardFrame == 0xFF);
|
||||
const auto unaddressed_status = unaddressed.handleForwardFrame({0xFF, 0xFE, 0x30});
|
||||
assert(unaddressed_status.backwardFrame == 0x2C);
|
||||
}
|
||||
Reference in New Issue
Block a user