c5a079e779
- 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>
333 lines
11 KiB
Markdown
333 lines
11 KiB
Markdown
# DALI C++ Library
|
|
|
|
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
|
|
|
|
1. Add the component to your ESP-IDF project (e.g., via `EXTRA_COMPONENT_DIRS`).
|
|
2. Provide UART callbacks when constructing `DaliComm`:
|
|
|
|
```cpp
|
|
DaliComm comm(
|
|
/* send */ [](const uint8_t* data, size_t len) {
|
|
// write bytes to the gateway UART
|
|
return my_uart_write(data, len) == ESP_OK;
|
|
},
|
|
/* read (optional) */ [](size_t len, uint32_t timeoutMs) -> std::vector<uint8_t> {
|
|
return my_uart_read(len, timeoutMs);
|
|
},
|
|
/* transact */ [](const uint8_t* data, size_t len) -> std::vector<uint8_t> {
|
|
my_uart_write(data, len);
|
|
return my_uart_read_response(); // should return the raw gateway reply
|
|
});
|
|
Dali dali(comm);
|
|
```
|
|
|
|
3. Use the API just like the Dart version:
|
|
|
|
```cpp
|
|
dali.base.setBright(5, 200); // direct arc power control
|
|
dali.base.setBright(5, 128, true); // direct arc power with logarithmic curve
|
|
dali.base.off(5);
|
|
dali.base.dtSelect(8);
|
|
dali.dt8.setColorTemperature(5, 4000); // Kelvin
|
|
std::vector<int> rgb = dali.dt8.getColourRGB(5);
|
|
```
|
|
|
|
## Behaviour Parity
|
|
|
|
- Frame formats match the Dart implementation: `[0x10, addr, cmd]` (send), `[0x11, addr, cmd]` (extended), `[0x12, addr, cmd]` (query with `[0xFF, data]` response).
|
|
- Address encoding matches Dart helpers: `dec*2` for direct arc, `dec*2+1` for command/query addresses.
|
|
- Colour conversion utilities (`rgb2xy`, `xy2rgb`, XYZ/LAB helpers) are ported from `lib/dali/color.dart`.
|
|
- Public APIs from `base.dart`, `dt8.dart`, `dt1.dart`, `addr.dart`, and `decode.dart` are exposed with matching method names.
|
|
- App-side model parity modules are included for `device.dart`, `sequence.dart`, and `sequence_store.dart` via `device.hpp`, `sequence.hpp`, and `sequence_store.hpp`.
|
|
- Utility APIs from `errors.dart`, `log.dart`, `query_scheduler.dart`, and `bus_monitor.dart` are available as embedded-friendly C++ headers.
|
|
|
|
## Generic Bridge Layer
|
|
|
|
The component now includes a protocol-agnostic bridge layer for mapping external fieldbus models into DALI operations.
|
|
|
|
### Bridge Building Blocks
|
|
|
|
- `bridge_model.hpp` defines the strongly typed mapping model: protocol kind, external point, DALI target, default operation, and value transform.
|
|
- `bridge.hpp` provides `DaliBridgeEngine`, which resolves models and dispatches requests into `DaliComm`, `DaliBase`, and `DaliDT8`.
|
|
- `bridge_provisioning.hpp` provides `BridgeProvisioningStore` for persisting bridge models in ESP-IDF NVS.
|
|
- Modbus and BACnet runtime support are owned by the native gateway project in `gateway/components/gateway_modbus` and `gateway/components/gateway_bacnet`.
|
|
|
|
### Example Model Mapping
|
|
|
|
```cpp
|
|
BridgeModel brightness;
|
|
brightness.id = "line-1-brightness";
|
|
brightness.name = "Line 1 brightness";
|
|
brightness.dali.shortAddress = 1;
|
|
brightness.operation = BridgeOperation::setBrightness;
|
|
brightness.valueTransform.clampMin = 0;
|
|
brightness.valueTransform.clampMax = 254;
|
|
|
|
DaliBridgeEngine engine(comm);
|
|
engine.upsertModel(brightness);
|
|
|
|
DaliBridgeRequest request;
|
|
request.modelID = "line-1-brightness";
|
|
request.value = 180;
|
|
engine.execute(request);
|
|
|
|
request.value = DaliValue::Object{{"value", 128}, {"logarithmicCurve", true}};
|
|
engine.execute(request); // set_brightness with app-side logarithmic curve
|
|
```
|
|
|
|
### Supported Bridge Operations
|
|
|
|
- `send`
|
|
- `send_ext`
|
|
- `query`
|
|
- `set_brightness`
|
|
- `set_brightness_percent`
|
|
- `on`
|
|
- `off`
|
|
- `recall_max_level`
|
|
- `recall_min_level`
|
|
- `set_color_temperature`
|
|
- `get_brightness`
|
|
- `get_status`
|
|
- `get_color_temperature`
|
|
- `get_color_status`
|
|
- `get_emergency_level`
|
|
- `get_emergency_status`
|
|
- `get_emergency_failure_status`
|
|
- `start_emergency_function_test`
|
|
- `stop_emergency_test`
|
|
|
|
`set_brightness` and `set_brightness_percent` accept optional
|
|
`logarithmicCurve` / `logarithmic_curve` boolean request fields.
|
|
|
|
Query-style operations return `data` when available and may include decoded flags in `meta`.
|
|
|
|
## Notes
|
|
|
|
- Query support works with either a `read` callback or a `transact` callback. When `read` is available, queries return as soon as the reply bytes arrive instead of waiting behind a fixed pre-read delay.
|
|
- `Dali` facade in `include/dali.hpp` mirrors `lib/dali/dali.dart` and wires `base`, `decode`, `dt1`, `dt8`, and `addr` together.
|
|
- The `t`, `d`, and `g` parameters in Dart are not required here; timing/gateway selection is driven by your callbacks.
|
|
- `DaliCloudBridge` now uses the shared bridge engine internally, so MQTT downlinks can target either raw DALI frames or registered bridge models.
|
|
|
|
## Bridge Provisioning via NVS
|
|
|
|
Use `BridgeProvisioningStore` to persist bridge models:
|
|
|
|
```cpp
|
|
BridgeRuntimeConfig runtime;
|
|
runtime.models.push_back(brightness);
|
|
|
|
BridgeProvisioningStore store;
|
|
store.save(runtime);
|
|
|
|
BridgeRuntimeConfig loaded;
|
|
if (store.load(&loaded) == ESP_OK) {
|
|
// register loaded.models with DaliBridgeEngine
|
|
}
|
|
```
|
|
|
|
The gateway project stores Modbus TCP settings in the same persisted JSON shape, but parses and applies that section in `gateway/components/gateway_modbus` rather than in this standalone DALI component.
|
|
|
|
## Cloud Bridge (ESP32 Gateway)
|
|
|
|
The component now includes `DaliCloudBridge` in `include/gateway_cloud.hpp` to connect ESP32 gateways to the backend MQTT broker.
|
|
|
|
### Topics
|
|
|
|
- Downlink: `devices/<deviceID>/down`
|
|
- Uplink: `devices/<deviceID>/up`
|
|
- Status: `devices/<deviceID>/status`
|
|
- Register: `devices/<deviceID>/register`
|
|
|
|
### Downlink JSON Envelope
|
|
|
|
```json
|
|
{
|
|
"type": "dali_cmd",
|
|
"seq": "123",
|
|
"model": "modbus-light-1",
|
|
"op": "send|send_ext|query",
|
|
"addr": 5,
|
|
"cmd": 160,
|
|
"shortAddress": 1,
|
|
"value": 180
|
|
}
|
|
```
|
|
|
|
`model`, `shortAddress`, and `value` are optional. If `model` is provided, the bridge resolves its mapped DALI target and default operation.
|
|
|
|
Successful query responses may also include a `meta` object with decoded status flags.
|
|
|
|
### Uplink JSON Envelope
|
|
|
|
```json
|
|
{
|
|
"type": "dali_resp",
|
|
"seq": "123",
|
|
"op": "query",
|
|
"ok": true,
|
|
"data": 255
|
|
}
|
|
```
|
|
|
|
### Usage
|
|
|
|
```cpp
|
|
GatewayCloudConfig cfg;
|
|
cfg.brokerURI = "mqtt://192.168.1.100:1883";
|
|
cfg.deviceID = "A1B2C3D4E5F6";
|
|
cfg.username = "device";
|
|
cfg.password = "A1B2C3D4E5F6";
|
|
|
|
DaliCloudBridge bridge(comm);
|
|
if (bridge.start(cfg)) {
|
|
bridge.publishStatus("online");
|
|
}
|
|
```
|
|
|
|
### Provisioning via NVS
|
|
|
|
Use `GatewayProvisioningStore` to persist cloud connection settings:
|
|
|
|
```cpp
|
|
GatewayProvisioningStore store;
|
|
GatewayCloudConfig cfg;
|
|
cfg.brokerURI = "mqtt://192.168.1.100:1883";
|
|
cfg.deviceID = "A1B2C3D4E5F6";
|
|
cfg.username = "device";
|
|
cfg.password = "A1B2C3D4E5F6";
|
|
|
|
store.save(cfg);
|
|
|
|
GatewayCloudConfig loaded;
|
|
if (store.load(&loaded) == ESP_OK) {
|
|
DaliCloudBridge bridge(comm);
|
|
bridge.start(loaded);
|
|
}
|
|
```
|
|
|
|
## ESP32-S3 Example Project
|
|
|
|
A standalone ESP-IDF example app is available at `examples/esp32s3_bridge/`.
|
|
|
|
### Environment Setup
|
|
|
|
Source the helper script from your shell so the exported ESP-IDF variables stay in your session:
|
|
|
|
```bash
|
|
. ./scripts/export_esp_idf.sh
|
|
```
|
|
|
|
The helper script sources the installed ESP-IDF v5.5.4 `export.sh`.
|
|
|
|
### Build the Example
|
|
|
|
```bash
|
|
cd examples/esp32s3_bridge
|
|
idf.py set-target esp32s3
|
|
idf.py build
|
|
```
|
|
|
|
The example persists its bridge config in NVS, registers generic bridge models, and routes requests through the shared bridge engine. The DALI gateway callbacks are still placeholders where you should connect your UART or transport driver. Use `gateway/apps/gateway` to exercise the gateway-owned Modbus and BACnet runtimes.
|