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:
Tony
2026-08-01 01:52:12 +08:00
parent d49f580ab6
commit c5a079e779
11 changed files with 1748 additions and 35 deletions
+58 -29
View File
@@ -1,31 +1,60 @@
idf_component_register(
SRCS
"src/dali_comm.cpp"
"src/base.cpp"
"src/addr.cpp"
"src/bridge.cpp"
"src/bridge_model.cpp"
"src/bridge_provisioning.cpp"
"src/decode.cpp"
"src/decode_device_type.cpp"
"src/decode_dt1.cpp"
"src/decode_dt4.cpp"
"src/decode_dt5.cpp"
"src/decode_dt6.cpp"
"src/decode_dt8.cpp"
"src/device.cpp"
"src/dt1.cpp"
"src/dt4.cpp"
"src/dt5.cpp"
"src/dt6.cpp"
"src/dt8.cpp"
"src/sequence.cpp"
"src/sequence_store.cpp"
"src/color.cpp"
"src/gateway_cloud.cpp"
"src/gateway_provisioning.cpp"
INCLUDE_DIRS "include"
REQUIRES mqtt cjson nvs_flash esp_driver_uart
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"
)
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
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"
"src/bridge_model.cpp"
"src/bridge_provisioning.cpp"
"src/decode.cpp"
"src/decode_device_type.cpp"
"src/decode_dt1.cpp"
"src/decode_dt4.cpp"
"src/decode_dt5.cpp"
"src/decode_dt6.cpp"
"src/decode_dt8.cpp"
"src/device.cpp"
"src/dt1.cpp"
"src/dt4.cpp"
"src/dt5.cpp"
"src/dt6.cpp"
"src/dt8.cpp"
"src/sequence.cpp"
"src/sequence_store.cpp"
"src/color.cpp"
"src/gateway_cloud.cpp"
"src/gateway_provisioning.cpp"
INCLUDE_DIRS "include"
REQUIRES mqtt cjson nvs_flash esp_driver_uart
)
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()
+102 -3
View File
@@ -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
View File
@@ -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:
+272
View File
@@ -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 -1
View File
@@ -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
+266
View File
@@ -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;
}
+359
View File
@@ -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;
}
+401
View File
@@ -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;
}
+141
View File
@@ -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;
}
+25
View File
@@ -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;
}
+119
View File
@@ -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);
}