initial commit

This commit is contained in:
Tony
2026-03-26 12:04:08 +08:00
commit 7e8ac7f566
31 changed files with 4304 additions and 0 deletions

16
CMakeLists.txt Normal file
View File

@@ -0,0 +1,16 @@
idf_component_register(
SRCS
"src/dali_comm.cpp"
"src/base.cpp"
"src/addr.cpp"
"src/decode.cpp"
"src/device.cpp"
"src/dt1.cpp"
"src/dt8.cpp"
"src/sequence.cpp"
"src/sequence_store.cpp"
"src/color.cpp"
INCLUDE_DIRS "include"
)
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)

49
README.md Normal file
View File

@@ -0,0 +1,49 @@
# DALI ESP-IDF Component
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.
## 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.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.
## Notes
- Optional query support: provide a `transact` callback that returns the gateway reply; otherwise, query methods return `std::nullopt`.
- `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.

9
idf_component.yml Normal file
View File

@@ -0,0 +1,9 @@
dependencies:
idf: '>=5.1'
description: ESP DALI CPP library component for ESP-IDF
repository: git://github.com/tony-cloud/esp-dali-cpp.git
repository_info:
commit_sha: ee66ac9af9bb8edd19ba48fb7b8c52c49dea74d2
path: components/dali_cpp
url: https://github.com/tony-cloud/esp-dali-cpp
version: 0.0.1

56
include/addr.hpp Normal file
View File

@@ -0,0 +1,56 @@
#pragma once
#include "base.hpp"
#include <optional>
#include <utility>
#include <vector>
struct DaliCompareAddrResult {
int retH = 0;
int retM = 0;
int retL = 0;
int nextAddr = 0;
};
class DaliAddr {
public:
explicit DaliAddr(DaliBase& base);
std::vector<int> onlineDevices;
bool isSearching = false;
int scanRangeStart = 0;
int scanRangeEnd = 63;
bool isAllocAddr() const;
void setIsAllocAddr(bool value);
int lastAllocAddr() const;
void setLastAllocAddr(int value);
void selectDevice(int address);
bool writeAddr(int addr, int newAddr);
bool removeAddr(int addr);
bool removeAllAddr();
std::vector<int> searchAddr(int addr = 63);
std::vector<int> searchAddrRange(int start = 0, int end = 63);
void stopSearch();
bool compareSingleAddress(int typ, int addr);
std::pair<int, int> precompareNew(int typ, int m = 0);
int compareAddress(int typ);
int compareAddressNew(int typ, int m = 0);
DaliCompareAddrResult compareAddr(int ad, std::optional<int> minH, std::optional<int> minM,
std::optional<int> minL);
int compareMulti(int h, int m, int l, int ad);
bool allocateAllAddr(int ads = 0);
void stopAllocAddr();
bool removeFromScene(int addr, int scene);
std::optional<int> getSceneBright(int addr, int scene);
bool resetAndAllocAddr(int n = 0, bool removeAddrFirst = false, bool closeLight = false);
private:
DaliBase& base_;
};

166
include/base.hpp Normal file
View File

@@ -0,0 +1,166 @@
#pragma once
#include "dali_comm.hpp"
#include <cmath>
#include <cstdint>
#include <chrono>
#include <map>
#include <optional>
#include <utility>
#include <vector>
struct DaliStatus {
bool controlGearPresent = false;
bool lampFailure = false;
bool lampPowerOn = false;
bool limitError = false;
bool fadingCompleted = false;
bool resetState = false;
bool missingShortAddress = false;
bool psFault = false;
static DaliStatus fromByte(uint8_t status);
};
class DaliBase {
public:
explicit DaliBase(DaliComm& comm);
// Runtime state mirrored from Dart DaliBase.
const int broadcast = 127;
bool isAllocAddr = false;
int lastAllocAddr = 0;
int selectedAddress = 127;
int64_t mcuTicks() const;
// Scene helpers
bool toScene(int a, int s);
bool reset(int a, int t = 2);
// Brightness helpers
int brightnessToLog(int brightness) const;
int logToBrightness(int logValue) const;
bool setBright(int a, int b);
bool setBrightPercentage(int a, double b);
bool stopFade(int a);
bool off(int a);
bool on(int a);
bool recallMaxLevel(int a);
bool recallMinLevel(int a);
int groupToAddr(int gp) const;
// Core sending wrappers
bool send(int a, uint8_t cmd);
bool sendCmd(uint8_t addr, uint8_t cmd);
bool sendExtCmd(int cmd, int value);
// DTR helpers
bool setDTR(int value);
bool setDTR1(int value);
bool setDTR2(int value);
std::optional<int> getDTR(int a);
std::optional<int> getDTR1(int a);
std::optional<int> getDTR2(int a);
bool copyCurrentBrightToDTR(int a);
// Colour value helpers (DT8 plumbing)
bool queryColourValue(int a);
bool storeDTRAsAddr(int a);
bool storeDTRAsSceneBright(int a, int s);
bool storeScene(int a, int s);
bool removeScene(int a, int s);
bool addToGroup(int a, int g);
bool removeFromGroup(int a, int g);
bool storeDTRAsFadeTime(int a);
bool storeDTRAsFadeRate(int a);
bool storeDTRAsPoweredBright(int a);
bool storeDTRAsSystemFailureLevel(int a);
bool storeDTRAsMinLevel(int a);
bool storeDTRAsMaxLevel(int a);
bool storeColourTempLimits(int a);
std::optional<bool> getOnlineStatus(int a);
std::optional<int> getBright(int a);
std::optional<int> getDeviceType(int a);
std::optional<int> getPhysicalMinLevel(int a);
std::optional<int> getDeviceVersion(int a);
bool dtSelect(int value);
bool activate(int a);
bool setDTRAsColourX(int a);
bool setDTRAsColourY(int a);
bool setDTRAsColourRGB(int a);
bool setDTRAsColourTemp(int a);
bool copyReportColourToTemp(int a);
bool setGradualChangeSpeed(int a, int value);
bool setGradualChangeRate(int a, int value);
std::optional<std::pair<int, int>> getGradualChange(int a);
std::optional<int> getGradualChangeRate(int a);
std::optional<int> getGradualChangeSpeed(int a);
bool setPowerOnLevel(int a, int value);
std::optional<int> getPowerOnLevel(int a);
bool setSystemFailureLevel(int a, int value);
std::optional<int> getSystemFailureLevel(int a);
bool setMinLevel(int a, int value);
std::optional<int> getMinLevel(int a);
bool setMaxLevel(int a, int value);
std::optional<int> getMaxLevel(int a);
bool setFadeTime(int a, int value);
std::optional<int> getFadeTime(int a);
bool setFadeRate(int a, int value);
std::optional<int> getFadeRate(int a);
std::optional<int> getNextDeviceType(int a);
std::optional<int> getGroupH(int a);
std::optional<int> getGroupL(int a);
std::optional<int> getGroup(int a);
bool setGroup(int a, int value);
std::optional<int> getScene(int a, int b);
bool setScene(int a, int b);
std::map<int, int> getScenes(int a);
std::optional<int> getStatus(int a);
std::optional<bool> getControlGearPresent(int a);
std::optional<bool> getLampFailureStatus(int a);
std::optional<bool> getLampPowerOnStatus(int a);
std::optional<bool> getLimitError(int a);
std::optional<bool> getResetStatus(int a);
std::optional<bool> getMissingShortAddress(int a);
bool terminate();
bool randomise();
bool initialiseAll();
bool initialise();
bool withdraw();
bool cancel();
bool physicalSelection();
bool queryAddressH(int addr);
bool queryAddressM(int addr);
bool queryAddressL(int addr);
bool programShortAddr(int a);
std::optional<int> queryShortAddr();
std::optional<bool> verifyShortAddr(int a);
std::optional<bool> compareAddress();
std::optional<bool> compare(int h, int m, int l);
std::optional<int> getRandomAddrH(int addr);
std::optional<int> getRandomAddrM(int addr);
std::optional<int> getRandomAddrL(int addr);
// Exposed query helpers mirroring Dart API.
std::optional<int> query(int a, uint8_t cmd);
std::optional<int> queryCmd(uint8_t addr, uint8_t cmd);
private:
DaliComm& comm_;
static uint8_t encodeCmdAddr(int dec_addr);
static uint8_t encodeArcAddr(int dec_addr);
};

71
include/bus_monitor.hpp Normal file
View File

@@ -0,0 +1,71 @@
#pragma once
#include "decode.hpp"
#include <cstdint>
#include <functional>
#include <vector>
enum class BusDir { front, back };
struct BusFrame {
BusDir dir = BusDir::front;
int proto = 0x10;
int b1 = 0;
int b2 = 0;
};
class BusMonitor {
public:
static BusMonitor& I() {
static BusMonitor inst;
return inst;
}
void setResponseWindowMs(int ms) { decoder.responseWindowMs = ms; }
void setRawSink(std::function<void(const BusFrame&)> sink) { rawSink_ = std::move(sink); }
void setDecodedSink(std::function<void(const DecodedRecord&)> sink) { decodedSink_ = std::move(sink); }
void emitFront(int proto, int addr, int cmd) {
BusFrame f{BusDir::front, proto, addr & 0xFF, cmd & 0xFF};
rawFrames.push_back(f);
if (rawSink_) rawSink_(f);
auto rec = decoder.decode(addr & 0xFF, cmd & 0xFF, proto);
records.push_back(rec);
if (decodedSink_) decodedSink_(rec);
}
void emitBack(int value, int prefix = 0xFF) {
BusFrame f{BusDir::back, 0xFF, prefix & 0xFF, value & 0xFF};
rawFrames.push_back(f);
if (rawSink_) rawSink_(f);
auto rec = decoder.decodeCmdResponse(value & 0xFF, prefix & 0xFF);
records.push_back(rec);
if (decodedSink_) decodedSink_(rec);
}
void clear() {
rawFrames.clear();
records.clear();
}
void dispose() {
rawFrames.clear();
records.clear();
rawSink_ = nullptr;
decodedSink_ = nullptr;
}
DaliDecode decoder;
std::vector<BusFrame> rawFrames;
std::vector<DecodedRecord> records;
private:
BusMonitor() = default;
std::function<void(const BusFrame&)> rawSink_;
std::function<void(const DecodedRecord&)> decodedSink_;
};

30
include/color.hpp Normal file
View File

@@ -0,0 +1,30 @@
#pragma once
#include <array>
#include <cstdint>
#include <vector>
// Colour conversion utilities mirroring lib/dali/color.dart.
class DaliColor {
public:
static std::array<int, 4> toIntList(double a, double r, double g, double b);
static int toInt(double a, double r, double g, double b);
static double decimalRound(int num, double idp);
static std::array<double, 3> gammaCorrection(double r, double g, double b, double gamma = 2.8);
static std::array<double, 3> rgb2xyz(double r, double g, double b);
static std::array<int, 3> xyz2rgb(double x, double y, double z);
static std::array<double, 2> xyz2xy(double x, double y, double z);
static std::array<double, 3> xy2xyz(double xVal, double yVal);
static std::array<double, 2> rgb2xy(double r, double g, double b);
static std::array<int, 3> xy2rgb(double xVal, double yVal);
static std::array<double, 3> xyz2lab(double x, double y, double z);
static std::array<double, 3> rgb2lab(double r, double g, double b);
static std::array<double, 3> lab2xyz(double l, double a, double b);
static std::array<int, 3> lab2rgb(double l, double a, double b);
private:
static double srgbToLinear(double value);
static double linearToSrgb(double value);
};

3
include/comm.hpp Normal file
View File

@@ -0,0 +1,3 @@
#pragma once
#include "dali_comm.hpp"

53
include/dali.hpp Normal file
View File

@@ -0,0 +1,53 @@
#pragma once
#include "base.hpp"
#include "bus_monitor.hpp"
#include "comm.hpp"
#include "dali_comm.hpp"
#include "decode.hpp"
#include "device.hpp"
#include "dt1.hpp"
#include "dt8.hpp"
#include "addr.hpp"
#include "color.hpp"
#include "errors.hpp"
#include "log.hpp"
#include "query_scheduler.hpp"
#include "sequence.hpp"
#include "sequence_store.hpp"
#include <memory>
#include <string>
// Convenience umbrella header for the ESP-IDF DALI component.
class Dali {
public:
static Dali& instance(DaliComm& comm) {
if (!instance_) {
instance_ = std::unique_ptr<Dali>(new Dali(comm));
}
return *instance_;
}
static void resetInstance() { instance_.reset(); }
static constexpr int broadcast = 127;
std::string name = "dali1";
int gw = 0;
DaliBase base;
DaliDecode decode;
DaliDT1 dt1;
DaliDT8 dt8;
DaliAddr addr;
explicit Dali(DaliComm& comm, int g = 0, const std::string& n = "dali1")
: name(n), gw(g), base(comm), decode(), dt1(base), dt8(base), addr(base) {}
void open() {}
void close() {}
private:
inline static std::unique_ptr<Dali> instance_ = nullptr;
};

68
include/dali_comm.hpp Normal file
View File

@@ -0,0 +1,68 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <functional>
#include <optional>
#include <vector>
// Lightweight communicator for DALI gateway type 1 (USB UART new frame format).
// Frames:
// - Send: [0x10, addr, cmd]
// - Ext: [0x11, addr, cmd] (sent twice with a small delay)
// - Query: [0x12, addr, cmd] -> expects [0xFF, data] on success
// Callers provide UART callbacks; this class only builds frames and parses basic responses.
class DaliComm {
public:
using SendCallback = std::function<bool(const uint8_t* data, size_t len)>;
using ReadCallback = std::function<std::vector<uint8_t>(size_t len, uint32_t timeout_ms)>;
using TransactCallback = std::function<std::vector<uint8_t>(const uint8_t* data, size_t len)>;
using DelayCallback = std::function<void(uint32_t ms)>;
explicit DaliComm(SendCallback send_cb,
ReadCallback read_cb = nullptr,
TransactCallback transact_cb = nullptr,
DelayCallback delay_cb = nullptr);
void setSendCallback(SendCallback cb);
void setReadCallback(ReadCallback cb);
void setTransactCallback(TransactCallback cb);
void setDelayCallback(DelayCallback cb);
// Methods mirroring lib/dali/comm.dart naming where practical.
static std::vector<uint8_t> checksum(const std::vector<uint8_t>& data);
bool write(const std::vector<uint8_t>& data) const;
std::vector<uint8_t> read(size_t len, uint32_t timeout_ms = 100) const;
int checkGatewayType(int gateway) const;
void flush() const;
bool resetBus() const;
bool sendRaw(uint8_t addr, uint8_t cmd) const;
bool sendRawNew(uint8_t addr, uint8_t cmd, bool needVerify = false) const;
bool sendExtRaw(uint8_t addr, uint8_t cmd) const;
bool sendExtRawNew(uint8_t addr, uint8_t cmd) const;
std::optional<uint8_t> queryRaw(uint8_t addr, uint8_t cmd) const;
std::optional<uint8_t> queryRawNew(uint8_t addr, uint8_t cmd) const;
bool send(int dec_addr, uint8_t cmd) const;
std::optional<uint8_t> query(int dec_addr, uint8_t cmd) const;
bool getBusStatus() const;
// Send standard command frame (0x10).
bool sendCmd(uint8_t addr, uint8_t cmd) const;
// Send extended command frame (0x11).
bool sendExtCmd(uint8_t addr, uint8_t cmd) const;
// Send query frame (0x12) and parse single-byte response. Returns nullopt on no/invalid response.
std::optional<uint8_t> queryCmd(uint8_t addr, uint8_t cmd) const;
// Helpers to mirror Dart address conversion (DEC short address -> DALI odd/even encoded).
static uint8_t toCmdAddr(int dec_addr); // odd address for commands
static uint8_t toArcAddr(int dec_addr); // even address for direct arc
private:
SendCallback send_;
ReadCallback read_;
TransactCallback transact_;
DelayCallback delay_;
bool writeFrame(const std::vector<uint8_t>& frame) const;
void sleepMs(uint32_t ms) const;
};

157
include/dali_define.hpp Normal file
View File

@@ -0,0 +1,157 @@
#pragma once
// Standard control commands (IEC 62386 command set)
#define DALI_CMD_OFF 0x00
#define DALI_CMD_RECALL_MAX 0x05
#define DALI_CMD_RECALL_MAX_LEVEL DALI_CMD_RECALL_MAX
#define DALI_CMD_RECALL_MIN 0x06
#define DALI_CMD_RECALL_MIN_LEVEL DALI_CMD_RECALL_MIN
#define DALI_CMD_RESET 0x20
#define DALI_CMD_STORE_ACTUAL_LEVEL_IN_THE_DTR 0x21
#define DALI_CMD_STORE_THE_DTR_AS_MAX_LEVEL 0x2A
#define DALI_CMD_STORE_THE_DTR_AS_MIN_LEVEL 0x2B
#define DALI_CMD_STORE_THE_DTR_AS_SYS_FAIL_LEVEL 0x2C
#define DALI_CMD_STORE_THE_DTR_AS_PWR_ON_LEVEL 0x2D
#define DALI_CMD_STORE_THE_DTR_AS_FADE_TIME 0x2E
#define DALI_CMD_STORE_THE_DTR_AS_FADE_RATE 0x2F
#define DALI_CMD_STORE_DTR_AS_SHORT_ADDRESS 0x80
#define DALI_CMD_STOP_FADE 0xFF
// Indexed command ranges
#define DALI_CMD_GO_TO_SCENE(scene) (0x10 + (scene))
#define DALI_CMD_SET_SCENE(scene) (0x40 + (scene))
#define DALI_CMD_REMOVE_SCENE(scene) (0x50 + (scene))
#define DALI_CMD_ADD_TO_GROUP(group) (0x60 + (group))
#define DALI_CMD_REMOVE_FROM_GROUP(group) (0x70 + (group))
#define DALI_CMD_QUERY_SCENE_LEVEL(scene) (0xB0 + (scene))
// Command range boundaries used by decoders
#define DALI_CMD_GO_TO_SCENE_MIN DALI_CMD_GO_TO_SCENE(0)
#define DALI_CMD_GO_TO_SCENE_MAX DALI_CMD_GO_TO_SCENE(15)
#define DALI_CMD_SET_SCENE_MIN DALI_CMD_SET_SCENE(0)
#define DALI_CMD_SET_SCENE_MAX DALI_CMD_SET_SCENE(15)
#define DALI_CMD_ADD_TO_GROUP_MIN DALI_CMD_ADD_TO_GROUP(0)
#define DALI_CMD_ADD_TO_GROUP_MAX DALI_CMD_ADD_TO_GROUP(15)
#define DALI_CMD_REMOVE_FROM_GROUP_MIN DALI_CMD_REMOVE_FROM_GROUP(0)
#define DALI_CMD_REMOVE_FROM_GROUP_MAX DALI_CMD_REMOVE_FROM_GROUP(15)
#define DALI_CMD_QUERY_SCENE_LEVEL_MIN DALI_CMD_QUERY_SCENE_LEVEL(0)
#define DALI_CMD_QUERY_SCENE_LEVEL_MAX DALI_CMD_QUERY_SCENE_LEVEL(15)
#define DALI_CMD_SPECIAL_RANGE_MIN DALI_CMD_QUERY_STATUS
#define DALI_CMD_SPECIAL_RANGE_MAX DALI_CMD_DT8_QUERY_ASSIGNED_COLOR
// Query commands
#define DALI_CMD_QUERY_STATUS 0x90
#define DALI_CMD_QUERY_BALLAST 0x91
#define DALI_CMD_QUERY_LAMP_FAILURE 0x92
#define DALI_CMD_QUERY_LAMP_POWER_ON 0x93
#define DALI_CMD_QUERY_LIMIT_ERROR 0x94
#define DALI_CMD_QUERY_RESET_STATE 0x95
#define DALI_CMD_QUERY_MISSING_SHORT_ADDRESS 0x96
#define DALI_CMD_QUERY_VERSION_NUMBER 0x97
#define DALI_CMD_QUERY_CONTENT_DTR 0x98
#define DALI_CMD_QUERY_DEVICE_TYPE 0x99
#define DALI_CMD_QUERY_PHYSICAL_MINIMUM_LEVEL 0x9A
#define DALI_CMD_QUERY_POWER_FAILURE 0x9B
#define DALI_CMD_QUERY_CONTENT_DTR1 0x9C
#define DALI_CMD_QUERY_CONTENT_DTR2 0x9D
#define DALI_CMD_QUERY_OPERATING_MODE 0x9E
#define DALI_CMD_QUERY_LIGHT_SOURCE_TYPE 0x9F
#define DALI_CMD_QUERY_ACTUAL_LEVEL 0xA0
#define DALI_CMD_QUERY_MAX_LEVEL 0xA1
#define DALI_CMD_QUERY_MIN_LEVEL 0xA2
#define DALI_CMD_QUERY_POWER_ON_LEVEL 0xA3
#define DALI_CMD_QUERY_SYSTEM_FAILURE_LEVEL 0xA4
#define DALI_CMD_QUERY_FADE_TIME_FADE_RATE 0xA5
#define DALI_CMD_QUERY_MANUFACTURER_SPECIFIC_MODE 0xA6
#define DALI_CMD_QUERY_NEXT_DEVICE_TYPE 0xA7
#define DALI_CMD_QUERY_EXTENDED_FADE_TIME 0xA8
#define DALI_CMD_QUERY_CONTROL_GEAR_FAILURE 0xAA
#define DALI_CMD_QUERY_GROUPS_0_7 0xC0
#define DALI_CMD_QUERY_GROUP_8_15 0xC1
#define DALI_CMD_QUERY_RANDOM_ADDRESS_H 0xC2
#define DALI_CMD_QUERY_RANDOM_ADDRESS_M 0xC3
#define DALI_CMD_QUERY_RANDOM_ADDRESS_L 0xC4
#define DALI_CMD_READ_MEMORY_LOCATION 0xC5
// Special/programming commands
#define DALI_CMD_SPECIAL_TERMINATE 0xA1
#define DALI_CMD_SPECIAL_SET_DTR0 0xA3
#define DALI_CMD_SPECIAL_INITIALIZE 0xA5
#define DALI_CMD_SPECIAL_RANDOMIZE 0xA7
#define DALI_CMD_SPECIAL_COMPARE 0xA9
#define DALI_CMD_SPECIAL_WITHDRAW 0xAB
#define DALI_CMD_SPECIAL_CANCEL 0xAD
#define DALI_CMD_SPECIAL_SEARCHADDRH 0xB1
#define DALI_CMD_SPECIAL_SEARCHADDRM 0xB3
#define DALI_CMD_SPECIAL_SEARCHADDRL 0xB5
#define DALI_CMD_SPECIAL_PROGRAM_SHORT_ADDRESS 0xB7
#define DALI_CMD_SPECIAL_VERIFY_SHORT_ADDRESS 0xB9
#define DALI_CMD_SPECIAL_QUERY_SHORT_ADDRESS 0xBB
#define DALI_CMD_SPECIAL_PHYSICAL_SELECTION 0xBD
#define DALI_CMD_SPECIAL_DT_SELECT 0xC1
#define DALI_CMD_SPECIAL_SET_DTR_1 0xC3
#define DALI_CMD_SPECIAL_SET_DTR_2 0xC5
// DT8 commands and queries
#define DALI_CMD_DT8_STORE_DTR_AS_COLORX 0xE0
#define DALI_CMD_DT8_STORE_DTR_AS_COLORY 0xE1
#define DALI_CMD_DT8_ACTIVATE 0xE2
#define DALI_CMD_DT8_STEP_UP_X_COORDINATE 0xE3
#define DALI_CMD_DT8_STEP_DOWN_X_COORDINATE 0xE4
#define DALI_CMD_DT8_STEP_UP_Y_COORDINATE 0xE5
#define DALI_CMD_DT8_STEP_DOWN_Y_COORDINATE 0xE6
#define DALI_CMD_DT8_SET_COLOR_TEMPERATURE 0xE7
#define DALI_CMD_DT8_STEP_UP_COLOR_TEMPERATURE 0xE8
#define DALI_CMD_DT8_STEP_DOWN_COLOR_TEMPERATURE 0xE9
#define DALI_CMD_DT8_SET_TEMPORARY_PRIMARY_DIM_LEVEL 0xEA
#define DALI_CMD_DT8_SET_TEMPORARY_RGB_DIM_LEVELS 0xEB
#define DALI_CMD_DT8_SET_TEMPORARY_WAF_DIM_LEVELS 0xEC
#define DALI_CMD_DT8_SET_TEMPORARY_RGBWAF_CONTROL 0xED
#define DALI_CMD_DT8_COPY_REPORT_TO_TEMPORARY 0xEE
#define DALI_CMD_DT8_STORE_PRIMARY_N_TY 0xF0
#define DALI_CMD_DT8_STORE_PRIMARY_N_XY 0xF1
#define DALI_CMD_DT8_STORE_COLOR_TEMPERATURE_LIMIT 0xF2
#define DALI_CMD_DT8_SET_GEAR_FEATURES 0xF3
#define DALI_CMD_DT8_ASSIGN_COLOR_TO_LINKED_CHANNEL 0xF5
#define DALI_CMD_DT8_START_AUTO_CALIBRATION 0xF6
#define DALI_CMD_DT8_QUERY_GEAR_FEATURES_STATUS 0xF7
#define DALI_CMD_QUERY_COLOR_STATUS 0xF8
#define DALI_CMD_QUERY_COLOR_TYPE 0xF9
#define DALI_CMD_QUERY_COLOR_VALUE 0xFA
#define DALI_CMD_DT8_QUERY_RGBWAF_CONTROL 0xFB
#define DALI_CMD_DT8_QUERY_ASSIGNED_COLOR 0xFC
#define DALI_CMD_DT8_QUERY_EXTENDED_VERSION 0xFF
// DT1 commands and queries
#define DALI_CMD_DT1_REST 0xE0
#define DALI_CMD_DT1_INHIBIT 0xE1
#define DALI_CMD_DT1_RE_LIGHT_RESET_INHIBIT 0xE2
#define DALI_CMD_DT1_START_FUNCTION_TEST 0xE3
#define DALI_CMD_DT1_START_DURATION_TEST 0xE4
#define DALI_CMD_DT1_STOP_TEST 0xE5
#define DALI_CMD_DT1_RESET_FUNCTION_TEST_DONE_FLAG 0xE6
#define DALI_CMD_DT1_RESET_DURATION_TEST_DONE_FLAG 0xE7
#define DALI_CMD_DT1_RESET_LAMP_TIME 0xE8
#define DALI_CMD_DT1_STORE_DTR_AS_EMERGENCY_LEVEL 0xE9
#define DALI_CMD_DT1_STORE_DTR_AS_DELAY_TIME_HIGH 0xEA
#define DALI_CMD_DT1_STORE_DTR_AS_DELAY_TIME_LOW 0xEB
#define DALI_CMD_DT1_STORE_DTR_AS_PROLONG_TIME 0xEC
#define DALI_CMD_DT1_STORE_DTR_AS_RATED_DURATION 0xED
#define DALI_CMD_DT1_STORE_DTR_AS_EMERGENCY_MIN_LEVEL 0xEE
#define DALI_CMD_DT1_STORE_DTR_AS_EMERGENCY_MAX_LEVEL 0xEF
#define DALI_CMD_DT1_START_IDENTIFICATION 0xF0
#define DALI_CMD_DT1_QUERY_EMERGENCY_LEVEL 0xF1
#define DALI_CMD_DT1_QUERY_EMERGENCY_MIN_LEVEL 0xF2
#define DALI_CMD_DT1_QUERY_EMERGENCY_MAX_LEVEL 0xF3
#define DALI_CMD_DT1_QUERY_PROLONG_TIME 0xF4
#define DALI_CMD_DT1_QUERY_FUNCTION_TEST_INTERVAL 0xF5
#define DALI_CMD_DT1_QUERY_DURATION_TEST_INTERVAL 0xF6
#define DALI_CMD_DT1_QUERY_DURATION_TEST_RESULT 0xF7
#define DALI_CMD_DT1_QUERY_LAMP_EMERGENCY_TIME 0xF8
#define DALI_CMD_DT1_QUERY_RATED_DURATION 0xF9
#define DALI_CMD_DT1_QUERY_EMERGENCY_MODE 0xFA
#define DALI_CMD_DT1_QUERY_FEATURE 0xFB
#define DALI_CMD_DT1_QUERY_FAILURE_STATUS 0xFC
#define DALI_CMD_DT1_QUERY_STATUS 0xFD
#define DALI_CMD_DT1_PERFORM_DTR_SELECTED_FUNCTION 0xFE
#define DALI_CMD_DT1_QUERY_EXTENDED_VERSION 0xFF

56
include/decode.hpp Normal file
View File

@@ -0,0 +1,56 @@
#pragma once
#include <cstdint>
#include <map>
#include <string>
#include <vector>
struct DecodedRecord {
std::string text;
std::string type;
int addr = -1;
int cmd = -1;
int proto = -1;
};
class DaliDecode {
public:
bool displayRaw = true;
int responseWindowMs = 100;
int dtr0 = 0;
int dtr1 = 0;
int dtr2 = 0;
DaliDecode();
int isQueryCmd(int cmd) const;
DecodedRecord decodeBright(int addr, int level);
DecodedRecord decodeScene(int addr, int sceneCmd);
DecodedRecord decodeCmd(int addr, int c);
DecodedRecord decodeSpCmd(int addr, int c);
DecodedRecord querySpCMD(int cmdByte, int dataByte);
DecodedRecord decodeQuery(int addr, int c);
DecodedRecord decodeCmdResponse(int value, int gwPrefix = 0xFF);
DecodedRecord decode(int addr, int c, int proto = 0x10);
private:
std::map<int, std::string> cmd_;
std::map<int, std::string> sCMD_;
std::vector<int> queryCmd_;
int lastQueryCmd_ = 0;
int64_t lastQueryAtMs_ = 0;
int pendingColourType_ = -1;
int64_t lastColourQueryAtMs_ = 0;
static int64_t nowMs();
static std::string hex(int v);
static std::string bin(int v);
static std::string yn(bool b);
static std::string whoLabel(int addr, bool even);
static std::string deviceTypeName(int v);
static std::string lightSourceName(int v);
DecodedRecord withRaw(const DecodedRecord& r) const;
};

116
include/device.hpp Normal file
View File

@@ -0,0 +1,116 @@
#pragma once
#include "model_value.hpp"
#include <optional>
#include <string>
#include <vector>
struct DaliLongAddress {
int h = 0;
int m = 0;
int l = 0;
static DaliLongAddress fromJson(const DaliValue::Object* json);
DaliValue::Object toJson() const;
};
struct DaliDeviceCapabilities {
std::optional<bool> supportsDt1;
std::optional<bool> supportsDt8;
static DaliDeviceCapabilities fromJson(const DaliValue::Object* json);
DaliValue::Object toJson() const;
void merge(const DaliDeviceCapabilities& other);
};
struct DaliStatusFlags {
std::optional<bool> controlGearPresent;
std::optional<bool> lampFailure;
std::optional<bool> lampPowerOn;
std::optional<bool> limitError;
std::optional<bool> fadingCompleted;
std::optional<bool> resetState;
std::optional<bool> missingShortAddress;
std::optional<bool> psFault;
static DaliStatusFlags fromJson(const DaliValue::Object* json);
DaliValue::Object toJson() const;
bool hasData() const;
void merge(const DaliStatusFlags& other);
};
struct DaliDt8State {
std::optional<int> colorType;
std::optional<std::string> activeMode;
std::optional<int> xyX;
std::optional<int> xyY;
std::optional<int> xyMinX;
std::optional<int> xyMaxX;
std::optional<int> xyMinY;
std::optional<int> xyMaxY;
std::optional<int> mirek;
std::optional<int> mirekMin;
std::optional<int> mirekMax;
std::optional<std::vector<int>> rgbwaf;
std::optional<std::vector<int>> primaryN;
static DaliDt8State fromJson(const DaliValue::Object* json);
DaliValue::Object toJson() const;
};
struct DaliDt1State {
std::optional<int> emergencyLevel;
std::optional<int> emergencyMinLevel;
std::optional<int> emergencyMaxLevel;
std::optional<int> prolongTimeMinutes;
std::optional<int> ratedDurationMinutes;
std::optional<int> testDelayTime;
std::optional<int> failureStatus;
std::optional<int> emergencyStatus;
std::optional<int> emergencyMode;
std::optional<int> feature;
std::optional<int> version;
static DaliDt1State fromJson(const DaliValue::Object* json);
DaliValue::Object toJson() const;
};
struct DaliDevice {
std::string id;
std::string name;
std::optional<int> shortAddress;
std::optional<DaliLongAddress> longAddress;
bool isolated = false;
std::optional<int> brightness;
std::optional<int> groupBits;
std::optional<std::vector<int>> scenes;
std::optional<int> fadeTime;
std::optional<int> fadeRate;
std::optional<int> powerOnLevel;
std::optional<int> systemFailureLevel;
std::optional<int> minLevel;
std::optional<int> maxLevel;
std::optional<int> operatingMode;
std::optional<int> physicalMinLevel;
std::optional<int> deviceType;
std::vector<int> extType;
std::optional<int> version;
DaliDeviceCapabilities capabilities;
std::optional<DaliDt8State> dt8;
std::optional<DaliDt1State> dt1;
DaliStatusFlags statusFlags;
std::optional<std::string> lastSyncedUtc;
DaliValue::Object metadata;
static DaliDevice fromJson(const DaliValue::Object& json);
DaliValue::Object toJson() const;
std::string displayName() const;
void merge(const DaliDevice& other);
};

129
include/dt1.hpp Normal file
View File

@@ -0,0 +1,129 @@
#pragma once
#include "base.hpp"
#include <cstdint>
#include <optional>
struct DT1TestStatusDetailed {
std::optional<int> failureStatus;
std::optional<int> emergencyStatus;
std::optional<int> emergencyMode;
std::optional<int> feature;
bool testInProgress = false;
bool lampFailure = false;
bool batteryFailure = false;
bool functionTestActive = false;
bool durationTestActive = false;
bool testDone = false;
bool identifyActive = false;
bool physicalSelectionActive = false;
};
class DaliDT1DeviceStatus {
public:
explicit DaliDT1DeviceStatus(int raw) : raw_(raw & 0xFF) {}
int raw() const { return raw_; }
bool controlGearFailure() const { return bit(0x01); }
bool controlGearOk() const { return !controlGearFailure(); }
bool lampFailure() const { return bit(0x02); }
bool lampPoweredByEmergencyGear() const { return bit(0x04); }
bool arcPowerBit3() const { return bit(0x08); }
bool arcPowerBit4() const { return bit(0x10); }
bool resetState() const { return bit(0x20); }
bool missingShortAddress() const { return bit(0x40); }
bool arcPowerBit7() const { return bit(0x80); }
private:
int raw_ = 0;
bool bit(int mask) const { return (raw_ & mask) != 0; }
};
class DaliDT1EmergencyStatus {
public:
explicit DaliDT1EmergencyStatus(int raw) : raw_(raw & 0xFF) {}
int raw() const { return raw_; }
bool inhibitMode() const { return bit(0x01); }
bool functionTestResultValid() const { return bit(0x02); }
bool durationTestResultValid() const { return bit(0x04); }
bool batteryFullyCharged() const { return bit(0x08); }
bool functionTestRequestPending() const { return bit(0x10); }
bool durationTestRequestPending() const { return bit(0x20); }
bool identificationActive() const { return bit(0x40); }
bool physicallySelected() const { return bit(0x80); }
bool batteryChargingInProgress() const { return !batteryFullyCharged(); }
private:
int raw_ = 0;
bool bit(int mask) const { return (raw_ & mask) != 0; }
};
class DaliDT1 {
public:
explicit DaliDT1(DaliBase& base);
bool enableDT1();
bool startDT1Test(int a, int t = 1);
std::optional<int> getDT1EmergencyMode(int a);
std::optional<int> getDT1Feature(int a);
std::optional<int> getDT1FailureStatus(int a);
std::optional<int> getDT1Status(int a);
std::optional<int> getDT1SelfTestStatus(int a);
std::optional<DT1TestStatusDetailed> getDT1TestStatusDetailed(int a);
bool performDT1Test(int a, int timeout = 5);
bool rest(int a);
bool inhibit(int a);
bool reLightOrResetInhibit(int a);
bool startFunctionTestCmd(int a);
bool startDurationTestCmd(int a);
bool stopTest(int a);
bool resetFunctionTestDoneFlag(int a);
bool resetDurationTestDoneFlag(int a);
bool resetLampTime(int a);
bool resetStatusFlags(int a);
bool resetLampOperationTime(int a);
bool resetTestResults(int a);
bool storeEmergencyLevel(int a, int level);
bool storeTestDelayTimeHighByte(int a, int highByte);
bool storeTestDelayTimeLowByte(int a, int lowByte);
bool storeFunctionTestIntervalDays(int a, int days);
bool storeDurationTestIntervalWeeks(int a, int weeks);
bool storeTestDelayTime16(int a, int quartersOfHour);
bool storeProlongTimeMinutes(int a, int minutes);
bool storeRatedDurationMinutes(int a, int minutes);
bool storeEmergencyMinLevel(int a, int level);
bool storeEmergencyMaxLevel(int a, int level);
bool startIdentification(int a);
bool performDTRSelectedFunction(int a,
const std::optional<int>& dtr0 = std::nullopt,
const std::optional<int>& dtr1 = std::nullopt);
std::optional<int> getExtendedVersionDT1(int a);
std::optional<int> getEmergencyLevel(int a);
std::optional<int> getEmergencyMinLevel(int a);
std::optional<int> getEmergencyMaxLevel(int a);
std::optional<int> getProlongTimeMinutes(int a);
std::optional<int> getFunctionTestIntervalDays(int a);
std::optional<int> getDurationTestIntervalWeeks(int a);
std::optional<int> getDurationTestResult(int a);
std::optional<int> getLampEmergencyTimeMinutes(int a);
std::optional<int> getRatedDurationMinutes(int a);
std::optional<DaliDT1DeviceStatus> getDeviceStatus(int a);
std::optional<DaliDT1EmergencyStatus> getEmergencyStatusDecoded(int a);
private:
DaliBase& base_;
bool enable();
static int addrOf(int a);
bool send(int a, int code);
std::optional<int> query(int a, int code);
};

156
include/dt8.hpp Normal file
View File

@@ -0,0 +1,156 @@
#pragma once
#include "base.hpp"
#include "color.hpp"
#include <optional>
#include <utility>
#include <vector>
class ColorStatus {
public:
explicit ColorStatus(int status) : status_(status) {}
bool xyOutOfRange() const { return (status_ & 0x01) != 0; }
bool ctOutOfRange() const { return (status_ & 0x02) != 0; }
bool autoCalibrationActive() const { return (status_ & 0x04) != 0; }
bool autoCalibrationSuccess() const { return (status_ & 0x08) != 0; }
bool xyActive() const { return (status_ & 0x10) != 0; }
bool ctActive() const { return (status_ & 0x20) != 0; }
bool primaryNActive() const { return (status_ & 0x40) != 0; }
bool rgbwafActive() const { return (status_ & 0x80) != 0; }
private:
int status_ = 0;
};
class ColorTypeFeature {
public:
explicit ColorTypeFeature(int features) : features_(features) {}
int features() const { return features_; }
bool xyCapable() const { return (features_ & 0x01) != 0; }
bool ctCapable() const { return (features_ & 0x02) != 0; }
int primaryCount() const { return (features_ >> 2) & 0x07; }
int rgbwafChannels() const { return (features_ >> 5) & 0x07; }
bool primaryNCapable() const { return primaryCount() > 0; }
bool rgbwafCapable() const { return rgbwafChannels() > 0; }
private:
int features_ = 0;
};
class DaliDT8 {
public:
explicit DaliDT8(DaliBase& base);
bool enableDT8();
std::optional<ColorTypeFeature> getColorTypeFeature(int a);
std::optional<ColorStatus> getColorStatus(int a);
std::optional<int> getColTempRaw(int a, int type = 2);
bool setColTempRaw(int a, int value);
bool setColorTemperature(int addr, int kelvin);
std::optional<int> getColorTemperature(int a);
std::optional<int> getMinColorTemperature(int a);
std::optional<int> getMaxColorTemperature(int a);
std::optional<int> getPhysicalMinColorTemperature(int a);
std::optional<int> getPhysicalMaxColorTemperature(int a);
bool setColourRaw(int addr, int x1, int y1);
// Temporary setters
bool setTemporaryColourXRaw(int addr, int x1);
bool setTemporaryColourYRaw(int addr, int y1);
bool setTemporaryColourXY(int a, double x, double y);
bool setTemporaryColourTemperature(int a, int kelvin);
bool setTemporaryPrimaryDimLevel(int a, int n, double level);
bool setTemporaryRGBDimLevels(int a, int r, int g, int b);
bool setTemporaryWAFDimLevels(int a, int w, int amber, int freecolour);
bool setTemporaryRGBWAFControl(int a, int control);
bool copyReportToTemporary(int a);
// Step commands
bool stepXUp(int a);
bool stepXDown(int a);
bool stepYUp(int a);
bool stepYDown(int a);
bool stepTcCooler(int a);
bool stepTcWarmer(int a);
bool setColourRGBRaw(int addr, int r, int g, int b);
bool setColour(int a, double x, double y);
std::optional<int> getColourRaw(int a, int type);
std::vector<double> getColour(int a);
bool setColourRGB(int addr, int r, int g, int b);
std::vector<int> getColourRGB(int a);
bool activateTemporaryColour(int a);
// Active-type queries
std::optional<int> getPrimaryDimLevel(int a, int n);
std::optional<int> getRedDimLevel(int a);
std::optional<int> getGreenDimLevel(int a);
std::optional<int> getBlueDimLevel(int a);
std::optional<int> getWhiteDimLevel(int a);
std::optional<int> getAmberDimLevel(int a);
std::optional<int> getFreecolourDimLevel(int a);
std::optional<int> getRGBWAFControl(int a);
// Temporary colour queries
std::optional<int> getTemporaryXRaw(int a);
std::optional<int> getTemporaryYRaw(int a);
std::optional<int> getTemporaryColourTemperatureRaw(int a);
std::optional<int> getTemporaryPrimaryDimLevel(int a, int n);
std::optional<int> getTemporaryRedDimLevel(int a);
std::optional<int> getTemporaryGreenDimLevel(int a);
std::optional<int> getTemporaryBlueDimLevel(int a);
std::optional<int> getTemporaryWhiteDimLevel(int a);
std::optional<int> getTemporaryAmberDimLevel(int a);
std::optional<int> getTemporaryFreecolourDimLevel(int a);
std::optional<int> getTemporaryRGBWAFControl(int a);
std::optional<int> getTemporaryColourType(int a);
std::vector<double> getTemporaryColour(int a);
std::optional<int> getTemporaryColorTemperature(int a);
// Report colour queries
std::optional<int> getReportXRaw(int a);
std::optional<int> getReportYRaw(int a);
std::optional<int> getReportColourTemperatureRaw(int a);
std::optional<int> getReportPrimaryDimLevel(int a, int n);
std::optional<int> getReportRedDimLevel(int a);
std::optional<int> getReportGreenDimLevel(int a);
std::optional<int> getReportBlueDimLevel(int a);
std::optional<int> getReportWhiteDimLevel(int a);
std::optional<int> getReportAmberDimLevel(int a);
std::optional<int> getReportFreecolourDimLevel(int a);
std::optional<int> getReportRGBWAFControl(int a);
std::optional<int> getReportColourType(int a);
std::vector<double> getReportColour(int a);
std::optional<int> getReportColorTemperature(int a);
std::optional<int> getNumberOfPrimaries(int a);
std::optional<int> getPrimaryXRaw(int a, int n);
std::optional<int> getPrimaryYRaw(int a, int n);
std::optional<int> getPrimaryTy(int a, int n);
std::vector<double> getSceneColor(int a, int sense);
// Store / config
bool storePrimaryTy(int a, int n, int ty);
bool storePrimaryXY(int a, int n, double x, double y);
bool storeColourTempLimitRaw(int a, int limitType, int mirek);
bool storeColourTempLimit(int a, int limitType, int kelvin);
bool setGearAutoActivate(int a, bool enable);
bool assignColourToLinkedChannels(int a, int colourId);
bool startAutoCalibration(int a);
// Direct queries
std::optional<int> getGearFeaturesStatus(int a);
std::optional<int> getRGBWAFControlDirect(int a);
std::optional<int> getAssignedColourForChannel(int a, int channelId);
std::optional<int> getExtendedVersion(int a);
private:
DaliBase& base_;
};

111
include/errors.hpp Normal file
View File

@@ -0,0 +1,111 @@
#pragma once
#include <exception>
#include <functional>
#include <optional>
#include <string>
class DaliQueryException : public std::exception {
public:
enum class Code {
busUnavailable,
gatewayTimeout,
deviceNoResponse,
invalidFrame,
invalidGatewayFrame,
unknown,
};
DaliQueryException(Code code,
std::string message,
std::optional<int> addr = std::nullopt,
std::optional<int> cmd = std::nullopt)
: code_(code), message_(std::move(message)), addr_(addr), cmd_(cmd) {}
const char* what() const noexcept override { return message_.c_str(); }
Code code() const { return code_; }
const std::optional<int>& addr() const { return addr_; }
const std::optional<int>& cmd() const { return cmd_; }
private:
Code code_ = Code::unknown;
std::string message_;
std::optional<int> addr_;
std::optional<int> cmd_;
};
class DaliBusUnavailableException : public DaliQueryException {
public:
explicit DaliBusUnavailableException(std::optional<int> addr = std::nullopt,
std::optional<int> cmd = std::nullopt)
: DaliQueryException(Code::busUnavailable, "Bus unavailable", addr, cmd) {}
};
class DaliGatewayTimeoutException : public DaliQueryException {
public:
explicit DaliGatewayTimeoutException(std::optional<int> addr = std::nullopt,
std::optional<int> cmd = std::nullopt)
: DaliQueryException(Code::gatewayTimeout, "Gateway no response", addr, cmd) {}
};
class DaliDeviceNoResponseException : public DaliQueryException {
public:
explicit DaliDeviceNoResponseException(std::optional<int> addr = std::nullopt,
std::optional<int> cmd = std::nullopt)
: DaliQueryException(Code::deviceNoResponse, "Device no response", addr, cmd) {}
};
class DaliInvalidFrameException : public DaliQueryException {
public:
explicit DaliInvalidFrameException(std::optional<int> addr = std::nullopt,
std::optional<int> cmd = std::nullopt)
: DaliQueryException(Code::invalidFrame, "Invalid frame", addr, cmd) {}
};
class DaliInvalidGatewayFrameException : public DaliQueryException {
public:
explicit DaliInvalidGatewayFrameException(std::optional<int> addr = std::nullopt,
std::optional<int> cmd = std::nullopt)
: DaliQueryException(Code::invalidGatewayFrame, "Invalid gateway frame", addr, cmd) {}
};
inline std::string mapDaliErrorToMessage(const DaliQueryException& e) {
switch (e.code()) {
case DaliQueryException::Code::busUnavailable:
return "dali.error.bus_unavailable";
case DaliQueryException::Code::gatewayTimeout:
return "dali.error.gateway_timeout";
case DaliQueryException::Code::deviceNoResponse:
return "dali.error.device_no_response";
case DaliQueryException::Code::invalidFrame:
return "dali.error.invalid_frame";
case DaliQueryException::Code::invalidGatewayFrame:
return "dali.error.invalid_gateway_frame";
case DaliQueryException::Code::unknown:
default:
break;
}
return "dali.error.unknown";
}
template <typename T>
std::optional<T> daliSafe(const std::function<T()>& action,
const std::function<void(const std::string&)>& onError = nullptr,
bool rethrowOthers = false) {
#if defined(__cpp_exceptions)
try {
return action();
} catch (const DaliQueryException& e) {
if (onError) onError(mapDaliErrorToMessage(e));
return std::nullopt;
} catch (...) {
if (rethrowOthers) throw;
if (onError) onError("Unexpected error");
return std::nullopt;
}
#else
(void)onError;
(void)rethrowOthers;
return action();
#endif
}

59
include/log.hpp Normal file
View File

@@ -0,0 +1,59 @@
#pragma once
#include <functional>
#include <mutex>
#include <string>
#include <vector>
enum class LogLevel { debug = 0, info = 1, warning = 2, error = 3 };
class DaliLog {
public:
static DaliLog& instance() {
static DaliLog inst;
return inst;
}
void setLevel(LogLevel level) {
std::lock_guard<std::mutex> lock(mu_);
level_ = level;
}
LogLevel currentLevel() const { return level_; }
void setSink(std::function<void(const std::string&)> sink) {
std::lock_guard<std::mutex> lock(mu_);
sink_ = std::move(sink);
}
void debugLog(const std::string& message) { log(LogLevel::debug, "DEBUG", message); }
void infoLog(const std::string& message) { log(LogLevel::info, "INFO", message); }
void warningLog(const std::string& message) { log(LogLevel::warning, "WARNING", message); }
void errorLog(const std::string& message) { log(LogLevel::error, "ERROR", message); }
std::vector<std::string> logMessages() const {
std::lock_guard<std::mutex> lock(mu_);
return logs_;
}
void clearLogs() {
std::lock_guard<std::mutex> lock(mu_);
logs_.clear();
}
private:
DaliLog() = default;
void log(LogLevel level, const char* tag, const std::string& message) {
std::lock_guard<std::mutex> lock(mu_);
if (static_cast<int>(level) < static_cast<int>(level_)) return;
const std::string line = std::string("[") + tag + "] " + message;
logs_.push_back(line);
if (sink_) sink_(line);
}
mutable std::mutex mu_;
LogLevel level_ = LogLevel::info;
std::vector<std::string> logs_;
std::function<void(const std::string&)> sink_;
};

144
include/model_value.hpp Normal file
View File

@@ -0,0 +1,144 @@
#pragma once
#include <cerrno>
#include <cstdlib>
#include <map>
#include <optional>
#include <string>
#include <variant>
#include <vector>
class DaliValue {
public:
using Array = std::vector<DaliValue>;
using Object = std::map<std::string, DaliValue>;
using Variant = std::variant<std::monostate, bool, int64_t, double, std::string, Array, Object>;
DaliValue() = default;
DaliValue(std::nullptr_t) : value_(std::monostate{}) {}
DaliValue(bool v) : value_(v) {}
DaliValue(int v) : value_(static_cast<int64_t>(v)) {}
DaliValue(int64_t v) : value_(v) {}
DaliValue(double v) : value_(v) {}
DaliValue(const char* v) : value_(std::string(v)) {}
DaliValue(std::string v) : value_(std::move(v)) {}
DaliValue(Array v) : value_(std::move(v)) {}
DaliValue(Object v) : value_(std::move(v)) {}
bool isNull() const { return std::holds_alternative<std::monostate>(value_); }
bool isBool() const { return std::holds_alternative<bool>(value_); }
bool isInt() const { return std::holds_alternative<int64_t>(value_); }
bool isDouble() const { return std::holds_alternative<double>(value_); }
bool isString() const { return std::holds_alternative<std::string>(value_); }
bool isArray() const { return std::holds_alternative<Array>(value_); }
bool isObject() const { return std::holds_alternative<Object>(value_); }
std::optional<bool> asBool() const {
if (isBool()) return std::get<bool>(value_);
if (isInt()) return std::get<int64_t>(value_) != 0;
if (isString()) {
const auto& s = std::get<std::string>(value_);
if (s == "true" || s == "TRUE" || s == "1") return true;
if (s == "false" || s == "FALSE" || s == "0") return false;
}
return std::nullopt;
}
std::optional<int> asInt() const {
if (isInt()) return static_cast<int>(std::get<int64_t>(value_));
if (isDouble()) return static_cast<int>(std::get<double>(value_));
if (isString()) {
const auto& s = std::get<std::string>(value_);
if (s.empty()) {
return std::nullopt;
}
char* end = nullptr;
errno = 0;
const long parsed = std::strtol(s.c_str(), &end, 10);
if (errno != 0 || end == s.c_str() || *end != '\0') {
return std::nullopt;
}
return static_cast<int>(parsed);
}
return std::nullopt;
}
std::optional<double> asDouble() const {
if (isDouble()) return std::get<double>(value_);
if (isInt()) return static_cast<double>(std::get<int64_t>(value_));
if (isString()) {
const auto& s = std::get<std::string>(value_);
if (s.empty()) {
return std::nullopt;
}
char* end = nullptr;
errno = 0;
const double parsed = std::strtod(s.c_str(), &end);
if (errno != 0 || end == s.c_str() || *end != '\0') {
return std::nullopt;
}
return parsed;
}
return std::nullopt;
}
std::optional<std::string> asString() const {
if (isString()) return std::get<std::string>(value_);
if (isBool()) return std::get<bool>(value_) ? "true" : "false";
if (isInt()) return std::to_string(std::get<int64_t>(value_));
if (isDouble()) return std::to_string(std::get<double>(value_));
return std::nullopt;
}
const Array* asArray() const {
if (!isArray()) return nullptr;
return &std::get<Array>(value_);
}
const Object* asObject() const {
if (!isObject()) return nullptr;
return &std::get<Object>(value_);
}
Array* asArray() {
if (!isArray()) return nullptr;
return &std::get<Array>(value_);
}
Object* asObject() {
if (!isObject()) return nullptr;
return &std::get<Object>(value_);
}
const Variant& variant() const { return value_; }
Variant& variant() { return value_; }
private:
Variant value_;
};
inline const DaliValue* getObjectValue(const DaliValue::Object& obj, const std::string& key) {
const auto it = obj.find(key);
if (it == obj.end()) return nullptr;
return &it->second;
}
inline std::optional<int> getObjectInt(const DaliValue::Object& obj, const std::string& key) {
const auto* v = getObjectValue(obj, key);
if (!v) return std::nullopt;
return v->asInt();
}
inline std::optional<bool> getObjectBool(const DaliValue::Object& obj, const std::string& key) {
const auto* v = getObjectValue(obj, key);
if (!v) return std::nullopt;
return v->asBool();
}
inline std::optional<std::string> getObjectString(const DaliValue::Object& obj,
const std::string& key) {
const auto* v = getObjectValue(obj, key);
if (!v) return std::nullopt;
return v->asString();
}

View File

@@ -0,0 +1,22 @@
#pragma once
#include <functional>
#include <mutex>
class DaliQueryScheduler {
public:
static DaliQueryScheduler& instance() {
static DaliQueryScheduler inst;
return inst;
}
template <typename Fn>
auto run(Fn&& action) -> decltype(action()) {
std::lock_guard<std::mutex> lock(mu_);
return action();
}
private:
DaliQueryScheduler() = default;
std::mutex mu_;
};

62
include/sequence.hpp Normal file
View File

@@ -0,0 +1,62 @@
#pragma once
#include "model_value.hpp"
#include <optional>
#include <string>
#include <vector>
// Mirrors lib/dali/sequence.dart command type names.
enum class DaliCommandType {
setBright,
on,
off,
toScene,
setScene,
removeScene,
addToGroup,
removeFromGroup,
setFadeTime,
setFadeRate,
wait,
modifyShortAddress,
deleteShortAddress,
};
std::string toString(DaliCommandType type);
DaliCommandType commandTypeFromString(const std::string& name,
DaliCommandType fallback = DaliCommandType::setBright);
struct DaliCommandParams {
DaliValue::Object data;
DaliCommandParams() = default;
explicit DaliCommandParams(DaliValue::Object d) : data(std::move(d)) {}
int getInt(const std::string& key, int def = 0) const;
DaliCommandParams copy() const;
DaliValue::Object toJson() const;
static DaliCommandParams fromJson(const DaliValue::Object& json);
};
struct SequenceStep {
std::string id;
std::optional<std::string> remark;
DaliCommandType type = DaliCommandType::setBright;
DaliCommandParams params;
SequenceStep copy() const;
DaliValue::Object toJson() const;
static SequenceStep fromJson(const DaliValue::Object& json);
};
struct CommandSequence {
std::string id;
std::string name;
std::vector<SequenceStep> steps;
DaliValue::Object toJson() const;
static CommandSequence fromJson(const DaliValue::Object& json);
};

View File

@@ -0,0 +1,37 @@
#pragma once
#include "model_value.hpp"
#include "sequence.hpp"
#include <functional>
#include <string>
#include <vector>
constexpr const char* kSequencesKey = "command_sequences_v1";
class SequenceRepository {
public:
using LoadCallback = std::function<bool(const std::string& key, DaliValue* out)>;
using SaveCallback = std::function<bool(const std::string& key, const DaliValue& value)>;
SequenceRepository() = default;
SequenceRepository(LoadCallback loadCb, SaveCallback saveCb)
: loadCallback_(std::move(loadCb)), saveCallback_(std::move(saveCb)) {}
void setLoadCallback(LoadCallback cb) { loadCallback_ = std::move(cb); }
void setSaveCallback(SaveCallback cb) { saveCallback_ = std::move(cb); }
bool load();
bool save() const;
const std::vector<CommandSequence>& sequences() const { return sequences_; }
void add(const CommandSequence& s);
void remove(const std::string& id);
void replace(const CommandSequence& s);
private:
std::vector<CommandSequence> sequences_;
LoadCallback loadCallback_;
SaveCallback saveCallback_;
};

386
src/addr.cpp Normal file
View File

@@ -0,0 +1,386 @@
#include "addr.hpp"
#include <algorithm>
#include <chrono>
#include <random>
#include <thread>
DaliAddr::DaliAddr(DaliBase& base) : base_(base) {}
bool DaliAddr::isAllocAddr() const { return base_.isAllocAddr; }
void DaliAddr::setIsAllocAddr(bool value) { base_.isAllocAddr = value; }
int DaliAddr::lastAllocAddr() const { return base_.lastAllocAddr; }
void DaliAddr::setLastAllocAddr(int value) { base_.lastAllocAddr = value; }
void DaliAddr::selectDevice(int address) { base_.selectedAddress = address; }
bool DaliAddr::writeAddr(int addr, int newAddr) {
const int nAddr = newAddr * 2 + 1;
return base_.setDTR(nAddr) && base_.storeDTRAsAddr(addr);
}
bool DaliAddr::removeAddr(int addr) { return base_.setDTR(0xFF) && base_.storeDTRAsAddr(addr); }
bool DaliAddr::removeAllAddr() { return removeAddr(base_.broadcast); }
std::vector<int> DaliAddr::searchAddr(int addr) {
isSearching = true;
onlineDevices.clear();
const int maxAddr = std::clamp(addr, 0, 63);
for (int i = 0; i < maxAddr; i++) {
if (!isSearching) break;
const auto status = base_.getOnlineStatus(i);
if (status.has_value() && status.value()) {
onlineDevices.push_back(i);
}
}
isSearching = false;
return onlineDevices;
}
std::vector<int> DaliAddr::searchAddrRange(int start, int end) {
int s = std::clamp(start, 0, 63);
int e = std::clamp(end, 0, 63);
if (s > e) return {};
isSearching = true;
onlineDevices.clear();
for (int i = s; i <= e; i++) {
if (!isSearching) break;
const auto status = base_.getOnlineStatus(i);
if (status.has_value() && status.value()) {
onlineDevices.push_back(i);
}
}
isSearching = false;
return onlineDevices;
}
void DaliAddr::stopSearch() { isSearching = false; }
bool DaliAddr::compareSingleAddress(int typ, int addr) {
bool ok = false;
if (typ == 1) {
ok = base_.queryAddressH(addr);
} else if (typ == 2) {
ok = base_.queryAddressM(addr);
} else if (typ == 3) {
ok = base_.queryAddressL(addr);
}
if (!ok) return false;
const auto matched = base_.compareAddress();
return matched.has_value() && matched.value();
}
std::pair<int, int> DaliAddr::precompareNew(int typ, int m) {
int min = m;
int max = 255;
while ((max - min) > 6) {
const int mid = static_cast<int>((max - min) / 2.0 + min);
const bool ok = compareSingleAddress(typ, mid);
if (ok) {
max = mid;
} else {
min = mid;
}
}
return {min, max};
}
int DaliAddr::compareAddress(int typ) {
int min = 0;
int max = 255;
int vAct = 0;
const auto mm = precompareNew(typ);
min = mm.first;
max = mm.second;
std::mt19937 rng(528643246);
for (int i = 0; i <= 100; i++) {
if (!base_.isAllocAddr) break;
if (min >= max) break;
std::uniform_int_distribution<int> dist(min, max);
const int v = dist(rng);
const bool res = compareSingleAddress(typ, v);
if (res) {
if (v == 0) {
vAct = v;
compareSingleAddress(typ, v);
break;
}
const bool res2 = compareSingleAddress(typ, v - 1);
if (res2) {
max = v - 1;
} else {
vAct = v - 1;
compareSingleAddress(typ, v - 1);
break;
}
} else if (v <= 254) {
const bool res3 = compareSingleAddress(typ, v + 1);
if (res3) {
vAct = v;
compareSingleAddress(typ, v);
break;
}
min = v + 1;
} else {
break;
}
}
return vAct;
}
int DaliAddr::compareAddressNew(int typ, int m) {
int minVal = m;
int maxVal = 255;
int vAct = 0;
const auto mm = precompareNew(typ, minVal);
minVal = mm.first;
maxVal = mm.second;
int v = maxVal;
for (int i = 0; i <= 10; i++) {
const bool ok = compareSingleAddress(typ, v);
if (ok) {
if (v == 0) {
vAct = v;
compareSingleAddress(typ, v);
break;
}
const bool ok2 = compareSingleAddress(typ, v - 1);
if (ok2) {
maxVal = v - 1;
} else {
vAct = v - 1;
compareSingleAddress(typ, v - 1);
break;
}
}
}
return vAct;
}
DaliCompareAddrResult DaliAddr::compareAddr(int ad, std::optional<int> /*minH*/,
std::optional<int> /*minM*/, std::optional<int> /*minL*/) {
DaliCompareAddrResult result;
result.nextAddr = ad;
if (ad > 63) {
result.nextAddr = 63;
return result;
}
base_.compare(128, 0, 0);
result.retH = compareAddress(1);
result.retM = compareAddress(2);
result.retL = compareAddress(3);
if (!base_.isAllocAddr) {
result.retH = 0;
result.retM = 0;
result.retL = 0;
return result;
}
if (result.retH == 0 && result.retM == 0 && result.retL == 0) {
return result;
}
const auto res = base_.compare(result.retH, result.retM, result.retL + 1);
if (!res.has_value() || !res.value()) {
base_.isAllocAddr = false;
result.retH = 0;
result.retM = 0;
result.retL = 0;
return result;
}
while (true) {
const auto status = base_.getOnlineStatus(ad);
if (!status.has_value() || !status.value()) break;
ad++;
if (ad > 63) break;
}
if (!base_.programShortAddr(ad)) return result;
const auto qsa = base_.queryShortAddr();
if (qsa.has_value() && qsa.value() == ad) {
base_.withdraw();
base_.setBright(ad, 254);
}
result.nextAddr = ad;
return result;
}
int DaliAddr::compareMulti(int h, int m, int l, int ad) {
int addr = ad + 1;
int retL = l;
for (int i = 0; i < 12; i++) {
if (!base_.isAllocAddr) return addr - 1;
retL++;
if (retL > 255) break;
const auto ok = base_.compare(h, m, retL);
if (!ok.has_value() || !ok.value()) {
addr--;
break;
}
while (true) {
const auto status = base_.getOnlineStatus(addr);
if (!status.has_value() || !status.value()) break;
addr++;
}
if (!base_.programShortAddr(addr)) continue;
const auto qsa = base_.queryShortAddr();
if (qsa.has_value() && qsa.value() == addr) {
base_.withdraw();
base_.setBright(addr, 254);
addr++;
}
}
return addr;
}
bool DaliAddr::allocateAllAddr(int ads) {
int ad = ads;
base_.isAllocAddr = true;
base_.lastAllocAddr = 255;
for (int i = 0; i <= 80; i++) {
if (!base_.isAllocAddr) break;
bool anyDevice = false;
const auto dev1 = base_.compare(255, 255, 255);
if (dev1.has_value() && dev1.value()) {
anyDevice = true;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
} else {
const auto dev2 = base_.compare(255, 255, 255);
if (dev2.has_value() && dev2.value()) {
anyDevice = true;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
} else {
const auto dev3 = base_.compare(255, 255, 255);
if (dev3.has_value() && dev3.value()) {
anyDevice = true;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
}
if (!anyDevice) {
break;
}
const auto retVals = compareAddr(ad, std::nullopt, std::nullopt, std::nullopt);
ad = retVals.nextAddr;
if (!base_.isAllocAddr) break;
if (!(retVals.retH == 0 && retVals.retM == 0 && retVals.retL == 0)) {
i = 0;
}
ad = compareMulti(retVals.retH, retVals.retM, retVals.retL + 1, ad);
base_.lastAllocAddr = ad;
ad++;
if (ad > 63) {
base_.isAllocAddr = false;
break;
}
if (i == 80) {
base_.isAllocAddr = false;
break;
}
}
base_.isAllocAddr = false;
if (ad <= 0) {
base_.lastAllocAddr = 255;
} else {
base_.lastAllocAddr = ad - 1;
}
return true;
}
void DaliAddr::stopAllocAddr() { base_.isAllocAddr = false; }
bool DaliAddr::removeFromScene(int addr, int scene) {
const int value = scene + 80;
return base_.send(addr, static_cast<uint8_t>(value));
}
std::optional<int> DaliAddr::getSceneBright(int addr, int scene) {
const int value = scene + 176;
return base_.query(addr, static_cast<uint8_t>(value));
}
bool DaliAddr::resetAndAllocAddr(int n, bool removeAddrFirst, bool closeLight) {
const int startTime = static_cast<int>(base_.mcuTicks());
base_.isAllocAddr = true;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (closeLight) {
base_.off(base_.broadcast);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
if (!base_.terminate()) {
base_.isAllocAddr = false;
return false;
}
std::this_thread::sleep_for(std::chrono::milliseconds(200));
if (removeAddrFirst) {
if (!base_.initialiseAll()) {
base_.isAllocAddr = false;
return false;
}
std::this_thread::sleep_for(std::chrono::milliseconds(200));
if (!removeAllAddr()) {
base_.isAllocAddr = false;
return false;
}
} else {
if (!base_.initialise()) {
base_.isAllocAddr = false;
return false;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(500));
if (!base_.randomise()) {
base_.isAllocAddr = false;
return false;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (!base_.randomise()) {
base_.isAllocAddr = false;
return false;
}
std::this_thread::sleep_for(std::chrono::milliseconds(300));
const bool ok = allocateAllAddr(n);
(void)startTime;
return ok;
}

396
src/base.cpp Normal file
View File

@@ -0,0 +1,396 @@
#include "base.hpp"
#include "dali_define.hpp"
#include <algorithm>
DaliStatus DaliStatus::fromByte(uint8_t status) {
DaliStatus s;
s.controlGearPresent = (status & 0x01) != 0;
s.lampFailure = (status & 0x02) != 0;
s.lampPowerOn = (status & 0x04) != 0;
s.limitError = (status & 0x08) != 0;
s.fadingCompleted = (status & 0x10) != 0;
s.resetState = (status & 0x20) != 0;
s.missingShortAddress = (status & 0x40) != 0;
s.psFault = (status & 0x80) != 0;
return s;
}
DaliBase::DaliBase(DaliComm& comm) : comm_(comm) {}
int64_t DaliBase::mcuTicks() const {
const auto now = std::chrono::time_point_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now());
return now.time_since_epoch().count();
}
uint8_t DaliBase::encodeCmdAddr(int dec_addr) { return DaliComm::toCmdAddr(dec_addr); }
uint8_t DaliBase::encodeArcAddr(int dec_addr) { return DaliComm::toArcAddr(dec_addr); }
bool DaliBase::toScene(int a, int s) {
const int scene = DALI_CMD_GO_TO_SCENE(s);
return send(a, static_cast<uint8_t>(scene));
}
bool DaliBase::reset(int a, int /*t*/) { return send(a, DALI_CMD_RESET); }
int DaliBase::brightnessToLog(int brightness) const {
const double val = std::log(static_cast<double>(brightness) + 1.0) / std::log(256.0) * 255.0;
return static_cast<int>(val);
}
int DaliBase::logToBrightness(int logValue) const {
const double val = std::pow(10.0, logValue * std::log(256.0) / std::log(10.0)) - 1.0;
return static_cast<int>(val);
}
bool DaliBase::setBright(int a, int b) {
int bright = std::clamp(b, 0, 254);
const auto addr = encodeArcAddr(a);
return comm_.sendCmd(addr, static_cast<uint8_t>(bright));
}
bool DaliBase::setBrightPercentage(int a, double b) {
int bright = static_cast<int>(b * 254.0 / 100.0);
bright = std::clamp(bright, 0, 254);
return setBright(a, bright);
}
bool DaliBase::stopFade(int a) { return sendCmd(encodeCmdAddr(a), DALI_CMD_STOP_FADE); }
bool DaliBase::off(int a) { return sendCmd(encodeCmdAddr(a), DALI_CMD_OFF); }
bool DaliBase::on(int a) { return sendCmd(encodeCmdAddr(a), DALI_CMD_RECALL_MAX); }
bool DaliBase::recallMaxLevel(int a) { return sendCmd(encodeCmdAddr(a), DALI_CMD_RECALL_MAX); }
bool DaliBase::recallMinLevel(int a) { return sendCmd(encodeCmdAddr(a), DALI_CMD_RECALL_MIN); }
int DaliBase::groupToAddr(int gp) const { return 64 + gp; }
bool DaliBase::send(int a, uint8_t cmd) { return sendCmd(encodeCmdAddr(a), cmd); }
bool DaliBase::sendCmd(uint8_t addr, uint8_t cmd) { return comm_.sendCmd(addr, cmd); }
bool DaliBase::sendExtCmd(int cmd, int value) { return comm_.sendExtCmd(static_cast<uint8_t>(cmd), static_cast<uint8_t>(value)); }
bool DaliBase::setDTR(int value) { return comm_.sendCmd(DALI_CMD_SPECIAL_SET_DTR0, static_cast<uint8_t>(value)); }
bool DaliBase::setDTR1(int value) { return comm_.sendCmd(DALI_CMD_SPECIAL_SET_DTR_1, static_cast<uint8_t>(value)); }
bool DaliBase::setDTR2(int value) { return comm_.sendCmd(DALI_CMD_SPECIAL_SET_DTR_2, static_cast<uint8_t>(value)); }
std::optional<int> DaliBase::getDTR(int a) { return query(a, DALI_CMD_QUERY_CONTENT_DTR); }
std::optional<int> DaliBase::getDTR1(int a) { return query(a, DALI_CMD_QUERY_CONTENT_DTR1); }
std::optional<int> DaliBase::getDTR2(int a) { return query(a, DALI_CMD_QUERY_CONTENT_DTR2); }
bool DaliBase::copyCurrentBrightToDTR(int a) {
return sendExtCmd(encodeCmdAddr(a), DALI_CMD_STORE_ACTUAL_LEVEL_IN_THE_DTR);
}
bool DaliBase::queryColourValue(int a) { return sendExtCmd(encodeCmdAddr(a), DALI_CMD_QUERY_COLOR_VALUE); }
bool DaliBase::storeDTRAsAddr(int a) { return sendExtCmd(encodeCmdAddr(a), DALI_CMD_STORE_DTR_AS_SHORT_ADDRESS); }
bool DaliBase::storeDTRAsSceneBright(int a, int s) {
const int value = DALI_CMD_SET_SCENE(s);
return sendExtCmd(encodeCmdAddr(a), value);
}
bool DaliBase::storeScene(int a, int s) {
return copyCurrentBrightToDTR(a) && storeDTRAsSceneBright(a, s);
}
bool DaliBase::removeScene(int a, int s) {
const int value = DALI_CMD_REMOVE_SCENE(s);
return sendExtCmd(encodeCmdAddr(a), value);
}
bool DaliBase::addToGroup(int a, int g) {
const int value = DALI_CMD_ADD_TO_GROUP(g);
return sendExtCmd(encodeCmdAddr(a), value);
}
bool DaliBase::removeFromGroup(int a, int g) {
const int value = DALI_CMD_REMOVE_FROM_GROUP(g);
return sendExtCmd(encodeCmdAddr(a), value);
}
bool DaliBase::storeDTRAsFadeTime(int a) { return sendExtCmd(encodeCmdAddr(a), DALI_CMD_STORE_THE_DTR_AS_FADE_TIME); }
bool DaliBase::storeDTRAsFadeRate(int a) { return sendExtCmd(encodeCmdAddr(a), DALI_CMD_STORE_THE_DTR_AS_FADE_RATE); }
bool DaliBase::storeDTRAsPoweredBright(int a) {
return sendExtCmd(encodeCmdAddr(a), DALI_CMD_STORE_THE_DTR_AS_PWR_ON_LEVEL);
}
bool DaliBase::storeDTRAsSystemFailureLevel(int a) {
return sendExtCmd(encodeCmdAddr(a), DALI_CMD_STORE_THE_DTR_AS_SYS_FAIL_LEVEL);
}
bool DaliBase::storeDTRAsMinLevel(int a) { return sendExtCmd(encodeCmdAddr(a), DALI_CMD_STORE_THE_DTR_AS_MIN_LEVEL); }
bool DaliBase::storeDTRAsMaxLevel(int a) { return sendExtCmd(encodeCmdAddr(a), DALI_CMD_STORE_THE_DTR_AS_MAX_LEVEL); }
bool DaliBase::storeColourTempLimits(int a) {
return sendExtCmd(encodeCmdAddr(a), DALI_CMD_DT8_STORE_COLOR_TEMPERATURE_LIMIT);
}
std::optional<bool> DaliBase::getOnlineStatus(int a) {
const auto status = query(a, DALI_CMD_QUERY_BALLAST);
if (!status.has_value()) return std::nullopt;
return status.value() == 255;
}
std::optional<int> DaliBase::getBright(int a) {
const auto res = query(a, DALI_CMD_QUERY_ACTUAL_LEVEL);
if (!res.has_value()) return std::nullopt;
if (res.value() == 255) return 254;
return res;
}
std::optional<int> DaliBase::getDeviceType(int a) { return queryCmd(encodeCmdAddr(a), DALI_CMD_QUERY_DEVICE_TYPE); }
std::optional<int> DaliBase::getPhysicalMinLevel(int a) {
return queryCmd(encodeCmdAddr(a), DALI_CMD_QUERY_PHYSICAL_MINIMUM_LEVEL);
}
std::optional<int> DaliBase::getDeviceVersion(int a) {
return queryCmd(encodeCmdAddr(a), DALI_CMD_QUERY_VERSION_NUMBER);
}
bool DaliBase::dtSelect(int value) { return comm_.sendCmd(DALI_CMD_SPECIAL_DT_SELECT, static_cast<uint8_t>(value)); }
bool DaliBase::activate(int a) { return comm_.sendCmd(encodeCmdAddr(a), DALI_CMD_DT8_ACTIVATE); }
bool DaliBase::setDTRAsColourX(int a) { return sendExtCmd(encodeCmdAddr(a), DALI_CMD_DT8_STORE_DTR_AS_COLORX); }
bool DaliBase::setDTRAsColourY(int a) { return sendExtCmd(encodeCmdAddr(a), DALI_CMD_DT8_STORE_DTR_AS_COLORY); }
bool DaliBase::setDTRAsColourRGB(int a) { return sendExtCmd(encodeCmdAddr(a), DALI_CMD_DT8_ACTIVATE); }
bool DaliBase::setDTRAsColourTemp(int a) { return sendExtCmd(encodeCmdAddr(a), DALI_CMD_DT8_SET_COLOR_TEMPERATURE); }
bool DaliBase::copyReportColourToTemp(int a) {
if (!dtSelect(8)) return false;
return sendExtCmd(encodeCmdAddr(a), DALI_CMD_DT8_COPY_REPORT_TO_TEMPORARY);
}
bool DaliBase::setGradualChangeSpeed(int a, int value) { return setDTR(value) && storeDTRAsFadeTime(a); }
bool DaliBase::setGradualChangeRate(int a, int value) { return setDTR(value) && storeDTRAsFadeRate(a); }
std::optional<std::pair<int, int>> DaliBase::getGradualChange(int a) {
const auto ret = query(a, DALI_CMD_QUERY_FADE_TIME_FADE_RATE);
if (!ret.has_value()) return std::nullopt;
int speed = ret.value();
int rate = 0;
while (speed > 15) {
speed -= 16;
rate++;
}
return std::make_pair(rate, speed);
}
std::optional<int> DaliBase::getGradualChangeRate(int a) {
const auto rs = getGradualChange(a);
if (!rs.has_value()) return std::nullopt;
return rs->second;
}
std::optional<int> DaliBase::getGradualChangeSpeed(int a) {
const auto rs = getGradualChange(a);
if (!rs.has_value()) return std::nullopt;
return rs->first;
}
bool DaliBase::setPowerOnLevel(int a, int value) { return setDTR(value) && storeDTRAsPoweredBright(a); }
std::optional<int> DaliBase::getPowerOnLevel(int a) { return query(a, DALI_CMD_QUERY_MAX_LEVEL); }
bool DaliBase::setSystemFailureLevel(int a, int value) { return setDTR(value) && storeDTRAsSystemFailureLevel(a); }
std::optional<int> DaliBase::getSystemFailureLevel(int a) { return query(a, DALI_CMD_QUERY_MIN_LEVEL); }
bool DaliBase::setMinLevel(int a, int value) { return setDTR(value) && storeDTRAsMinLevel(a); }
std::optional<int> DaliBase::getMinLevel(int a) { return query(a, DALI_CMD_QUERY_POWER_ON_LEVEL); }
bool DaliBase::setMaxLevel(int a, int value) { return setDTR(value) && storeDTRAsMaxLevel(a); }
std::optional<int> DaliBase::getMaxLevel(int a) { return query(a, DALI_CMD_QUERY_SYSTEM_FAILURE_LEVEL); }
bool DaliBase::setFadeTime(int a, int value) {
int v = value;
if (v > 15) v = 15;
return setDTR(v) && storeDTRAsFadeTime(a);
}
std::optional<int> DaliBase::getFadeTime(int a) {
const auto rs = getGradualChange(a);
if (!rs.has_value()) return std::nullopt;
return rs->first;
}
bool DaliBase::setFadeRate(int a, int value) { return setDTR(value) && storeDTRAsFadeRate(a); }
std::optional<int> DaliBase::getFadeRate(int a) {
const auto rs = getGradualChange(a);
if (!rs.has_value()) return std::nullopt;
return rs->second;
}
std::optional<int> DaliBase::getNextDeviceType(int a) { return query(a, DALI_CMD_QUERY_NEXT_DEVICE_TYPE); }
std::optional<int> DaliBase::getGroupH(int a) { return query(a, DALI_CMD_QUERY_GROUP_8_15); }
std::optional<int> DaliBase::getGroupL(int a) { return query(a, DALI_CMD_QUERY_GROUPS_0_7); }
std::optional<int> DaliBase::getGroup(int a) {
const auto h = getGroupH(a);
const auto l = getGroupL(a);
if (!h.has_value() || !l.has_value()) return std::nullopt;
return h.value() * 256 + l.value();
}
bool DaliBase::setGroup(int a, int value) {
auto currentGroupOpt = getGroup(a);
int currentGroup = currentGroupOpt.value_or(-1);
for (int i = 0; i < 16; i++) {
if (currentGroup != -1 && (currentGroup & (1 << i)) == (value & (1 << i))) {
continue;
}
if ((value & (1 << i)) != 0) {
if (!addToGroup(a, i)) return false;
} else {
if (!removeFromGroup(a, i)) return false;
}
}
return true;
}
std::optional<int> DaliBase::getScene(int a, int b) {
return query(a, static_cast<uint8_t>(DALI_CMD_QUERY_SCENE_LEVEL(b)));
}
bool DaliBase::setScene(int a, int b) { return setDTR(b) && storeDTRAsSceneBright(a, b); }
std::map<int, int> DaliBase::getScenes(int a) {
std::map<int, int> ret;
for (int i = 0; i < 16; i++) {
const auto r = getScene(a, i);
if (r.has_value()) ret[i] = r.value();
}
return ret;
}
std::optional<int> DaliBase::getStatus(int a) { return query(a, DALI_CMD_QUERY_STATUS); }
std::optional<bool> DaliBase::getControlGearPresent(int a) {
const auto ret = query(a, DALI_CMD_QUERY_BALLAST);
if (!ret.has_value()) return std::nullopt;
return ret.value() == 255;
}
std::optional<bool> DaliBase::getLampFailureStatus(int a) {
const auto ret = query(a, DALI_CMD_QUERY_LAMP_FAILURE);
if (!ret.has_value()) return std::nullopt;
return ret.value() == 255;
}
std::optional<bool> DaliBase::getLampPowerOnStatus(int a) {
const auto ret = query(a, DALI_CMD_QUERY_LAMP_POWER_ON);
if (!ret.has_value()) return std::nullopt;
return ret.value() == 255;
}
std::optional<bool> DaliBase::getLimitError(int a) {
const auto ret = query(a, DALI_CMD_QUERY_LIMIT_ERROR);
if (!ret.has_value()) return std::nullopt;
return ret.value() == 255;
}
std::optional<bool> DaliBase::getResetStatus(int a) {
const auto ret = query(a, DALI_CMD_QUERY_RESET_STATE);
if (!ret.has_value()) return std::nullopt;
return ret.value() == 255;
}
std::optional<bool> DaliBase::getMissingShortAddress(int a) {
const auto ret = query(a, DALI_CMD_QUERY_MISSING_SHORT_ADDRESS);
if (!ret.has_value()) return std::nullopt;
return ret.value() == 255;
}
bool DaliBase::terminate() { return comm_.sendCmd(DALI_CMD_SPECIAL_TERMINATE, DALI_CMD_OFF); }
bool DaliBase::randomise() { return comm_.sendExtCmd(DALI_CMD_SPECIAL_RANDOMIZE, DALI_CMD_OFF); }
bool DaliBase::initialiseAll() { return comm_.sendExtCmd(DALI_CMD_SPECIAL_INITIALIZE, DALI_CMD_OFF); }
bool DaliBase::initialise() { return comm_.sendExtCmd(DALI_CMD_SPECIAL_INITIALIZE, DALI_CMD_STOP_FADE); }
bool DaliBase::withdraw() { return comm_.sendCmd(DALI_CMD_SPECIAL_WITHDRAW, DALI_CMD_OFF); }
bool DaliBase::cancel() { return comm_.sendCmd(DALI_CMD_SPECIAL_CANCEL, DALI_CMD_OFF); }
bool DaliBase::physicalSelection() {
return comm_.sendCmd(DALI_CMD_SPECIAL_PHYSICAL_SELECTION, DALI_CMD_OFF);
}
bool DaliBase::queryAddressH(int addr) {
return comm_.sendCmd(DALI_CMD_SPECIAL_SEARCHADDRH, static_cast<uint8_t>(addr));
}
bool DaliBase::queryAddressM(int addr) {
return comm_.sendCmd(DALI_CMD_SPECIAL_SEARCHADDRM, static_cast<uint8_t>(addr));
}
bool DaliBase::queryAddressL(int addr) {
return comm_.sendCmd(DALI_CMD_SPECIAL_SEARCHADDRL, static_cast<uint8_t>(addr));
}
bool DaliBase::programShortAddr(int a) {
return comm_.sendCmd(DALI_CMD_SPECIAL_PROGRAM_SHORT_ADDRESS, encodeCmdAddr(a));
}
std::optional<int> DaliBase::queryShortAddr() {
const auto ret1 = queryCmd(DALI_CMD_SPECIAL_QUERY_SHORT_ADDRESS, DALI_CMD_OFF);
if (!ret1.has_value()) return std::nullopt;
int ret = ret1.value() - 1;
return ret / 2;
}
std::optional<bool> DaliBase::verifyShortAddr(int a) {
const auto res = queryCmd(DALI_CMD_SPECIAL_VERIFY_SHORT_ADDRESS, encodeCmdAddr(a));
if (!res.has_value()) return std::nullopt;
return true;
}
std::optional<bool> DaliBase::compareAddress() {
const auto ret = queryCmd(DALI_CMD_SPECIAL_COMPARE, DALI_CMD_OFF);
if (!ret.has_value()) return std::nullopt;
return ret.value() >= 0;
}
std::optional<bool> DaliBase::compare(int h, int m, int l) {
if (!queryAddressL(l)) return std::nullopt;
if (!queryAddressM(m)) return std::nullopt;
if (!queryAddressH(h)) return std::nullopt;
const auto matched = compareAddress();
if (!matched.has_value()) return std::nullopt;
return matched.value();
}
std::optional<int> DaliBase::getRandomAddrH(int addr) { return query(addr, DALI_CMD_QUERY_RANDOM_ADDRESS_H); }
std::optional<int> DaliBase::getRandomAddrM(int addr) { return query(addr, DALI_CMD_QUERY_RANDOM_ADDRESS_M); }
std::optional<int> DaliBase::getRandomAddrL(int addr) { return query(addr, DALI_CMD_QUERY_RANDOM_ADDRESS_L); }
std::optional<int> DaliBase::query(int a, uint8_t cmd) { return comm_.queryCmd(encodeCmdAddr(a), cmd); }
std::optional<int> DaliBase::queryCmd(uint8_t addr, uint8_t cmd) { return comm_.queryCmd(addr, cmd); }

145
src/color.cpp Normal file
View File

@@ -0,0 +1,145 @@
#include "color.hpp"
#include <algorithm>
#include <cmath>
std::array<int, 4> DaliColor::toIntList(double a, double r, double g, double b) {
const int ai = static_cast<int>(a * 255.0);
const int ri = static_cast<int>(r * 255.0);
const int gi = static_cast<int>(g * 255.0);
const int bi = static_cast<int>(b * 255.0);
return {ai, ri, gi, bi};
}
int DaliColor::toInt(double a, double r, double g, double b) {
const auto c = toIntList(a, r, g, b);
return (c[0] << 24) | (c[1] << 16) | (c[2] << 8) | c[3];
}
double DaliColor::decimalRound(int num, double idp) {
const double mult = std::pow(10.0, idp);
return (num * mult + 0.5) / mult;
}
std::array<double, 3> DaliColor::gammaCorrection(double r, double g, double b, double gamma) {
return {std::pow(r, gamma), std::pow(g, gamma), std::pow(b, gamma)};
}
std::array<double, 3> DaliColor::rgb2xyz(double r, double g, double b) {
const double lr = srgbToLinear(r);
const double lg = srgbToLinear(g);
const double lb = srgbToLinear(b);
const double x = 0.412453 * lr + 0.357580 * lg + 0.180423 * lb;
const double y = 0.212671 * lr + 0.715160 * lg + 0.072169 * lb;
const double z = 0.019334 * lr + 0.119193 * lg + 0.950227 * lb;
return {x, y, z};
}
std::array<int, 3> DaliColor::xyz2rgb(double x, double y, double z) {
const double lr = 3.240479 * x - 1.537150 * y - 0.498535 * z;
const double lg = -0.969256 * x + 1.875992 * y + 0.041556 * z;
const double lb = 0.055648 * x - 0.204043 * y + 1.057311 * z;
const double sr = linearToSrgb(lr);
const double sg = linearToSrgb(lg);
const double sb = linearToSrgb(lb);
auto toChannel = [](double v) {
return static_cast<int>(std::clamp(std::round(v * 255.0), 0.0, 255.0));
};
return {toChannel(sr), toChannel(sg), toChannel(sb)};
}
std::array<double, 2> DaliColor::xyz2xy(double x, double y, double z) {
const double sum = x + y + z;
if (sum == 0.0) return {0.0, 0.0};
return {x / sum, y / sum};
}
std::array<double, 3> DaliColor::xy2xyz(double xVal, double yVal) {
if (yVal == 0.0) return {0.0, 0.0, 0.0};
const double x = xVal / yVal;
const double y = 1.0;
const double z = (1.0 - xVal - yVal) / yVal;
return {x, y, z};
}
std::array<double, 2> DaliColor::rgb2xy(double r, double g, double b) {
const auto xyz = rgb2xyz(r, g, b);
const auto xy = xyz2xy(xyz[0], xyz[1], xyz[2]);
return {xy[0], xy[1]};
}
std::array<int, 3> DaliColor::xy2rgb(double xVal, double yVal) {
const auto xyz = xy2xyz(xVal, yVal);
return xyz2rgb(xyz[0], xyz[1], xyz[2]);
}
std::array<double, 3> DaliColor::xyz2lab(double x, double y, double z) {
constexpr double xn = 0.950456;
constexpr double yn = 1.000000;
constexpr double zn = 1.088754;
double fx = x / xn;
double fy = y / yn;
double fz = z / zn;
auto f = [](double v) {
return (v > 0.008856) ? std::pow(v, 1.0 / 3.0) : (7.787 * v) + (16.0 / 116.0);
};
fx = f(fx);
fy = f(fy);
fz = f(fz);
const double l = (116.0 * fy) - 16.0;
const double a = 500.0 * (fx - fy);
const double b = 200.0 * (fy - fz);
return {l, a, b};
}
std::array<double, 3> DaliColor::rgb2lab(double r, double g, double b) {
const auto xyz = rgb2xyz(r, g, b);
const auto lab = xyz2lab(xyz[0], xyz[1], xyz[2]);
return {lab[0], lab[1], lab[2]};
}
std::array<double, 3> DaliColor::lab2xyz(double l, double a, double b) {
const double fy = (l + 16.0) / 116.0;
const double fx = fy + (a / 500.0);
const double fz = fy - (b / 200.0);
const double fx3 = fx * fx * fx;
const double fy3 = fy * fy * fy;
const double fz3 = fz * fz * fz;
const double xr = (fx3 > 0.008856) ? fx3 : ((fx - (16.0 / 116.0)) / 7.787);
const double yr = (fy3 > 0.008856) ? fy3 : ((fy - (16.0 / 116.0)) / 7.787);
const double zr = (fz3 > 0.008856) ? fz3 : ((fz - (16.0 / 116.0)) / 7.787);
constexpr double xn = 0.950456;
constexpr double yn = 1.000000;
constexpr double zn = 1.088754;
return {xr * xn, yr * yn, zr * zn};
}
std::array<int, 3> DaliColor::lab2rgb(double l, double a, double b) {
const auto xyz = lab2xyz(l, a, b);
return xyz2rgb(xyz[0], xyz[1], xyz[2]);
}
double DaliColor::srgbToLinear(double value) {
const double v = std::clamp(value, 0.0, 1.0);
if (v <= 0.04045) {
return v / 12.92;
}
return std::pow((v + 0.055) / 1.055, 2.4);
}
double DaliColor::linearToSrgb(double value) {
if (value <= 0.0) return 0.0;
const double srgb = (value <= 0.0031308) ? (value * 12.92) : (1.055 * std::pow(value, 1.0 / 2.4) - 0.055);
return std::clamp(srgb, 0.0, 1.0);
}

156
src/dali_comm.cpp Normal file
View File

@@ -0,0 +1,156 @@
#include "dali_comm.hpp"
#include <chrono>
#include <thread>
#ifdef ESP_PLATFORM
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#endif
DaliComm::DaliComm(SendCallback send_cb,
ReadCallback read_cb,
TransactCallback transact_cb,
DelayCallback delay_cb)
: send_(std::move(send_cb)),
read_(std::move(read_cb)),
transact_(std::move(transact_cb)),
delay_(std::move(delay_cb)) {}
void DaliComm::setSendCallback(SendCallback cb) { send_ = std::move(cb); }
void DaliComm::setReadCallback(ReadCallback cb) { read_ = std::move(cb); }
void DaliComm::setTransactCallback(TransactCallback cb) { transact_ = std::move(cb); }
void DaliComm::setDelayCallback(DelayCallback cb) { delay_ = std::move(cb); }
std::vector<uint8_t> DaliComm::checksum(const std::vector<uint8_t>& data) {
std::vector<uint8_t> out = data;
uint32_t sum = 0;
for (const auto b : out) {
sum += b;
}
out.push_back(static_cast<uint8_t>(sum & 0xFF));
return out;
}
bool DaliComm::write(const std::vector<uint8_t>& data) const { return writeFrame(data); }
std::vector<uint8_t> DaliComm::read(size_t len, uint32_t timeout_ms) const {
if (!read_) return {};
return read_(len, timeout_ms);
}
int DaliComm::checkGatewayType(int gateway) const {
if (!transact_) return 0;
const std::vector<uint8_t> usbProbe{0x01, 0x00, 0x00};
const std::vector<uint8_t> legacyProbe{0x28, 0x01, static_cast<uint8_t>(gateway), 0x11, 0x00, 0x00,
0xFF};
const std::vector<uint8_t> newProbe{0x28, 0x01, static_cast<uint8_t>(gateway), 0x11, 0x00,
0x00, 0x00, 0x00, 0x00, 0xFF};
const auto usbResp = transact_(usbProbe.data(), usbProbe.size());
if (!usbResp.empty() && usbResp[0] > 0) return 1;
const auto legacyResp = transact_(legacyProbe.data(), legacyProbe.size());
if (legacyResp.size() >= 2 && legacyResp[0] == gateway) return 2;
const auto newResp = transact_(newProbe.data(), newProbe.size());
if (newResp.size() >= 2 && newResp[0] == gateway) return 3;
return 0;
}
void DaliComm::flush() const {
if (!read_) return;
for (int i = 0; i < 10; i++) {
const auto data = read_(2, 100);
if (data.empty()) break;
}
}
bool DaliComm::resetBus() const {
const std::vector<uint8_t> frame{0x00, 0x00, 0x00};
if (!write(frame)) return false;
sleepMs(100);
if (!write(frame)) return false;
sleepMs(100);
return true;
}
bool DaliComm::sendRaw(uint8_t addr, uint8_t cmd) const { return sendCmd(addr, cmd); }
bool DaliComm::sendRawNew(uint8_t addr, uint8_t cmd, bool needVerify) const {
if (!needVerify) return sendCmd(addr, cmd);
return queryCmd(addr, cmd).has_value();
}
bool DaliComm::sendExtRaw(uint8_t addr, uint8_t cmd) const { return sendExtCmd(addr, cmd); }
bool DaliComm::sendExtRawNew(uint8_t addr, uint8_t cmd) const { return sendExtCmd(addr, cmd); }
std::optional<uint8_t> DaliComm::queryRaw(uint8_t addr, uint8_t cmd) const { return queryCmd(addr, cmd); }
std::optional<uint8_t> DaliComm::queryRawNew(uint8_t addr, uint8_t cmd) const {
return queryCmd(addr, cmd);
}
bool DaliComm::send(int dec_addr, uint8_t cmd) const { return sendCmd(toCmdAddr(dec_addr), cmd); }
std::optional<uint8_t> DaliComm::query(int dec_addr, uint8_t cmd) const {
return queryCmd(toCmdAddr(dec_addr), cmd);
}
bool DaliComm::getBusStatus() const { return checkGatewayType(0) > 0; }
uint8_t DaliComm::toCmdAddr(int dec_addr) { return static_cast<uint8_t>(dec_addr * 2 + 1); }
uint8_t DaliComm::toArcAddr(int dec_addr) { return static_cast<uint8_t>(dec_addr * 2); }
bool DaliComm::writeFrame(const std::vector<uint8_t>& frame) const {
if (!send_) return false;
return send_(frame.data(), frame.size());
}
void DaliComm::sleepMs(uint32_t ms) const {
if (delay_) {
delay_(ms);
return;
}
#ifdef ESP_PLATFORM
vTaskDelay(pdMS_TO_TICKS(ms));
#else
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
#endif
}
bool DaliComm::sendCmd(uint8_t addr, uint8_t cmd) const {
const std::vector<uint8_t> frame{0x10, addr, cmd};
return writeFrame(frame);
}
bool DaliComm::sendExtCmd(uint8_t addr, uint8_t cmd) const {
const std::vector<uint8_t> frame{0x11, addr, cmd};
const bool ret = writeFrame(frame);
sleepMs(100);
return ret;
}
std::optional<uint8_t> DaliComm::queryCmd(uint8_t addr, uint8_t cmd) const {
if (!transact_) return std::nullopt;
const std::vector<uint8_t> frame{0x12, addr, cmd};
const auto resp = transact_(frame.data(), frame.size());
if (resp.empty()) return std::nullopt;
// Gateway type 1 returns: 0xFF <data> on success; 0xFE no response; 0xFD invalid frame.
if (resp.size() == 1) {
if (resp[0] == 0xFE || resp[0] == 0xFD) return std::nullopt;
return resp[0];
}
if (resp[0] == 0xFF && resp.size() >= 2) {
return resp[1];
}
return resp.back();
}

418
src/decode.cpp Normal file
View File

@@ -0,0 +1,418 @@
#include "decode.hpp"
#include "base.hpp"
#include "dali_define.hpp"
#include <algorithm>
#include <chrono>
#include <iomanip>
#include <sstream>
DaliDecode::DaliDecode() {
cmd_ = {
{DALI_CMD_OFF, "OFF"},
{DALI_CMD_RECALL_MAX_LEVEL, "RECALL_MAX_LEVEL"},
{DALI_CMD_RECALL_MIN_LEVEL, "RECALL_MIN_LEVEL"},
{DALI_CMD_RESET, "RESET"},
{DALI_CMD_STORE_ACTUAL_LEVEL_IN_THE_DTR, "STORE_ACTUAL_LEVEL_IN_THE_DTR"},
{DALI_CMD_STORE_THE_DTR_AS_MAX_LEVEL, "STORE_THE_DTR_AS_MAX_LEVEL"},
{DALI_CMD_STORE_THE_DTR_AS_MIN_LEVEL, "STORE_THE_DTR_AS_MIN_LEVEL"},
{DALI_CMD_STORE_THE_DTR_AS_SYS_FAIL_LEVEL, "STORE_THE_DTR_AS_SYS_FAIL_LEVEL"},
{DALI_CMD_STORE_THE_DTR_AS_PWR_ON_LEVEL, "STORE_THE_DTR_AS_PWR_ON_LEVEL"},
{DALI_CMD_STORE_THE_DTR_AS_FADE_TIME, "STORE_THE_DTR_AS_FADE_TIME"},
{DALI_CMD_STORE_THE_DTR_AS_FADE_RATE, "STORE_THE_DTR_AS_FADE_RATE"},
{DALI_CMD_STORE_DTR_AS_SHORT_ADDRESS, "STORE_DTR_AS_SHORT_ADDRESS"},
{DALI_CMD_QUERY_STATUS, "QUERY_STATUS"},
{DALI_CMD_QUERY_BALLAST, "QUERY_BALLAST"},
{DALI_CMD_QUERY_LAMP_FAILURE, "QUERY_LAMP_FAILURE"},
{DALI_CMD_QUERY_LAMP_POWER_ON, "QUERY_LAMP_POWER_ON"},
{DALI_CMD_QUERY_LIMIT_ERROR, "QUERY_LIMIT_ERROR"},
{DALI_CMD_QUERY_RESET_STATE, "QUERY_RESET_STATE"},
{DALI_CMD_QUERY_MISSING_SHORT_ADDRESS, "QUERY_MISSING_SHORT_ADDRESS"},
{DALI_CMD_QUERY_VERSION_NUMBER, "QUERY_VERSION_NUMBER"},
{DALI_CMD_QUERY_CONTENT_DTR, "QUERY_CONTENT_DTR"},
{DALI_CMD_QUERY_DEVICE_TYPE, "QUERY_DEVICE_TYPE"},
{DALI_CMD_QUERY_PHYSICAL_MINIMUM_LEVEL, "QUERY_PHYSICAL_MINIMUM_LEVEL"},
{DALI_CMD_QUERY_CONTENT_DTR1, "QUERY_CONTENT_DTR1"},
{DALI_CMD_QUERY_CONTENT_DTR2, "QUERY_CONTENT_DTR2"},
{DALI_CMD_QUERY_OPERATING_MODE, "QUERY_OPERATING_MODE"},
{DALI_CMD_QUERY_LIGHT_SOURCE_TYPE, "QUERY_LIGHT_SOURCE_TYPE"},
{DALI_CMD_QUERY_ACTUAL_LEVEL, "QUERY_ACTUAL_LEVEL"},
{DALI_CMD_QUERY_MAX_LEVEL, "QUERY_MAX_LEVEL"},
{DALI_CMD_QUERY_MIN_LEVEL, "QUERY_MIN_LEVEL"},
{DALI_CMD_QUERY_POWER_ON_LEVEL, "QUERY_POWER_ON_LEVEL"},
{DALI_CMD_QUERY_SYSTEM_FAILURE_LEVEL, "QUERY_SYSTEM_FAILURE_LEVEL"},
{DALI_CMD_QUERY_FADE_TIME_FADE_RATE, "QUERY_FADE_TIME/FADE_RATE"},
{DALI_CMD_QUERY_NEXT_DEVICE_TYPE, "QUERY_NEXT_DEVICE_TYPE"},
{DALI_CMD_QUERY_GROUPS_0_7, "QUERY_GROUPS_0-7"},
{DALI_CMD_QUERY_GROUP_8_15, "QUERY_GROUP_8-15"},
{DALI_CMD_QUERY_RANDOM_ADDRESS_H, "QUERY_RANDOM_ADDRESS_(H)"},
{DALI_CMD_QUERY_RANDOM_ADDRESS_M, "QUERY_RANDOM_ADDRESS_(M)"},
{DALI_CMD_QUERY_RANDOM_ADDRESS_L, "QUERY_RANDOM_ADDRESS_(L)"},
{DALI_CMD_READ_MEMORY_LOCATION, "READ_MEMORY_LOCATION"},
{DALI_CMD_DT8_STORE_DTR_AS_COLORX, "STORE_DTR_AS_COLORX"},
{DALI_CMD_DT8_STORE_DTR_AS_COLORY, "STORE_DTR_AS_COLORY"},
{DALI_CMD_DT8_ACTIVATE, "ACTIVATE"},
{DALI_CMD_DT8_SET_COLOR_TEMPERATURE, "SET_COLOR_TEMPERATURE"},
{DALI_CMD_DT8_STEP_UP_COLOR_TEMPERATURE, "STEP_UP_COLOR_TEMPERATURE"},
{DALI_CMD_DT8_STEP_DOWN_COLOR_TEMPERATURE, "STEP_DOWN_COLOR_TEMPERATURE"},
{DALI_CMD_QUERY_COLOR_STATUS, "QUERY_COLOR_STATUS"},
{DALI_CMD_QUERY_COLOR_TYPE, "QUERY_COLOR_TYPE"},
{DALI_CMD_QUERY_COLOR_VALUE, "QUERY_COLOR_VALUE"},
};
sCMD_ = {
{DALI_CMD_SPECIAL_TERMINATE, "TERMINATE"},
{DALI_CMD_SPECIAL_SET_DTR0, "SET_DTR0"},
{DALI_CMD_SPECIAL_INITIALIZE, "INITIALIZE"},
{DALI_CMD_SPECIAL_RANDOMIZE, "RANDOMIZE"},
{DALI_CMD_SPECIAL_COMPARE, "COMPARE"},
{DALI_CMD_SPECIAL_WITHDRAW, "WITHDRAW"},
{DALI_CMD_SPECIAL_SEARCHADDRH, "SEARCHADDRH"},
{DALI_CMD_SPECIAL_SEARCHADDRM, "SEARCHADDRM"},
{DALI_CMD_SPECIAL_SEARCHADDRL, "SEARCHADDRL"},
{DALI_CMD_SPECIAL_PROGRAM_SHORT_ADDRESS, "PROGRAM_SHORT_ADDRESS"},
{DALI_CMD_SPECIAL_VERIFY_SHORT_ADDRESS, "VERIFY_SHORT_ADDRESS"},
{DALI_CMD_SPECIAL_QUERY_SHORT_ADDRESS, "QUERY_SHORT_ADDRESS"},
{DALI_CMD_SPECIAL_PHYSICAL_SELECTION, "PHYSICAL_SELECTION"},
{DALI_CMD_SPECIAL_DT_SELECT, "DT_SELECT"},
{DALI_CMD_SPECIAL_SET_DTR_1, "SET_DTR_1"},
{DALI_CMD_SPECIAL_SET_DTR_2, "SET_DTR_2"},
};
queryCmd_ = {DALI_CMD_QUERY_STATUS,
DALI_CMD_QUERY_BALLAST,
DALI_CMD_QUERY_LAMP_FAILURE,
DALI_CMD_QUERY_LAMP_POWER_ON,
DALI_CMD_QUERY_LIMIT_ERROR,
DALI_CMD_QUERY_RESET_STATE,
DALI_CMD_QUERY_MISSING_SHORT_ADDRESS,
DALI_CMD_QUERY_VERSION_NUMBER,
DALI_CMD_QUERY_CONTENT_DTR,
DALI_CMD_QUERY_DEVICE_TYPE,
DALI_CMD_QUERY_PHYSICAL_MINIMUM_LEVEL,
DALI_CMD_QUERY_POWER_FAILURE,
DALI_CMD_QUERY_CONTENT_DTR1,
DALI_CMD_QUERY_CONTENT_DTR2,
DALI_CMD_QUERY_OPERATING_MODE,
DALI_CMD_QUERY_LIGHT_SOURCE_TYPE,
DALI_CMD_QUERY_ACTUAL_LEVEL,
DALI_CMD_QUERY_MAX_LEVEL,
DALI_CMD_QUERY_MIN_LEVEL,
DALI_CMD_QUERY_POWER_ON_LEVEL,
DALI_CMD_QUERY_SYSTEM_FAILURE_LEVEL,
DALI_CMD_QUERY_FADE_TIME_FADE_RATE,
DALI_CMD_QUERY_MANUFACTURER_SPECIFIC_MODE,
DALI_CMD_QUERY_NEXT_DEVICE_TYPE,
DALI_CMD_QUERY_EXTENDED_FADE_TIME,
DALI_CMD_QUERY_CONTROL_GEAR_FAILURE,
DALI_CMD_QUERY_GROUPS_0_7,
DALI_CMD_QUERY_GROUP_8_15,
DALI_CMD_QUERY_RANDOM_ADDRESS_H,
DALI_CMD_QUERY_RANDOM_ADDRESS_M,
DALI_CMD_QUERY_RANDOM_ADDRESS_L,
DALI_CMD_READ_MEMORY_LOCATION,
DALI_CMD_DT8_ACTIVATE,
DALI_CMD_DT8_SET_COLOR_TEMPERATURE,
DALI_CMD_DT8_STEP_UP_COLOR_TEMPERATURE,
DALI_CMD_DT8_STEP_DOWN_COLOR_TEMPERATURE,
DALI_CMD_QUERY_COLOR_VALUE};
for (int c = DALI_CMD_QUERY_SCENE_LEVEL_MIN; c <= DALI_CMD_QUERY_SCENE_LEVEL_MAX; c++) {
queryCmd_.push_back(c);
}
}
int DaliDecode::isQueryCmd(int cmd) const {
return std::find(queryCmd_.begin(), queryCmd_.end(), cmd) != queryCmd_.end() ? 1 : 0;
}
DecodedRecord DaliDecode::decodeBright(int addr, int level) {
DecodedRecord r;
r.text = whoLabel(addr, true) + " DAPC level=" + std::to_string(level);
r.type = "brightness";
r.addr = addr;
r.cmd = level;
r.proto = 0x10;
return withRaw(r);
}
DecodedRecord DaliDecode::decodeScene(int addr, int sceneCmd) {
const int sc = sceneCmd - DALI_CMD_GO_TO_SCENE_MIN;
DecodedRecord r;
r.text = whoLabel(addr, false) + " GO_TO_SCENE " + std::to_string(sc);
r.type = "cmd";
r.addr = addr;
r.cmd = sceneCmd;
r.proto = 0x10;
return withRaw(r);
}
DecodedRecord DaliDecode::decodeCmd(int addr, int c) {
std::string name = "CMD 0x" + hex(c);
const auto it = cmd_.find(c);
if (it != cmd_.end()) name = it->second;
if (c >= DALI_CMD_SET_SCENE_MIN && c <= DALI_CMD_SET_SCENE_MAX) {
name = "SET_SCENE " + std::to_string(c - DALI_CMD_SET_SCENE_MIN);
} else if (c >= DALI_CMD_ADD_TO_GROUP_MIN && c <= DALI_CMD_ADD_TO_GROUP_MAX) {
name = "ADD_TO_GROUP " + std::to_string(c - DALI_CMD_ADD_TO_GROUP_MIN);
} else if (c >= DALI_CMD_REMOVE_FROM_GROUP_MIN && c <= DALI_CMD_REMOVE_FROM_GROUP_MAX) {
name = "REMOVE_FROM_GROUP " + std::to_string(c - DALI_CMD_REMOVE_FROM_GROUP_MIN);
}
DecodedRecord r;
r.text = whoLabel(addr, false) + " " + name;
r.type = "cmd";
r.addr = addr;
r.cmd = c;
r.proto = 0x10;
return withRaw(r);
}
DecodedRecord DaliDecode::decodeSpCmd(int addr, int c) {
const int cmdByte = addr & 0xFF;
const int dataByte = c & 0xFF;
if (cmdByte == DALI_CMD_SPECIAL_COMPARE || cmdByte == DALI_CMD_SPECIAL_VERIFY_SHORT_ADDRESS ||
cmdByte == DALI_CMD_SPECIAL_QUERY_SHORT_ADDRESS) {
return querySpCMD(cmdByte, dataByte);
}
std::string name = "SPECIAL_CMD 0x" + hex(cmdByte);
const auto it = sCMD_.find(cmdByte);
if (it != sCMD_.end()) name = it->second;
if (cmdByte == DALI_CMD_SPECIAL_SET_DTR0) {
dtr0 = dataByte;
} else if (cmdByte == DALI_CMD_SPECIAL_SET_DTR_1) {
dtr1 = dataByte;
} else if (cmdByte == DALI_CMD_SPECIAL_SET_DTR_2) {
dtr2 = dataByte;
}
DecodedRecord r;
r.text = name + " data=0x" + hex(dataByte) + " (" + std::to_string(dataByte) + ")";
r.type = "special";
r.addr = addr;
r.cmd = c;
r.proto = 0x10;
return withRaw(r);
}
DecodedRecord DaliDecode::querySpCMD(int cmdByte, int dataByte) {
lastQueryCmd_ = cmdByte;
lastQueryAtMs_ = nowMs();
std::string name = "SPECIAL_CMD 0x" + hex(cmdByte);
const auto it = sCMD_.find(cmdByte);
if (it != sCMD_.end()) name = it->second;
DecodedRecord r;
r.text = name + " query data=0x" + hex(dataByte) + " (" + std::to_string(dataByte) + ")";
r.type = "query";
r.addr = cmdByte;
r.cmd = dataByte;
r.proto = 0x10;
return withRaw(r);
}
DecodedRecord DaliDecode::decodeQuery(int addr, int c) {
lastQueryCmd_ = c;
lastQueryAtMs_ = nowMs();
std::string name = "QUERY 0x" + hex(c);
const auto it = cmd_.find(c);
if (it != cmd_.end()) name = it->second;
if (c >= DALI_CMD_QUERY_SCENE_LEVEL_MIN && c <= DALI_CMD_QUERY_SCENE_LEVEL_MAX) {
name = "QUERY_SCENE_LEVEL " + std::to_string(c - DALI_CMD_QUERY_SCENE_LEVEL_MIN);
}
if (c == DALI_CMD_QUERY_COLOR_VALUE) {
lastColourQueryAtMs_ = nowMs();
int t = dtr0;
if (t == 128) t = 0;
if (t == 130) t = 1;
pendingColourType_ = (t == 0 || t == 1 || t == 2) ? t : -1;
if (pendingColourType_ == 0) name = "QUERY_COLOUR_VALUE(x)";
if (pendingColourType_ == 1) name = "QUERY_COLOUR_VALUE(y)";
if (pendingColourType_ == 2) name = "QUERY_COLOUR_VALUE(ct)";
}
DecodedRecord r;
r.text = whoLabel(addr, false) + " " + name;
r.type = "query";
r.addr = addr;
r.cmd = c;
r.proto = 0x12;
return withRaw(r);
}
DecodedRecord DaliDecode::decodeCmdResponse(int value, int /*gwPrefix*/) {
const int64_t now = nowMs();
if ((now - lastQueryAtMs_) > responseWindowMs || lastQueryCmd_ == 0) {
DecodedRecord r{"unknown back 0x" + hex(value) + " / " + std::to_string(value), "unknown", -1,
-1, 0xFF};
return withRaw(r);
}
const int c = lastQueryCmd_;
if (c == DALI_CMD_QUERY_CONTENT_DTR) dtr0 = value & 0xFF;
if (c == DALI_CMD_QUERY_CONTENT_DTR1) dtr1 = value & 0xFF;
if (c == DALI_CMD_QUERY_CONTENT_DTR2) dtr2 = value & 0xFF;
if (c == DALI_CMD_QUERY_STATUS) {
const auto ds = DaliStatus::fromByte(static_cast<uint8_t>(value));
std::ostringstream oss;
oss << "status gearPresent=" << yn(ds.controlGearPresent) << " lampFailure=" << yn(ds.lampFailure)
<< " lampOn=" << yn(ds.lampPowerOn) << " limitError=" << yn(ds.limitError)
<< " fadingDone=" << yn(ds.fadingCompleted) << " reset=" << yn(ds.resetState)
<< " missingAddr=" << yn(ds.missingShortAddress) << " psFault=" << yn(ds.psFault);
return withRaw({oss.str(), "response", -1, c, 0xFF});
}
if (c == DALI_CMD_QUERY_DEVICE_TYPE) {
const auto name = deviceTypeName(value);
std::string text = "QUERY_DEVICE_TYPE => ";
text += name.empty() ? ("0x" + hex(value) + " / " + std::to_string(value))
: (name + " (0x" + hex(value) + " / " + std::to_string(value) + ")");
return withRaw({text, "response", -1, c, 0xFF});
}
if (c == DALI_CMD_QUERY_LIGHT_SOURCE_TYPE) {
const auto name = lightSourceName(value);
std::string text = "QUERY_LIGHT_SOURCE_TYPE => ";
text += name.empty() ? ("0x" + hex(value) + " / " + std::to_string(value))
: (name + " (0x" + hex(value) + " / " + std::to_string(value) + ")");
return withRaw({text, "response", -1, c, 0xFF});
}
if (c == DALI_CMD_QUERY_FADE_TIME_FADE_RATE) {
const int ft = (value >> 4) & 0xF;
const int fr = value & 0xF;
return withRaw({"fade time index=" + std::to_string(ft) + ", fade rate code=" + std::to_string(fr),
"response", -1, c, 0xFF});
}
if (c >= DALI_CMD_QUERY_SCENE_LEVEL_MIN && c <= DALI_CMD_QUERY_SCENE_LEVEL_MAX) {
const int idx = c - DALI_CMD_QUERY_SCENE_LEVEL_MIN;
const std::string text = (value == 0xFF)
? ("scene level " + std::to_string(idx) + " = MASK")
: ("scene level " + std::to_string(idx) + " = " + std::to_string(value));
return withRaw({text, "response", -1, c, 0xFF});
}
if (pendingColourType_ >= 0 && (now - lastColourQueryAtMs_) <= 1000) {
const int combined = dtr1 * 256 + dtr0;
std::string pretty;
if (pendingColourType_ == 2) {
const int kelvin = combined == 0 ? 0 : (1000000 / combined);
pretty = "ct mirek=" + std::to_string(combined) + " kelvin=" + std::to_string(kelvin);
} else {
const char key = pendingColourType_ == 0 ? 'x' : 'y';
std::ostringstream oss;
oss << key << "=" << std::fixed << std::setprecision(4)
<< (static_cast<double>(combined) / 65535.0) << " (" << combined << ")";
pretty = oss.str();
}
return withRaw({"colour value " + pretty, "response", -1, c, 0xFF});
}
return withRaw({"0x" + hex(value) + " / " + std::to_string(value) + " / " + bin(value), "response",
-1, c, 0xFF});
}
DecodedRecord DaliDecode::decode(int addr, int c, int proto) {
if (addr >= DALI_CMD_SPECIAL_RANGE_MIN && addr <= DALI_CMD_SPECIAL_RANGE_MAX) {
return decodeSpCmd(addr, c);
}
if (proto == 0x12 || isQueryCmd(c) == 1) {
return decodeQuery(addr, c);
}
if ((addr & 1) == 0) {
return decodeBright(addr, c);
}
if (c >= DALI_CMD_GO_TO_SCENE_MIN && c <= DALI_CMD_GO_TO_SCENE_MAX) {
return decodeScene(addr, c);
}
if (proto == 0x11) {
if (c == DALI_CMD_QUERY_COLOR_VALUE) return decodeQuery(addr, c);
std::string name = "EXT 0x" + hex(c);
const auto it = cmd_.find(c);
if (it != cmd_.end()) name = it->second;
return withRaw({whoLabel(addr, false) + " " + name, "ext", addr, c, proto});
}
return decodeCmd(addr, c);
}
int64_t DaliDecode::nowMs() {
const auto now = std::chrono::time_point_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now());
return now.time_since_epoch().count();
}
std::string DaliDecode::hex(int v) {
std::ostringstream oss;
oss << std::hex << std::uppercase << std::setfill('0') << std::setw(2) << (v & 0xFF);
return oss.str();
}
std::string DaliDecode::bin(int v) {
std::string out;
out.reserve(8);
for (int i = 7; i >= 0; i--) {
out.push_back(((v >> i) & 0x1) ? '1' : '0');
}
return out;
}
std::string DaliDecode::yn(bool b) { return b ? "yes" : "no"; }
std::string DaliDecode::whoLabel(int addr, bool even) {
if (even) {
if (addr == 0xFE) return "broadcast";
if (addr >= 0x80 && addr <= 0x8F) return "group " + std::to_string(addr - 0x80);
return "short " + std::to_string(addr / 2);
}
if (addr == 0xFF) return "broadcast";
if (addr >= 0x80 && addr <= 0x8F) return "group " + std::to_string(addr - 0x80);
return "short " + std::to_string((addr - 1) / 2);
}
std::string DaliDecode::deviceTypeName(int v) {
static const std::map<int, std::string> map = {{0x00, "general control gear"},
{0x01, "self-contained emergency"},
{0x06, "LED control gear"},
{0x08, "colour control (DT8)"}};
const auto it = map.find(v);
return it == map.end() ? "" : it->second;
}
std::string DaliDecode::lightSourceName(int v) {
static const std::map<int, std::string> map = {
{0x00, "fluorescent"}, {0x01, "compact fluorescent"}, {0x02, "high intensity discharge"},
{0x04, "incandescent/halogen"}, {0x06, "LED"}, {0x07, "other"}};
const auto it = map.find(v);
return it == map.end() ? "" : it->second;
}
DecodedRecord DaliDecode::withRaw(const DecodedRecord& r) const {
if (!displayRaw) return r;
std::vector<std::string> parts;
if (r.addr >= 0) parts.push_back("0x" + hex(r.addr));
if (r.cmd >= 0) parts.push_back("0x" + hex(r.cmd));
if (parts.empty()) return r;
DecodedRecord out = r;
std::ostringstream oss;
oss << r.text << " [";
for (size_t i = 0; i < parts.size(); i++) {
if (i > 0) oss << ", ";
oss << parts[i];
}
oss << "]";
out.text = oss.str();
return out;
}

352
src/device.cpp Normal file
View File

@@ -0,0 +1,352 @@
#include "device.hpp"
#include <utility>
namespace {
std::optional<std::vector<int>> asIntList(const DaliValue* value) {
if (!value) return std::nullopt;
const auto* arr = value->asArray();
if (!arr) return std::nullopt;
std::vector<int> out;
out.reserve(arr->size());
for (const auto& v : *arr) {
out.push_back(v.asInt().value_or(0));
}
return out;
}
DaliValue toIntArray(const std::optional<std::vector<int>>& values) {
if (!values.has_value()) return DaliValue();
DaliValue::Array out;
out.reserve(values->size());
for (const int v : *values) {
out.emplace_back(v);
}
return DaliValue(std::move(out));
}
} // namespace
DaliLongAddress DaliLongAddress::fromJson(const DaliValue::Object* json) {
if (!json) return DaliLongAddress{};
DaliLongAddress addr;
addr.h = getObjectInt(*json, "H").value_or(0);
addr.m = getObjectInt(*json, "M").value_or(0);
addr.l = getObjectInt(*json, "L").value_or(0);
return addr;
}
DaliValue::Object DaliLongAddress::toJson() const {
DaliValue::Object out;
out["H"] = h;
out["M"] = m;
out["L"] = l;
return out;
}
DaliDeviceCapabilities DaliDeviceCapabilities::fromJson(const DaliValue::Object* json) {
DaliDeviceCapabilities c;
if (!json) return c;
c.supportsDt1 = getObjectBool(*json, "dt1");
c.supportsDt8 = getObjectBool(*json, "dt8");
return c;
}
DaliValue::Object DaliDeviceCapabilities::toJson() const {
DaliValue::Object out;
if (supportsDt1.has_value()) out["dt1"] = supportsDt1.value();
if (supportsDt8.has_value()) out["dt8"] = supportsDt8.value();
return out;
}
void DaliDeviceCapabilities::merge(const DaliDeviceCapabilities& other) {
if (!supportsDt1.has_value()) supportsDt1 = other.supportsDt1;
if (!supportsDt8.has_value()) supportsDt8 = other.supportsDt8;
}
DaliStatusFlags DaliStatusFlags::fromJson(const DaliValue::Object* json) {
DaliStatusFlags s;
if (!json) return s;
s.controlGearPresent = getObjectBool(*json, "controlGearPresent");
s.lampFailure = getObjectBool(*json, "lampFailure");
s.lampPowerOn = getObjectBool(*json, "lampPowerOn");
s.limitError = getObjectBool(*json, "limitError");
s.fadingCompleted = getObjectBool(*json, "fadingCompleted");
s.resetState = getObjectBool(*json, "resetState");
s.missingShortAddress = getObjectBool(*json, "missingShortAddress");
s.psFault = getObjectBool(*json, "psFault");
return s;
}
DaliValue::Object DaliStatusFlags::toJson() const {
DaliValue::Object out;
if (controlGearPresent.has_value()) out["controlGearPresent"] = controlGearPresent.value();
if (lampFailure.has_value()) out["lampFailure"] = lampFailure.value();
if (lampPowerOn.has_value()) out["lampPowerOn"] = lampPowerOn.value();
if (limitError.has_value()) out["limitError"] = limitError.value();
if (fadingCompleted.has_value()) out["fadingCompleted"] = fadingCompleted.value();
if (resetState.has_value()) out["resetState"] = resetState.value();
if (missingShortAddress.has_value()) out["missingShortAddress"] = missingShortAddress.value();
if (psFault.has_value()) out["psFault"] = psFault.value();
return out;
}
bool DaliStatusFlags::hasData() const {
return controlGearPresent.has_value() || lampFailure.has_value() || lampPowerOn.has_value() ||
limitError.has_value() || fadingCompleted.has_value() || resetState.has_value() ||
missingShortAddress.has_value() || psFault.has_value();
}
void DaliStatusFlags::merge(const DaliStatusFlags& other) {
if (!controlGearPresent.has_value()) controlGearPresent = other.controlGearPresent;
if (!lampFailure.has_value()) lampFailure = other.lampFailure;
if (!lampPowerOn.has_value()) lampPowerOn = other.lampPowerOn;
if (!limitError.has_value()) limitError = other.limitError;
if (!fadingCompleted.has_value()) fadingCompleted = other.fadingCompleted;
if (!resetState.has_value()) resetState = other.resetState;
if (!missingShortAddress.has_value()) missingShortAddress = other.missingShortAddress;
if (!psFault.has_value()) psFault = other.psFault;
}
DaliDt8State DaliDt8State::fromJson(const DaliValue::Object* json) {
DaliDt8State s;
if (!json) return s;
s.colorType = getObjectInt(*json, "colorType");
s.activeMode = getObjectString(*json, "activeMode");
if (const auto* xyVal = getObjectValue(*json, "xy")) {
if (const auto* xy = xyVal->asObject()) {
s.xyX = getObjectInt(*xy, "x");
s.xyY = getObjectInt(*xy, "y");
}
}
if (const auto* gamutVal = getObjectValue(*json, "gamut")) {
if (const auto* gamut = gamutVal->asObject()) {
s.xyMinX = getObjectInt(*gamut, "xMin");
s.xyMaxX = getObjectInt(*gamut, "xMax");
s.xyMinY = getObjectInt(*gamut, "yMin");
s.xyMaxY = getObjectInt(*gamut, "yMax");
}
}
s.mirek = getObjectInt(*json, "mirek");
s.mirekMin = getObjectInt(*json, "mirekMin");
s.mirekMax = getObjectInt(*json, "mirekMax");
s.rgbwaf = asIntList(getObjectValue(*json, "rgbwaf"));
s.primaryN = asIntList(getObjectValue(*json, "primaryN"));
return s;
}
DaliValue::Object DaliDt8State::toJson() const {
DaliValue::Object out;
if (colorType.has_value()) out["colorType"] = colorType.value();
if (activeMode.has_value()) out["activeMode"] = activeMode.value();
if (xyX.has_value() || xyY.has_value()) {
DaliValue::Object xy;
if (xyX.has_value()) xy["x"] = xyX.value();
if (xyY.has_value()) xy["y"] = xyY.value();
out["xy"] = std::move(xy);
}
if (xyMinX.has_value() || xyMaxX.has_value() || xyMinY.has_value() || xyMaxY.has_value()) {
DaliValue::Object gamut;
if (xyMinX.has_value()) gamut["xMin"] = xyMinX.value();
if (xyMaxX.has_value()) gamut["xMax"] = xyMaxX.value();
if (xyMinY.has_value()) gamut["yMin"] = xyMinY.value();
if (xyMaxY.has_value()) gamut["yMax"] = xyMaxY.value();
out["gamut"] = std::move(gamut);
}
if (mirek.has_value()) out["mirek"] = mirek.value();
if (mirekMin.has_value()) out["mirekMin"] = mirekMin.value();
if (mirekMax.has_value()) out["mirekMax"] = mirekMax.value();
if (rgbwaf.has_value()) out["rgbwaf"] = toIntArray(rgbwaf);
if (primaryN.has_value()) out["primaryN"] = toIntArray(primaryN);
return out;
}
DaliDt1State DaliDt1State::fromJson(const DaliValue::Object* json) {
DaliDt1State s;
if (!json) return s;
s.emergencyLevel = getObjectInt(*json, "emergencyLevel");
s.emergencyMinLevel = getObjectInt(*json, "emergencyMinLevel");
s.emergencyMaxLevel = getObjectInt(*json, "emergencyMaxLevel");
s.prolongTimeMinutes = getObjectInt(*json, "prolongTimeMinutes");
s.ratedDurationMinutes = getObjectInt(*json, "ratedDurationMinutes");
s.testDelayTime = getObjectInt(*json, "testDelayTime");
s.failureStatus = getObjectInt(*json, "failureStatus");
s.emergencyStatus = getObjectInt(*json, "emergencyStatus");
s.emergencyMode = getObjectInt(*json, "emergencyMode");
s.feature = getObjectInt(*json, "feature");
s.version = getObjectInt(*json, "version");
return s;
}
DaliValue::Object DaliDt1State::toJson() const {
DaliValue::Object out;
if (emergencyLevel.has_value()) out["emergencyLevel"] = emergencyLevel.value();
if (emergencyMinLevel.has_value()) out["emergencyMinLevel"] = emergencyMinLevel.value();
if (emergencyMaxLevel.has_value()) out["emergencyMaxLevel"] = emergencyMaxLevel.value();
if (prolongTimeMinutes.has_value()) out["prolongTimeMinutes"] = prolongTimeMinutes.value();
if (ratedDurationMinutes.has_value()) out["ratedDurationMinutes"] = ratedDurationMinutes.value();
if (testDelayTime.has_value()) out["testDelayTime"] = testDelayTime.value();
if (failureStatus.has_value()) out["failureStatus"] = failureStatus.value();
if (emergencyStatus.has_value()) out["emergencyStatus"] = emergencyStatus.value();
if (emergencyMode.has_value()) out["emergencyMode"] = emergencyMode.value();
if (feature.has_value()) out["feature"] = feature.value();
if (version.has_value()) out["version"] = version.value();
return out;
}
DaliDevice DaliDevice::fromJson(const DaliValue::Object& json) {
DaliDevice d;
d.name = getObjectString(json, "name").value_or("");
d.id = getObjectString(json, "id").value_or("");
if (d.id.empty()) {
d.id = d.name.empty() ? "device-unknown" : d.name;
}
if (d.name.empty()) {
d.name = d.id;
}
d.shortAddress = getObjectInt(json, "shortAddress");
if (const auto* lv = getObjectValue(json, "longAddress")) {
if (const auto* lo = lv->asObject()) {
d.longAddress = DaliLongAddress::fromJson(lo);
}
}
d.isolated = getObjectBool(json, "isolated").value_or(false);
d.brightness = getObjectInt(json, "brightness");
d.groupBits = getObjectInt(json, "groupBits");
d.scenes = asIntList(getObjectValue(json, "scenes"));
d.fadeTime = getObjectInt(json, "fadeTime");
d.fadeRate = getObjectInt(json, "fadeRate");
d.powerOnLevel = getObjectInt(json, "powerOnLevel");
d.systemFailureLevel = getObjectInt(json, "systemFailureLevel");
d.minLevel = getObjectInt(json, "minLevel");
d.maxLevel = getObjectInt(json, "maxLevel");
d.operatingMode = getObjectInt(json, "operatingMode");
d.physicalMinLevel = getObjectInt(json, "physicalMinLevel");
d.deviceType = getObjectInt(json, "deviceType");
d.extType = asIntList(getObjectValue(json, "extType")).value_or(std::vector<int>{});
d.version = getObjectInt(json, "version");
if (const auto* cv = getObjectValue(json, "capabilities")) {
d.capabilities = DaliDeviceCapabilities::fromJson(cv->asObject());
}
if (const auto* dt8v = getObjectValue(json, "dt8")) {
if (const auto* obj = dt8v->asObject()) {
d.dt8 = DaliDt8State::fromJson(obj);
}
}
if (const auto* dt1v = getObjectValue(json, "dt1")) {
if (const auto* obj = dt1v->asObject()) {
d.dt1 = DaliDt1State::fromJson(obj);
}
}
if (const auto* sfv = getObjectValue(json, "statusFlags")) {
d.statusFlags = DaliStatusFlags::fromJson(sfv->asObject());
}
d.lastSyncedUtc = getObjectString(json, "lastSyncedUtc");
if (const auto* mv = getObjectValue(json, "meta")) {
if (const auto* obj = mv->asObject()) {
d.metadata = *obj;
}
}
return d;
}
DaliValue::Object DaliDevice::toJson() const {
DaliValue::Object out;
out["id"] = id;
out["name"] = name;
if (shortAddress.has_value()) out["shortAddress"] = shortAddress.value();
if (longAddress.has_value()) out["longAddress"] = longAddress->toJson();
out["isolated"] = isolated;
if (brightness.has_value()) out["brightness"] = brightness.value();
if (groupBits.has_value()) out["groupBits"] = groupBits.value();
if (scenes.has_value()) out["scenes"] = toIntArray(scenes);
if (fadeTime.has_value()) out["fadeTime"] = fadeTime.value();
if (fadeRate.has_value()) out["fadeRate"] = fadeRate.value();
if (powerOnLevel.has_value()) out["powerOnLevel"] = powerOnLevel.value();
if (systemFailureLevel.has_value()) out["systemFailureLevel"] = systemFailureLevel.value();
if (minLevel.has_value()) out["minLevel"] = minLevel.value();
if (maxLevel.has_value()) out["maxLevel"] = maxLevel.value();
if (operatingMode.has_value()) out["operatingMode"] = operatingMode.value();
if (physicalMinLevel.has_value()) out["physicalMinLevel"] = physicalMinLevel.value();
if (deviceType.has_value()) out["deviceType"] = deviceType.value();
if (!extType.empty()) {
DaliValue::Array arr;
arr.reserve(extType.size());
for (const int t : extType) arr.emplace_back(t);
out["extType"] = std::move(arr);
}
if (version.has_value()) out["version"] = version.value();
const auto caps = capabilities.toJson();
if (!caps.empty()) out["capabilities"] = caps;
if (dt8.has_value()) out["dt8"] = dt8->toJson();
if (dt1.has_value()) out["dt1"] = dt1->toJson();
if (statusFlags.hasData()) out["statusFlags"] = statusFlags.toJson();
if (lastSyncedUtc.has_value()) out["lastSyncedUtc"] = lastSyncedUtc.value();
if (!metadata.empty()) out["meta"] = metadata;
return out;
}
std::string DaliDevice::displayName() const { return name.empty() ? id : name; }
void DaliDevice::merge(const DaliDevice& other) {
if (other.shortAddress.has_value()) shortAddress = other.shortAddress;
if (!longAddress.has_value()) longAddress = other.longAddress;
isolated = other.isolated;
if (!brightness.has_value()) brightness = other.brightness;
if (!groupBits.has_value()) groupBits = other.groupBits;
if (!scenes.has_value() && other.scenes.has_value()) scenes = other.scenes;
if (!fadeTime.has_value()) fadeTime = other.fadeTime;
if (!fadeRate.has_value()) fadeRate = other.fadeRate;
if (!powerOnLevel.has_value()) powerOnLevel = other.powerOnLevel;
if (!systemFailureLevel.has_value()) systemFailureLevel = other.systemFailureLevel;
if (!minLevel.has_value()) minLevel = other.minLevel;
if (!maxLevel.has_value()) maxLevel = other.maxLevel;
if (!operatingMode.has_value()) operatingMode = other.operatingMode;
if (!physicalMinLevel.has_value()) physicalMinLevel = other.physicalMinLevel;
if (!deviceType.has_value()) deviceType = other.deviceType;
if (extType.empty() && !other.extType.empty()) extType = other.extType;
if (!version.has_value()) version = other.version;
capabilities.merge(other.capabilities);
if (!dt8.has_value() && other.dt8.has_value()) dt8 = other.dt8;
if (!dt1.has_value() && other.dt1.has_value()) dt1 = other.dt1;
statusFlags.merge(other.statusFlags);
if (other.lastSyncedUtc.has_value()) lastSyncedUtc = other.lastSyncedUtc;
for (const auto& kv : other.metadata) {
metadata[kv.first] = kv.second;
}
}

226
src/dt1.cpp Normal file
View File

@@ -0,0 +1,226 @@
#include "dt1.hpp"
#include "dali_define.hpp"
#include <algorithm>
#include <chrono>
#include <thread>
DaliDT1::DaliDT1(DaliBase& base) : base_(base) {}
bool DaliDT1::enable() { return base_.dtSelect(1); }
int DaliDT1::addrOf(int a) { return a * 2 + 1; }
bool DaliDT1::send(int a, int code) { return enable() && base_.sendExtCmd(addrOf(a), code); }
std::optional<int> DaliDT1::query(int a, int code) {
if (!enable()) return std::nullopt;
const auto v = base_.queryCmd(static_cast<uint8_t>(addrOf(a)), static_cast<uint8_t>(code));
if (!v.has_value() || v.value() == 0xFF) return std::nullopt;
return v;
}
bool DaliDT1::enableDT1() { return enable(); }
bool DaliDT1::startDT1Test(int a, int t) {
if (t != 1) return false;
return send(a, DALI_CMD_DT1_START_FUNCTION_TEST);
}
std::optional<int> DaliDT1::getDT1EmergencyMode(int a) { return query(a, DALI_CMD_DT1_QUERY_EMERGENCY_MODE); }
std::optional<int> DaliDT1::getDT1Feature(int a) { return query(a, DALI_CMD_DT1_QUERY_FEATURE); }
std::optional<int> DaliDT1::getDT1FailureStatus(int a) {
return query(a, DALI_CMD_DT1_QUERY_FAILURE_STATUS);
}
std::optional<int> DaliDT1::getDT1Status(int a) { return query(a, DALI_CMD_DT1_QUERY_STATUS); }
std::optional<int> DaliDT1::getDT1SelfTestStatus(int a) {
const auto ret = getDT1FailureStatus(a);
if (!ret.has_value()) return std::nullopt;
const bool inProgress = (ret.value() & 0x01) != 0;
return inProgress ? 1 : 0;
}
std::optional<DT1TestStatusDetailed> DaliDT1::getDT1TestStatusDetailed(int a) {
DT1TestStatusDetailed result;
result.failureStatus = getDT1FailureStatus(a);
result.emergencyStatus = getDT1Status(a);
result.emergencyMode = getDT1EmergencyMode(a);
result.feature = getDT1Feature(a);
if (!result.failureStatus.has_value()) return std::nullopt;
const int failure = result.failureStatus.value();
result.testInProgress = (failure & 0x01) != 0;
result.lampFailure = (failure & 0x02) != 0;
result.batteryFailure = (failure & 0x04) != 0;
result.functionTestActive = (failure & 0x08) != 0;
result.durationTestActive = (failure & 0x10) != 0;
result.testDone = (failure & 0x20) != 0;
result.identifyActive = (failure & 0x40) != 0;
result.physicalSelectionActive = (failure & 0x80) != 0;
return result;
}
bool DaliDT1::performDT1Test(int a, int timeout) {
if (!startDT1Test(a)) return false;
for (int i = 0; i < timeout; i++) {
const auto ret = getDT1SelfTestStatus(a);
if (!ret.has_value()) return false;
if (ret.value() == 0) return true;
if (ret.value() != 1) return false;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
return false;
}
bool DaliDT1::rest(int a) { return send(a, DALI_CMD_DT1_REST); }
bool DaliDT1::inhibit(int a) { return send(a, DALI_CMD_DT1_INHIBIT); }
bool DaliDT1::reLightOrResetInhibit(int a) { return send(a, DALI_CMD_DT1_RE_LIGHT_RESET_INHIBIT); }
bool DaliDT1::startFunctionTestCmd(int a) { return send(a, DALI_CMD_DT1_START_FUNCTION_TEST); }
bool DaliDT1::startDurationTestCmd(int a) { return send(a, DALI_CMD_DT1_START_DURATION_TEST); }
bool DaliDT1::stopTest(int a) { return send(a, DALI_CMD_DT1_STOP_TEST); }
bool DaliDT1::resetFunctionTestDoneFlag(int a) {
return send(a, DALI_CMD_DT1_RESET_FUNCTION_TEST_DONE_FLAG);
}
bool DaliDT1::resetDurationTestDoneFlag(int a) {
return send(a, DALI_CMD_DT1_RESET_DURATION_TEST_DONE_FLAG);
}
bool DaliDT1::resetLampTime(int a) { return send(a, DALI_CMD_DT1_RESET_LAMP_TIME); }
bool DaliDT1::resetStatusFlags(int a) { return resetFunctionTestDoneFlag(a); }
bool DaliDT1::resetLampOperationTime(int a) { return resetDurationTestDoneFlag(a); }
bool DaliDT1::resetTestResults(int a) { return resetLampTime(a); }
bool DaliDT1::storeEmergencyLevel(int a, int level) {
const int v = std::clamp(level, 0, 254);
return enable() && base_.setDTR(v) &&
base_.sendExtCmd(addrOf(a), DALI_CMD_DT1_STORE_DTR_AS_EMERGENCY_LEVEL);
}
bool DaliDT1::storeTestDelayTimeHighByte(int a, int highByte) {
const int v = std::clamp(highByte, 0, 255);
return enable() && base_.setDTR(v) &&
base_.sendExtCmd(addrOf(a), DALI_CMD_DT1_STORE_DTR_AS_DELAY_TIME_HIGH);
}
bool DaliDT1::storeTestDelayTimeLowByte(int a, int lowByte) {
const int v = std::clamp(lowByte, 0, 255);
return enable() && base_.setDTR(v) &&
base_.sendExtCmd(addrOf(a), DALI_CMD_DT1_STORE_DTR_AS_DELAY_TIME_LOW);
}
bool DaliDT1::storeFunctionTestIntervalDays(int a, int days) {
return storeTestDelayTimeHighByte(a, days);
}
bool DaliDT1::storeDurationTestIntervalWeeks(int a, int weeks) {
return storeTestDelayTimeLowByte(a, weeks);
}
bool DaliDT1::storeTestDelayTime16(int a, int quartersOfHour) {
const int v = std::clamp(quartersOfHour, 0, 0xFFFF);
const int hi = (v >> 8) & 0xFF;
const int lo = v & 0xFF;
return storeTestDelayTimeHighByte(a, hi) && storeTestDelayTimeLowByte(a, lo);
}
bool DaliDT1::storeProlongTimeMinutes(int a, int minutes) {
const int v = std::clamp(minutes, 0, 255);
return enable() && base_.setDTR(v) &&
base_.sendExtCmd(addrOf(a), DALI_CMD_DT1_STORE_DTR_AS_PROLONG_TIME);
}
bool DaliDT1::storeRatedDurationMinutes(int a, int minutes) {
const int v = std::clamp(minutes, 0, 255);
return enable() && base_.setDTR(v) &&
base_.sendExtCmd(addrOf(a), DALI_CMD_DT1_STORE_DTR_AS_RATED_DURATION);
}
bool DaliDT1::storeEmergencyMinLevel(int a, int level) {
const int v = std::clamp(level, 0, 254);
return enable() && base_.setDTR(v) &&
base_.sendExtCmd(addrOf(a), DALI_CMD_DT1_STORE_DTR_AS_EMERGENCY_MIN_LEVEL);
}
bool DaliDT1::storeEmergencyMaxLevel(int a, int level) {
const int v = std::clamp(level, 0, 254);
return enable() && base_.setDTR(v) &&
base_.sendExtCmd(addrOf(a), DALI_CMD_DT1_STORE_DTR_AS_EMERGENCY_MAX_LEVEL);
}
bool DaliDT1::startIdentification(int a) { return send(a, DALI_CMD_DT1_START_IDENTIFICATION); }
bool DaliDT1::performDTRSelectedFunction(int a,
const std::optional<int>& dtr0,
const std::optional<int>& dtr1) {
if (!enable()) return false;
if (dtr0.has_value() && !base_.setDTR(dtr0.value() & 0xFF)) return false;
if (dtr1.has_value() && !base_.setDTR1(dtr1.value() & 0xFF)) return false;
return base_.sendExtCmd(addrOf(a), DALI_CMD_DT1_PERFORM_DTR_SELECTED_FUNCTION);
}
std::optional<int> DaliDT1::getExtendedVersionDT1(int a) {
return query(a, DALI_CMD_DT1_QUERY_EXTENDED_VERSION);
}
std::optional<int> DaliDT1::getEmergencyLevel(int a) { return query(a, DALI_CMD_DT1_QUERY_EMERGENCY_LEVEL); }
std::optional<int> DaliDT1::getEmergencyMinLevel(int a) {
return query(a, DALI_CMD_DT1_QUERY_EMERGENCY_MIN_LEVEL);
}
std::optional<int> DaliDT1::getEmergencyMaxLevel(int a) {
return query(a, DALI_CMD_DT1_QUERY_EMERGENCY_MAX_LEVEL);
}
std::optional<int> DaliDT1::getProlongTimeMinutes(int a) {
return query(a, DALI_CMD_DT1_QUERY_PROLONG_TIME);
}
std::optional<int> DaliDT1::getFunctionTestIntervalDays(int a) {
return query(a, DALI_CMD_DT1_QUERY_FUNCTION_TEST_INTERVAL);
}
std::optional<int> DaliDT1::getDurationTestIntervalWeeks(int a) {
return query(a, DALI_CMD_DT1_QUERY_DURATION_TEST_INTERVAL);
}
std::optional<int> DaliDT1::getDurationTestResult(int a) {
return query(a, DALI_CMD_DT1_QUERY_DURATION_TEST_RESULT);
}
std::optional<int> DaliDT1::getLampEmergencyTimeMinutes(int a) {
return query(a, DALI_CMD_DT1_QUERY_LAMP_EMERGENCY_TIME);
}
std::optional<int> DaliDT1::getRatedDurationMinutes(int a) {
return query(a, DALI_CMD_DT1_QUERY_RATED_DURATION);
}
std::optional<DaliDT1DeviceStatus> DaliDT1::getDeviceStatus(int a) {
if (!enable()) return std::nullopt;
const auto raw = base_.queryCmd(static_cast<uint8_t>(addrOf(a)), DALI_CMD_QUERY_STATUS);
if (!raw.has_value()) return std::nullopt;
return DaliDT1DeviceStatus(raw.value());
}
std::optional<DaliDT1EmergencyStatus> DaliDT1::getEmergencyStatusDecoded(int a) {
const auto v = getDT1Status(a);
if (!v.has_value()) return std::nullopt;
return DaliDT1EmergencyStatus(v.value());
}

468
src/dt8.cpp Normal file
View File

@@ -0,0 +1,468 @@
#include "dt8.hpp"
#include "dali_define.hpp"
#include <algorithm>
#include <cmath>
DaliDT8::DaliDT8(DaliBase& base) : base_(base) {}
bool DaliDT8::enableDT8() { return base_.dtSelect(8); }
std::optional<ColorTypeFeature> DaliDT8::getColorTypeFeature(int a) {
const auto addr = DaliComm::toCmdAddr(a);
base_.dtSelect(8);
const auto result = base_.queryCmd(addr, DALI_CMD_QUERY_COLOR_TYPE);
if (!result.has_value()) return std::nullopt;
return ColorTypeFeature(result.value());
}
std::optional<ColorStatus> DaliDT8::getColorStatus(int a) {
const auto addr = DaliComm::toCmdAddr(a);
base_.dtSelect(8);
const auto result = base_.queryCmd(addr, DALI_CMD_QUERY_COLOR_STATUS);
if (!result.has_value()) return std::nullopt;
return ColorStatus(result.value());
}
std::optional<int> DaliDT8::getColTempRaw(int a, int type) {
int selector;
switch (type) {
case 0:
selector = 128;
break;
case 1:
selector = 130;
break;
case 3:
selector = 129;
break;
case 4:
selector = 131;
break;
case 2:
default:
selector = 2;
break;
}
const auto features = getColorTypeFeature(a);
if (!features.has_value() || !features->ctCapable()) return 0;
const auto v = getColourRaw(a, selector);
if (!v.has_value()) return 0;
return v.value();
}
bool DaliDT8::setColTempRaw(int a, int value) {
int v = value;
if (v < 0) v = 0;
if (v > 65535) v = 65535;
const int dtr = v & 0xFF;
const int dtr1 = (v >> 8) & 0xFF;
if (!base_.setDTR(dtr)) return false;
if (!base_.setDTR1(dtr1)) return false;
if (!base_.dtSelect(8)) return false;
if (!base_.setDTRAsColourTemp(a)) return false;
if (!base_.dtSelect(8)) return false;
return base_.activate(a);
}
bool DaliDT8::setColorTemperature(int addr, int kelvin) {
int v = kelvin == 0 ? 1 : kelvin;
const int mirek = static_cast<int>(std::floor(1000000.0 / static_cast<double>(v)));
return setColTempRaw(addr, mirek);
}
std::optional<int> DaliDT8::getColorTemperature(int a) {
const auto mirek = getColourRaw(a, 2);
if (!mirek.has_value() || mirek.value() == 0) return std::nullopt;
const int kelvin = static_cast<int>(std::floor(1000000.0 / static_cast<double>(mirek.value())));
return kelvin;
}
std::optional<int> DaliDT8::getMinColorTemperature(int a) {
const auto mirek = getColTempRaw(a, 0);
if (!mirek.has_value() || mirek.value() == 0) return std::nullopt;
return static_cast<int>(std::floor(1000000.0 / static_cast<double>(mirek.value())));
}
std::optional<int> DaliDT8::getMaxColorTemperature(int a) {
const auto mirek = getColTempRaw(a, 1);
if (!mirek.has_value() || mirek.value() == 0) return std::nullopt;
return static_cast<int>(std::floor(1000000.0 / static_cast<double>(mirek.value())));
}
std::optional<int> DaliDT8::getPhysicalMinColorTemperature(int a) {
const auto mirek = getColTempRaw(a, 3);
if (!mirek.has_value() || mirek.value() == 0) return std::nullopt;
return static_cast<int>(std::floor(1000000.0 / static_cast<double>(mirek.value())));
}
std::optional<int> DaliDT8::getPhysicalMaxColorTemperature(int a) {
const auto mirek = getColTempRaw(a, 4);
if (!mirek.has_value() || mirek.value() == 0) return std::nullopt;
return static_cast<int>(std::floor(1000000.0 / static_cast<double>(mirek.value())));
}
bool DaliDT8::setColourRaw(int addr, int x1, int y1) {
const int x1L = x1 & 0xFF;
const int y1L = y1 & 0xFF;
const int x1H = (x1 >> 8) & 0xFF;
const int y1H = (y1 >> 8) & 0xFF;
const int a = addr / 2;
if (!base_.setDTR(x1L)) return false;
if (!base_.setDTR1(x1H)) return false;
if (!base_.dtSelect(8)) return false;
if (!base_.setDTRAsColourX(a)) return false;
if (!base_.setDTR(y1L)) return false;
if (!base_.setDTR1(y1H)) return false;
if (!base_.dtSelect(8)) return false;
if (!base_.setDTRAsColourY(a)) return false;
if (!base_.dtSelect(8)) return false;
return base_.activate(a);
}
bool DaliDT8::setTemporaryColourXRaw(int addr, int x1) {
const int x1L = x1 & 0xFF;
const int x1H = (x1 >> 8) & 0xFF;
const int a = addr / 2;
return base_.setDTR(x1L) && base_.setDTR1(x1H) && base_.dtSelect(8) && base_.setDTRAsColourX(a);
}
bool DaliDT8::setTemporaryColourYRaw(int addr, int y1) {
const int y1L = y1 & 0xFF;
const int y1H = (y1 >> 8) & 0xFF;
const int a = addr / 2;
return base_.setDTR(y1L) && base_.setDTR1(y1H) && base_.dtSelect(8) && base_.setDTRAsColourY(a);
}
bool DaliDT8::setTemporaryColourXY(int a, double x, double y) {
double xClamped = std::clamp(x, 0.0, 1.0);
double yClamped = std::clamp(y, 0.0, 1.0);
const int x1 = static_cast<int>(std::round(xClamped * 65535.0));
const int y1 = static_cast<int>(std::round(yClamped * 65535.0));
const int addr = a * 2 + 1;
return setTemporaryColourXRaw(addr, x1) && setTemporaryColourYRaw(addr, y1);
}
bool DaliDT8::setTemporaryColourTemperature(int a, int kelvin) {
int k = kelvin <= 0 ? 1 : kelvin;
const int mirek = static_cast<int>(std::floor(1000000.0 / static_cast<double>(k)));
const int addr = a * 2 + 1;
const int dtr = mirek & 0xFF;
const int dtr1 = (mirek >> 8) & 0xFF;
const int dec = addr / 2;
return base_.setDTR(dtr) && base_.setDTR1(dtr1) && base_.dtSelect(8) && base_.setDTRAsColourTemp(dec);
}
bool DaliDT8::setTemporaryPrimaryDimLevel(int a, int n, double level) {
int idx = std::clamp(n, 0, 5);
double v = std::clamp(level, 0.0, 1.0);
int raw = static_cast<int>(std::round(v * 65535.0));
if (raw > 65534) raw = 65534;
const int lsb = raw & 0xFF;
const int msb = (raw >> 8) & 0xFF;
const int addr = a * 2 + 1;
return base_.setDTR(lsb) && base_.setDTR1(msb) && base_.setDTR2(idx) &&
base_.dtSelect(8) && base_.sendExtCmd(addr, DALI_CMD_DT8_SET_TEMPORARY_PRIMARY_DIM_LEVEL);
}
bool DaliDT8::setTemporaryRGBDimLevels(int a, int r, int g, int b) {
const int R = std::clamp(r, 0, 255);
const int G = std::clamp(g, 0, 255);
const int B = std::clamp(b, 0, 255);
const int addr = a * 2 + 1;
return base_.setDTR(R) && base_.setDTR1(G) && base_.setDTR2(B) &&
base_.dtSelect(8) && base_.sendExtCmd(addr, DALI_CMD_DT8_SET_TEMPORARY_RGB_DIM_LEVELS);
}
bool DaliDT8::setTemporaryWAFDimLevels(int a, int w, int amber, int freecolour) {
const int W = std::clamp(w, 0, 255);
const int A = std::clamp(amber, 0, 255);
const int F = std::clamp(freecolour, 0, 255);
const int addr = a * 2 + 1;
return base_.setDTR(W) && base_.setDTR1(A) && base_.setDTR2(F) &&
base_.dtSelect(8) && base_.sendExtCmd(addr, DALI_CMD_DT8_SET_TEMPORARY_WAF_DIM_LEVELS);
}
bool DaliDT8::setTemporaryRGBWAFControl(int a, int control) {
const int addr = a * 2 + 1;
return base_.setDTR(control & 0xFF) && base_.dtSelect(8) &&
base_.sendExtCmd(addr, DALI_CMD_DT8_SET_TEMPORARY_RGBWAF_CONTROL);
}
bool DaliDT8::copyReportToTemporary(int a) { return base_.dtSelect(8) && base_.copyReportColourToTemp(a); }
bool DaliDT8::stepXUp(int a) {
return base_.dtSelect(8) && base_.sendExtCmd(a * 2 + 1, DALI_CMD_DT8_STEP_UP_X_COORDINATE);
}
bool DaliDT8::stepXDown(int a) {
return base_.dtSelect(8) && base_.sendExtCmd(a * 2 + 1, DALI_CMD_DT8_STEP_DOWN_X_COORDINATE);
}
bool DaliDT8::stepYUp(int a) {
return base_.dtSelect(8) && base_.sendExtCmd(a * 2 + 1, DALI_CMD_DT8_STEP_UP_Y_COORDINATE);
}
bool DaliDT8::stepYDown(int a) {
return base_.dtSelect(8) && base_.sendExtCmd(a * 2 + 1, DALI_CMD_DT8_STEP_DOWN_Y_COORDINATE);
}
bool DaliDT8::stepTcCooler(int a) {
return base_.dtSelect(8) &&
base_.sendExtCmd(a * 2 + 1, DALI_CMD_DT8_STEP_UP_COLOR_TEMPERATURE);
}
bool DaliDT8::stepTcWarmer(int a) {
return base_.dtSelect(8) &&
base_.sendExtCmd(a * 2 + 1, DALI_CMD_DT8_STEP_DOWN_COLOR_TEMPERATURE);
}
bool DaliDT8::setColourRGBRaw(int addr, int r, int g, int b) {
const int a = addr / 2;
return base_.setDTR(r) && base_.setDTR1(g) && base_.setDTR2(b) && base_.dtSelect(8) && base_.setDTRAsColourRGB(a) && base_.dtSelect(8) && base_.activate(a);
}
bool DaliDT8::setColour(int a, double x, double y) {
double xClamped = std::clamp(x, 0.0, 1.0);
double yClamped = std::clamp(y, 0.0, 1.0);
const int x1 = static_cast<int>(std::round(xClamped * 65535.0));
const int y1 = static_cast<int>(std::round(yClamped * 65535.0));
const int addr = a * 2 + 1;
return setColourRaw(addr, x1, y1);
}
std::optional<int> DaliDT8::getColourRaw(int a, int type) {
const uint8_t code = static_cast<uint8_t>(type & 0xFF);
const auto features = getColorTypeFeature(a);
if (!features.has_value()) return std::nullopt;
if ((code == 2 || code == 128 || code == 129 || code == 130 || code == 131) && !features->ctCapable()) {
return std::nullopt;
}
bool is8bit = false;
if (code == 82 || (code >= 9 && code <= 15) || (code >= 201 && code <= 207) || code == 208 ||
(code >= 233 && code <= 239) || code == 240) {
is8bit = true;
}
if (!base_.setDTR(code)) return std::nullopt;
if (!base_.dtSelect(8)) return std::nullopt;
if (!base_.queryColourValue(a)) return std::nullopt;
const auto dtr = base_.getDTR(a);
const auto dtr1 = base_.getDTR1(a);
if (!dtr.has_value() || !dtr1.has_value()) return std::nullopt;
if (is8bit) {
if (dtr.value() == 0xFF) return std::nullopt;
return dtr.value() & 0xFF;
}
if (dtr.value() == 0xFF && dtr1.value() == 0xFF) return std::nullopt;
return ((dtr1.value() & 0xFF) << 8) | (dtr.value() & 0xFF);
}
std::vector<double> DaliDT8::getColour(int a) {
const auto x = getColourRaw(a, 0);
const auto y = getColourRaw(a, 1);
if (!x.has_value() || !y.has_value()) return {};
return {x.value() / 65535.0, y.value() / 65535.0};
}
bool DaliDT8::setColourRGB(int addr, int r, int g, int b) {
int R = std::clamp(r, 0, 255);
int G = std::clamp(g, 0, 255);
int B = std::clamp(b, 0, 255);
const auto xy = DaliColor::rgb2xy(static_cast<double>(R) / 255.0, static_cast<double>(G) / 255.0,
static_cast<double>(B) / 255.0);
return setColour(addr, xy[0], xy[1]);
}
std::vector<int> DaliDT8::getColourRGB(int a) {
const auto xy = getColour(a);
if (xy.empty()) return {};
const auto rgb = DaliColor::xy2rgb(xy[0], xy[1]);
return {rgb[0], rgb[1], rgb[2]};
}
bool DaliDT8::activateTemporaryColour(int a) { return base_.dtSelect(8) && base_.activate(a); }
std::optional<int> DaliDT8::getPrimaryDimLevel(int a, int n) {
if (n < 0 || n > 5) return std::nullopt;
return getColourRaw(a, 3 + n);
}
std::optional<int> DaliDT8::getRedDimLevel(int a) { return getColourRaw(a, 9); }
std::optional<int> DaliDT8::getGreenDimLevel(int a) { return getColourRaw(a, 10); }
std::optional<int> DaliDT8::getBlueDimLevel(int a) { return getColourRaw(a, 11); }
std::optional<int> DaliDT8::getWhiteDimLevel(int a) { return getColourRaw(a, 12); }
std::optional<int> DaliDT8::getAmberDimLevel(int a) { return getColourRaw(a, 13); }
std::optional<int> DaliDT8::getFreecolourDimLevel(int a) { return getColourRaw(a, 14); }
std::optional<int> DaliDT8::getRGBWAFControl(int a) { return getColourRaw(a, 15); }
std::optional<int> DaliDT8::getTemporaryXRaw(int a) { return getColourRaw(a, 192); }
std::optional<int> DaliDT8::getTemporaryYRaw(int a) { return getColourRaw(a, 193); }
std::optional<int> DaliDT8::getTemporaryColourTemperatureRaw(int a) { return getColourRaw(a, 194); }
std::optional<int> DaliDT8::getTemporaryPrimaryDimLevel(int a, int n) {
if (n < 0 || n > 5) return std::nullopt;
return getColourRaw(a, 195 + n);
}
std::optional<int> DaliDT8::getTemporaryRedDimLevel(int a) { return getColourRaw(a, 201); }
std::optional<int> DaliDT8::getTemporaryGreenDimLevel(int a) { return getColourRaw(a, 202); }
std::optional<int> DaliDT8::getTemporaryBlueDimLevel(int a) { return getColourRaw(a, 203); }
std::optional<int> DaliDT8::getTemporaryWhiteDimLevel(int a) { return getColourRaw(a, 204); }
std::optional<int> DaliDT8::getTemporaryAmberDimLevel(int a) { return getColourRaw(a, 205); }
std::optional<int> DaliDT8::getTemporaryFreecolourDimLevel(int a) { return getColourRaw(a, 206); }
std::optional<int> DaliDT8::getTemporaryRGBWAFControl(int a) { return getColourRaw(a, 207); }
std::optional<int> DaliDT8::getTemporaryColourType(int a) { return getColourRaw(a, 208); }
std::vector<double> DaliDT8::getTemporaryColour(int a) {
const auto x = getTemporaryXRaw(a);
const auto y = getTemporaryYRaw(a);
if (!x.has_value() || !y.has_value()) return {};
return {x.value() / 65535.0, y.value() / 65535.0};
}
std::optional<int> DaliDT8::getTemporaryColorTemperature(int a) {
const auto mirek = getTemporaryColourTemperatureRaw(a);
if (!mirek.has_value() || mirek.value() == 0) return std::nullopt;
return static_cast<int>(std::floor(1000000.0 / static_cast<double>(mirek.value())));
}
std::optional<int> DaliDT8::getReportXRaw(int a) { return getColourRaw(a, 224); }
std::optional<int> DaliDT8::getReportYRaw(int a) { return getColourRaw(a, 225); }
std::optional<int> DaliDT8::getReportColourTemperatureRaw(int a) { return getColourRaw(a, 226); }
std::optional<int> DaliDT8::getReportPrimaryDimLevel(int a, int n) {
if (n < 0 || n > 5) return std::nullopt;
return getColourRaw(a, 227 + n);
}
std::optional<int> DaliDT8::getReportRedDimLevel(int a) { return getColourRaw(a, 233); }
std::optional<int> DaliDT8::getReportGreenDimLevel(int a) { return getColourRaw(a, 234); }
std::optional<int> DaliDT8::getReportBlueDimLevel(int a) { return getColourRaw(a, 235); }
std::optional<int> DaliDT8::getReportWhiteDimLevel(int a) { return getColourRaw(a, 236); }
std::optional<int> DaliDT8::getReportAmberDimLevel(int a) { return getColourRaw(a, 237); }
std::optional<int> DaliDT8::getReportFreecolourDimLevel(int a) { return getColourRaw(a, 238); }
std::optional<int> DaliDT8::getReportRGBWAFControl(int a) { return getColourRaw(a, 239); }
std::optional<int> DaliDT8::getReportColourType(int a) { return getColourRaw(a, 240); }
std::vector<double> DaliDT8::getReportColour(int a) {
const auto x = getReportXRaw(a);
const auto y = getReportYRaw(a);
if (!x.has_value() || !y.has_value()) return {};
return {x.value() / 65535.0, y.value() / 65535.0};
}
std::optional<int> DaliDT8::getReportColorTemperature(int a) {
const auto mirek = getReportColourTemperatureRaw(a);
if (!mirek.has_value() || mirek.value() == 0) return std::nullopt;
return static_cast<int>(std::floor(1000000.0 / static_cast<double>(mirek.value())));
}
std::optional<int> DaliDT8::getNumberOfPrimaries(int a) { return getColourRaw(a, 82); }
std::optional<int> DaliDT8::getPrimaryXRaw(int a, int n) {
if (n < 0 || n > 5) return std::nullopt;
return getColourRaw(a, 64 + 3 * n);
}
std::optional<int> DaliDT8::getPrimaryYRaw(int a, int n) {
if (n < 0 || n > 5) return std::nullopt;
return getColourRaw(a, 65 + 3 * n);
}
std::optional<int> DaliDT8::getPrimaryTy(int a, int n) {
if (n < 0 || n > 5) return std::nullopt;
return getColourRaw(a, 66 + 3 * n);
}
std::vector<double> DaliDT8::getSceneColor(int a, int sense) {
const auto bright = base_.getScene(a, sense);
if (!bright.has_value() || bright.value() == 255) return {};
base_.copyReportColourToTemp(a);
return getColour(a);
}
bool DaliDT8::storePrimaryTy(int a, int n, int ty) {
int idx = std::clamp(n, 0, 5);
int t = std::clamp(ty, 0, 65535);
const int lsb = t & 0xFF;
const int msb = (t >> 8) & 0xFF;
const int addr = a * 2 + 1;
return base_.setDTR(lsb) && base_.setDTR1(msb) && base_.setDTR2(idx) &&
base_.dtSelect(8) && base_.sendExtCmd(addr, DALI_CMD_DT8_STORE_PRIMARY_N_TY);
}
bool DaliDT8::storePrimaryXY(int a, int n, double x, double y) {
int idx = std::clamp(n, 0, 5);
if (!setTemporaryColourXY(a, x, y)) return false;
const int addr = a * 2 + 1;
return base_.setDTR2(idx) && base_.dtSelect(8) &&
base_.sendExtCmd(addr, DALI_CMD_DT8_STORE_PRIMARY_N_XY);
}
bool DaliDT8::storeColourTempLimitRaw(int a, int limitType, int mirek) {
int m = mirek;
if (m < 1) m = 1;
if (m > 65534) m = 65534;
const int lsb = m & 0xFF;
const int msb = (m >> 8) & 0xFF;
const int addr = a * 2 + 1;
return base_.setDTR(lsb) && base_.setDTR1(msb) && base_.setDTR2(limitType & 0xFF) &&
base_.dtSelect(8) && base_.sendExtCmd(addr, DALI_CMD_DT8_STORE_COLOR_TEMPERATURE_LIMIT);
}
bool DaliDT8::storeColourTempLimit(int a, int limitType, int kelvin) {
const int mirek = static_cast<int>(std::floor(1000000.0 / static_cast<double>(kelvin <= 0 ? 1 : kelvin)));
return storeColourTempLimitRaw(a, limitType, mirek);
}
bool DaliDT8::setGearAutoActivate(int a, bool enable) {
const int addr = a * 2 + 1;
const int opts = enable ? 0x01 : 0x00;
return base_.setDTR(opts) && base_.dtSelect(8) &&
base_.sendExtCmd(addr, DALI_CMD_DT8_SET_GEAR_FEATURES);
}
bool DaliDT8::assignColourToLinkedChannels(int a, int colourId) {
const int addr = a * 2 + 1;
const int id = std::clamp(colourId, 0, 6);
return base_.setDTR(id) && base_.dtSelect(8) &&
base_.sendExtCmd(addr, DALI_CMD_DT8_ASSIGN_COLOR_TO_LINKED_CHANNEL);
}
bool DaliDT8::startAutoCalibration(int a) {
const int addr = a * 2 + 1;
return base_.dtSelect(8) && base_.sendExtCmd(addr, DALI_CMD_DT8_START_AUTO_CALIBRATION);
}
std::optional<int> DaliDT8::getGearFeaturesStatus(int a) {
const int addr = a * 2 + 1;
base_.dtSelect(8);
return base_.queryCmd(addr, DALI_CMD_DT8_QUERY_GEAR_FEATURES_STATUS);
}
std::optional<int> DaliDT8::getRGBWAFControlDirect(int a) {
const int addr = a * 2 + 1;
base_.dtSelect(8);
return base_.queryCmd(addr, DALI_CMD_DT8_QUERY_RGBWAF_CONTROL);
}
std::optional<int> DaliDT8::getAssignedColourForChannel(int a, int channelId) {
const int addr = a * 2 + 1;
const int ch = channelId & 0xFF;
if (!base_.setDTR(ch)) return std::nullopt;
if (!base_.dtSelect(8)) return std::nullopt;
return base_.queryCmd(addr, DALI_CMD_DT8_QUERY_ASSIGNED_COLOR);
}
std::optional<int> DaliDT8::getExtendedVersion(int a) {
const int addr = a * 2 + 1;
base_.dtSelect(8);
return base_.queryCmd(addr, DALI_CMD_DT8_QUERY_EXTENDED_VERSION);
}

125
src/sequence.cpp Normal file
View File

@@ -0,0 +1,125 @@
#include "sequence.hpp"
#include <map>
std::string toString(DaliCommandType type) {
static const std::map<DaliCommandType, std::string> map = {
{DaliCommandType::setBright, "setBright"},
{DaliCommandType::on, "on"},
{DaliCommandType::off, "off"},
{DaliCommandType::toScene, "toScene"},
{DaliCommandType::setScene, "setScene"},
{DaliCommandType::removeScene, "removeScene"},
{DaliCommandType::addToGroup, "addToGroup"},
{DaliCommandType::removeFromGroup, "removeFromGroup"},
{DaliCommandType::setFadeTime, "setFadeTime"},
{DaliCommandType::setFadeRate, "setFadeRate"},
{DaliCommandType::wait, "wait"},
{DaliCommandType::modifyShortAddress, "modifyShortAddress"},
{DaliCommandType::deleteShortAddress, "deleteShortAddress"},
};
const auto it = map.find(type);
if (it == map.end()) return "setBright";
return it->second;
}
DaliCommandType commandTypeFromString(const std::string& name, DaliCommandType fallback) {
static const std::map<std::string, DaliCommandType> map = {
{"setBright", DaliCommandType::setBright},
{"on", DaliCommandType::on},
{"off", DaliCommandType::off},
{"toScene", DaliCommandType::toScene},
{"setScene", DaliCommandType::setScene},
{"removeScene", DaliCommandType::removeScene},
{"addToGroup", DaliCommandType::addToGroup},
{"removeFromGroup", DaliCommandType::removeFromGroup},
{"setFadeTime", DaliCommandType::setFadeTime},
{"setFadeRate", DaliCommandType::setFadeRate},
{"wait", DaliCommandType::wait},
{"modifyShortAddress", DaliCommandType::modifyShortAddress},
{"deleteShortAddress", DaliCommandType::deleteShortAddress},
};
const auto it = map.find(name);
if (it == map.end()) return fallback;
return it->second;
}
int DaliCommandParams::getInt(const std::string& key, int def) const {
const auto* value = getObjectValue(data, key);
if (!value) return def;
return value->asInt().value_or(def);
}
DaliCommandParams DaliCommandParams::copy() const { return DaliCommandParams(data); }
DaliValue::Object DaliCommandParams::toJson() const { return data; }
DaliCommandParams DaliCommandParams::fromJson(const DaliValue::Object& json) {
return DaliCommandParams(json);
}
SequenceStep SequenceStep::copy() const {
SequenceStep s;
s.id = id;
s.remark = remark;
s.type = type;
s.params = params.copy();
return s;
}
DaliValue::Object SequenceStep::toJson() const {
DaliValue::Object out;
out["id"] = id;
if (remark.has_value()) out["remark"] = remark.value();
out["type"] = toString(type);
out["params"] = params.toJson();
return out;
}
SequenceStep SequenceStep::fromJson(const DaliValue::Object& json) {
SequenceStep s;
s.id = getObjectString(json, "id").value_or("");
s.remark = getObjectString(json, "remark");
s.type = commandTypeFromString(getObjectString(json, "type").value_or("setBright"));
if (const auto* paramsVal = getObjectValue(json, "params")) {
if (const auto* obj = paramsVal->asObject()) {
s.params = DaliCommandParams::fromJson(*obj);
}
}
return s;
}
DaliValue::Object CommandSequence::toJson() const {
DaliValue::Object out;
out["id"] = id;
out["name"] = name;
DaliValue::Array arr;
arr.reserve(steps.size());
for (const auto& s : steps) {
arr.emplace_back(s.toJson());
}
out["steps"] = std::move(arr);
return out;
}
CommandSequence CommandSequence::fromJson(const DaliValue::Object& json) {
CommandSequence seq;
seq.id = getObjectString(json, "id").value_or("");
seq.name = getObjectString(json, "name").value_or("");
if (const auto* stepsVal = getObjectValue(json, "steps")) {
if (const auto* arr = stepsVal->asArray()) {
seq.steps.reserve(arr->size());
for (const auto& v : *arr) {
if (const auto* stepObj = v.asObject()) {
seq.steps.push_back(SequenceStep::fromJson(*stepObj));
}
}
}
}
return seq;
}

62
src/sequence_store.cpp Normal file
View File

@@ -0,0 +1,62 @@
#include "sequence_store.hpp"
#include <algorithm>
bool SequenceRepository::load() {
if (!loadCallback_) {
sequences_.clear();
return false;
}
DaliValue stored;
if (!loadCallback_(kSequencesKey, &stored)) {
sequences_.clear();
return false;
}
const auto* arr = stored.asArray();
if (!arr) {
sequences_.clear();
return true;
}
sequences_.clear();
sequences_.reserve(arr->size());
for (const auto& v : *arr) {
const auto* obj = v.asObject();
if (!obj) continue;
sequences_.push_back(CommandSequence::fromJson(*obj));
}
return true;
}
bool SequenceRepository::save() const {
if (!saveCallback_) return false;
DaliValue::Array arr;
arr.reserve(sequences_.size());
for (const auto& s : sequences_) {
arr.emplace_back(s.toJson());
}
return saveCallback_(kSequencesKey, DaliValue(std::move(arr)));
}
void SequenceRepository::add(const CommandSequence& s) { sequences_.push_back(s); }
void SequenceRepository::remove(const std::string& id) {
sequences_.erase(
std::remove_if(sequences_.begin(), sequences_.end(),
[&](const CommandSequence& seq) { return seq.id == id; }),
sequences_.end());
}
void SequenceRepository::replace(const CommandSequence& s) {
for (auto& seq : sequences_) {
if (seq.id == s.id) {
seq = s;
return;
}
}
}