From 7e8ac7f56697eb6d089fed8a8951c5b55c227ce3 Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 26 Mar 2026 12:04:08 +0800 Subject: [PATCH] initial commit --- CMakeLists.txt | 16 ++ README.md | 49 ++++ idf_component.yml | 9 + include/addr.hpp | 56 +++++ include/base.hpp | 166 +++++++++++++ include/bus_monitor.hpp | 71 ++++++ include/color.hpp | 30 +++ include/comm.hpp | 3 + include/dali.hpp | 53 ++++ include/dali_comm.hpp | 68 ++++++ include/dali_define.hpp | 157 ++++++++++++ include/decode.hpp | 56 +++++ include/device.hpp | 116 +++++++++ include/dt1.hpp | 129 ++++++++++ include/dt8.hpp | 156 ++++++++++++ include/errors.hpp | 111 +++++++++ include/log.hpp | 59 +++++ include/model_value.hpp | 144 +++++++++++ include/query_scheduler.hpp | 22 ++ include/sequence.hpp | 62 +++++ include/sequence_store.hpp | 37 +++ src/addr.cpp | 386 +++++++++++++++++++++++++++++ src/base.cpp | 396 ++++++++++++++++++++++++++++++ src/color.cpp | 145 +++++++++++ src/dali_comm.cpp | 156 ++++++++++++ src/decode.cpp | 418 ++++++++++++++++++++++++++++++++ src/device.cpp | 352 +++++++++++++++++++++++++++ src/dt1.cpp | 226 +++++++++++++++++ src/dt8.cpp | 468 ++++++++++++++++++++++++++++++++++++ src/sequence.cpp | 125 ++++++++++ src/sequence_store.cpp | 62 +++++ 31 files changed, 4304 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 idf_component.yml create mode 100644 include/addr.hpp create mode 100644 include/base.hpp create mode 100644 include/bus_monitor.hpp create mode 100644 include/color.hpp create mode 100644 include/comm.hpp create mode 100644 include/dali.hpp create mode 100644 include/dali_comm.hpp create mode 100644 include/dali_define.hpp create mode 100644 include/decode.hpp create mode 100644 include/device.hpp create mode 100644 include/dt1.hpp create mode 100644 include/dt8.hpp create mode 100644 include/errors.hpp create mode 100644 include/log.hpp create mode 100644 include/model_value.hpp create mode 100644 include/query_scheduler.hpp create mode 100644 include/sequence.hpp create mode 100644 include/sequence_store.hpp create mode 100644 src/addr.cpp create mode 100644 src/base.cpp create mode 100644 src/color.cpp create mode 100644 src/dali_comm.cpp create mode 100644 src/decode.cpp create mode 100644 src/device.cpp create mode 100644 src/dt1.cpp create mode 100644 src/dt8.cpp create mode 100644 src/sequence.cpp create mode 100644 src/sequence_store.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..cbf4d60 --- /dev/null +++ b/CMakeLists.txt @@ -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) diff --git a/README.md b/README.md new file mode 100644 index 0000000..c8b9f24 --- /dev/null +++ b/README.md @@ -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 { + return my_uart_read(len, timeoutMs); + }, + /* transact */ [](const uint8_t* data, size_t len) -> std::vector { + 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 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. diff --git a/idf_component.yml b/idf_component.yml new file mode 100644 index 0000000..c93aff6 --- /dev/null +++ b/idf_component.yml @@ -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 diff --git a/include/addr.hpp b/include/addr.hpp new file mode 100644 index 0000000..1534113 --- /dev/null +++ b/include/addr.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include "base.hpp" + +#include +#include +#include + +struct DaliCompareAddrResult { + int retH = 0; + int retM = 0; + int retL = 0; + int nextAddr = 0; +}; + +class DaliAddr { + public: + explicit DaliAddr(DaliBase& base); + + std::vector 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 searchAddr(int addr = 63); + std::vector searchAddrRange(int start = 0, int end = 63); + void stopSearch(); + + bool compareSingleAddress(int typ, int addr); + std::pair precompareNew(int typ, int m = 0); + int compareAddress(int typ); + int compareAddressNew(int typ, int m = 0); + DaliCompareAddrResult compareAddr(int ad, std::optional minH, std::optional minM, + std::optional 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 getSceneBright(int addr, int scene); + bool resetAndAllocAddr(int n = 0, bool removeAddrFirst = false, bool closeLight = false); + + private: + DaliBase& base_; +}; diff --git a/include/base.hpp b/include/base.hpp new file mode 100644 index 0000000..bbf9349 --- /dev/null +++ b/include/base.hpp @@ -0,0 +1,166 @@ +#pragma once + +#include "dali_comm.hpp" + +#include +#include +#include +#include +#include +#include +#include + +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 getDTR(int a); + std::optional getDTR1(int a); + std::optional 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 getOnlineStatus(int a); + std::optional getBright(int a); + std::optional getDeviceType(int a); + std::optional getPhysicalMinLevel(int a); + std::optional 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> getGradualChange(int a); + std::optional getGradualChangeRate(int a); + std::optional getGradualChangeSpeed(int a); + + bool setPowerOnLevel(int a, int value); + std::optional getPowerOnLevel(int a); + bool setSystemFailureLevel(int a, int value); + std::optional getSystemFailureLevel(int a); + bool setMinLevel(int a, int value); + std::optional getMinLevel(int a); + bool setMaxLevel(int a, int value); + std::optional getMaxLevel(int a); + bool setFadeTime(int a, int value); + std::optional getFadeTime(int a); + bool setFadeRate(int a, int value); + std::optional getFadeRate(int a); + std::optional getNextDeviceType(int a); + + std::optional getGroupH(int a); + std::optional getGroupL(int a); + std::optional getGroup(int a); + bool setGroup(int a, int value); + + std::optional getScene(int a, int b); + bool setScene(int a, int b); + std::map getScenes(int a); + + std::optional getStatus(int a); + std::optional getControlGearPresent(int a); + std::optional getLampFailureStatus(int a); + std::optional getLampPowerOnStatus(int a); + std::optional getLimitError(int a); + std::optional getResetStatus(int a); + std::optional 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 queryShortAddr(); + std::optional verifyShortAddr(int a); + std::optional compareAddress(); + std::optional compare(int h, int m, int l); + + std::optional getRandomAddrH(int addr); + std::optional getRandomAddrM(int addr); + std::optional getRandomAddrL(int addr); + + // Exposed query helpers mirroring Dart API. + std::optional query(int a, uint8_t cmd); + std::optional queryCmd(uint8_t addr, uint8_t cmd); + + private: + DaliComm& comm_; + + static uint8_t encodeCmdAddr(int dec_addr); + static uint8_t encodeArcAddr(int dec_addr); +}; diff --git a/include/bus_monitor.hpp b/include/bus_monitor.hpp new file mode 100644 index 0000000..8641aec --- /dev/null +++ b/include/bus_monitor.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include "decode.hpp" + +#include +#include +#include + +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 sink) { rawSink_ = std::move(sink); } + void setDecodedSink(std::function 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 rawFrames; + std::vector records; + + private: + BusMonitor() = default; + + std::function rawSink_; + std::function decodedSink_; +}; diff --git a/include/color.hpp b/include/color.hpp new file mode 100644 index 0000000..087bcea --- /dev/null +++ b/include/color.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include + +// Colour conversion utilities mirroring lib/dali/color.dart. +class DaliColor { + public: + static std::array 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 gammaCorrection(double r, double g, double b, double gamma = 2.8); + + static std::array rgb2xyz(double r, double g, double b); + static std::array xyz2rgb(double x, double y, double z); + static std::array xyz2xy(double x, double y, double z); + static std::array xy2xyz(double xVal, double yVal); + static std::array rgb2xy(double r, double g, double b); + static std::array xy2rgb(double xVal, double yVal); + + static std::array xyz2lab(double x, double y, double z); + static std::array rgb2lab(double r, double g, double b); + static std::array lab2xyz(double l, double a, double b); + static std::array lab2rgb(double l, double a, double b); + + private: + static double srgbToLinear(double value); + static double linearToSrgb(double value); +}; diff --git a/include/comm.hpp b/include/comm.hpp new file mode 100644 index 0000000..5548822 --- /dev/null +++ b/include/comm.hpp @@ -0,0 +1,3 @@ +#pragma once + +#include "dali_comm.hpp" diff --git a/include/dali.hpp b/include/dali.hpp new file mode 100644 index 0000000..68e7105 --- /dev/null +++ b/include/dali.hpp @@ -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 +#include + +// Convenience umbrella header for the ESP-IDF DALI component. + +class Dali { + public: + static Dali& instance(DaliComm& comm) { + if (!instance_) { + instance_ = std::unique_ptr(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 instance_ = nullptr; +}; diff --git a/include/dali_comm.hpp b/include/dali_comm.hpp new file mode 100644 index 0000000..196982e --- /dev/null +++ b/include/dali_comm.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include +#include +#include + +// 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; + using ReadCallback = std::function(size_t len, uint32_t timeout_ms)>; + using TransactCallback = std::function(const uint8_t* data, size_t len)>; + using DelayCallback = std::function; + + 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 checksum(const std::vector& data); + bool write(const std::vector& data) const; + std::vector 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 queryRaw(uint8_t addr, uint8_t cmd) const; + std::optional queryRawNew(uint8_t addr, uint8_t cmd) const; + bool send(int dec_addr, uint8_t cmd) const; + std::optional 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 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& frame) const; + void sleepMs(uint32_t ms) const; +}; diff --git a/include/dali_define.hpp b/include/dali_define.hpp new file mode 100644 index 0000000..0c4d2f3 --- /dev/null +++ b/include/dali_define.hpp @@ -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 \ No newline at end of file diff --git a/include/decode.hpp b/include/decode.hpp new file mode 100644 index 0000000..760af0b --- /dev/null +++ b/include/decode.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include +#include + +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 cmd_; + std::map sCMD_; + std::vector 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; +}; diff --git a/include/device.hpp b/include/device.hpp new file mode 100644 index 0000000..58a2fc0 --- /dev/null +++ b/include/device.hpp @@ -0,0 +1,116 @@ +#pragma once + +#include "model_value.hpp" + +#include +#include +#include + +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 supportsDt1; + std::optional supportsDt8; + + static DaliDeviceCapabilities fromJson(const DaliValue::Object* json); + DaliValue::Object toJson() const; + void merge(const DaliDeviceCapabilities& other); +}; + +struct DaliStatusFlags { + std::optional controlGearPresent; + std::optional lampFailure; + std::optional lampPowerOn; + std::optional limitError; + std::optional fadingCompleted; + std::optional resetState; + std::optional missingShortAddress; + std::optional psFault; + + static DaliStatusFlags fromJson(const DaliValue::Object* json); + DaliValue::Object toJson() const; + bool hasData() const; + void merge(const DaliStatusFlags& other); +}; + +struct DaliDt8State { + std::optional colorType; + std::optional activeMode; + std::optional xyX; + std::optional xyY; + std::optional xyMinX; + std::optional xyMaxX; + std::optional xyMinY; + std::optional xyMaxY; + std::optional mirek; + std::optional mirekMin; + std::optional mirekMax; + std::optional> rgbwaf; + std::optional> primaryN; + + static DaliDt8State fromJson(const DaliValue::Object* json); + DaliValue::Object toJson() const; +}; + +struct DaliDt1State { + std::optional emergencyLevel; + std::optional emergencyMinLevel; + std::optional emergencyMaxLevel; + std::optional prolongTimeMinutes; + std::optional ratedDurationMinutes; + std::optional testDelayTime; + std::optional failureStatus; + std::optional emergencyStatus; + std::optional emergencyMode; + std::optional feature; + std::optional version; + + static DaliDt1State fromJson(const DaliValue::Object* json); + DaliValue::Object toJson() const; +}; + +struct DaliDevice { + std::string id; + std::string name; + std::optional shortAddress; + std::optional longAddress; + bool isolated = false; + + std::optional brightness; + std::optional groupBits; + std::optional> scenes; + + std::optional fadeTime; + std::optional fadeRate; + std::optional powerOnLevel; + std::optional systemFailureLevel; + std::optional minLevel; + std::optional maxLevel; + std::optional operatingMode; + std::optional physicalMinLevel; + + std::optional deviceType; + std::vector extType; + std::optional version; + + DaliDeviceCapabilities capabilities; + std::optional dt8; + std::optional dt1; + DaliStatusFlags statusFlags; + + std::optional lastSyncedUtc; + DaliValue::Object metadata; + + static DaliDevice fromJson(const DaliValue::Object& json); + DaliValue::Object toJson() const; + + std::string displayName() const; + void merge(const DaliDevice& other); +}; diff --git a/include/dt1.hpp b/include/dt1.hpp new file mode 100644 index 0000000..5f30707 --- /dev/null +++ b/include/dt1.hpp @@ -0,0 +1,129 @@ +#pragma once + +#include "base.hpp" + +#include +#include + +struct DT1TestStatusDetailed { + std::optional failureStatus; + std::optional emergencyStatus; + std::optional emergencyMode; + std::optional 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 getDT1EmergencyMode(int a); + std::optional getDT1Feature(int a); + std::optional getDT1FailureStatus(int a); + std::optional getDT1Status(int a); + std::optional getDT1SelfTestStatus(int a); + std::optional 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& dtr0 = std::nullopt, + const std::optional& dtr1 = std::nullopt); + std::optional getExtendedVersionDT1(int a); + + std::optional getEmergencyLevel(int a); + std::optional getEmergencyMinLevel(int a); + std::optional getEmergencyMaxLevel(int a); + std::optional getProlongTimeMinutes(int a); + std::optional getFunctionTestIntervalDays(int a); + std::optional getDurationTestIntervalWeeks(int a); + std::optional getDurationTestResult(int a); + std::optional getLampEmergencyTimeMinutes(int a); + std::optional getRatedDurationMinutes(int a); + + std::optional getDeviceStatus(int a); + std::optional getEmergencyStatusDecoded(int a); + + private: + DaliBase& base_; + + bool enable(); + static int addrOf(int a); + bool send(int a, int code); + std::optional query(int a, int code); +}; + diff --git a/include/dt8.hpp b/include/dt8.hpp new file mode 100644 index 0000000..355618e --- /dev/null +++ b/include/dt8.hpp @@ -0,0 +1,156 @@ +#pragma once + +#include "base.hpp" +#include "color.hpp" + +#include +#include +#include + +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 getColorTypeFeature(int a); + std::optional getColorStatus(int a); + + std::optional getColTempRaw(int a, int type = 2); + bool setColTempRaw(int a, int value); + bool setColorTemperature(int addr, int kelvin); + std::optional getColorTemperature(int a); + std::optional getMinColorTemperature(int a); + std::optional getMaxColorTemperature(int a); + std::optional getPhysicalMinColorTemperature(int a); + std::optional 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 getColourRaw(int a, int type); + std::vector getColour(int a); + bool setColourRGB(int addr, int r, int g, int b); + std::vector getColourRGB(int a); + + bool activateTemporaryColour(int a); + + // Active-type queries + std::optional getPrimaryDimLevel(int a, int n); + std::optional getRedDimLevel(int a); + std::optional getGreenDimLevel(int a); + std::optional getBlueDimLevel(int a); + std::optional getWhiteDimLevel(int a); + std::optional getAmberDimLevel(int a); + std::optional getFreecolourDimLevel(int a); + std::optional getRGBWAFControl(int a); + + // Temporary colour queries + std::optional getTemporaryXRaw(int a); + std::optional getTemporaryYRaw(int a); + std::optional getTemporaryColourTemperatureRaw(int a); + std::optional getTemporaryPrimaryDimLevel(int a, int n); + std::optional getTemporaryRedDimLevel(int a); + std::optional getTemporaryGreenDimLevel(int a); + std::optional getTemporaryBlueDimLevel(int a); + std::optional getTemporaryWhiteDimLevel(int a); + std::optional getTemporaryAmberDimLevel(int a); + std::optional getTemporaryFreecolourDimLevel(int a); + std::optional getTemporaryRGBWAFControl(int a); + std::optional getTemporaryColourType(int a); + std::vector getTemporaryColour(int a); + std::optional getTemporaryColorTemperature(int a); + + // Report colour queries + std::optional getReportXRaw(int a); + std::optional getReportYRaw(int a); + std::optional getReportColourTemperatureRaw(int a); + std::optional getReportPrimaryDimLevel(int a, int n); + std::optional getReportRedDimLevel(int a); + std::optional getReportGreenDimLevel(int a); + std::optional getReportBlueDimLevel(int a); + std::optional getReportWhiteDimLevel(int a); + std::optional getReportAmberDimLevel(int a); + std::optional getReportFreecolourDimLevel(int a); + std::optional getReportRGBWAFControl(int a); + std::optional getReportColourType(int a); + std::vector getReportColour(int a); + std::optional getReportColorTemperature(int a); + + std::optional getNumberOfPrimaries(int a); + std::optional getPrimaryXRaw(int a, int n); + std::optional getPrimaryYRaw(int a, int n); + std::optional getPrimaryTy(int a, int n); + + std::vector 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 getGearFeaturesStatus(int a); + std::optional getRGBWAFControlDirect(int a); + std::optional getAssignedColourForChannel(int a, int channelId); + std::optional getExtendedVersion(int a); + + private: + DaliBase& base_; +}; diff --git a/include/errors.hpp b/include/errors.hpp new file mode 100644 index 0000000..a3f5e34 --- /dev/null +++ b/include/errors.hpp @@ -0,0 +1,111 @@ +#pragma once + +#include +#include +#include +#include + +class DaliQueryException : public std::exception { + public: + enum class Code { + busUnavailable, + gatewayTimeout, + deviceNoResponse, + invalidFrame, + invalidGatewayFrame, + unknown, + }; + + DaliQueryException(Code code, + std::string message, + std::optional addr = std::nullopt, + std::optional 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& addr() const { return addr_; } + const std::optional& cmd() const { return cmd_; } + + private: + Code code_ = Code::unknown; + std::string message_; + std::optional addr_; + std::optional cmd_; +}; + +class DaliBusUnavailableException : public DaliQueryException { + public: + explicit DaliBusUnavailableException(std::optional addr = std::nullopt, + std::optional cmd = std::nullopt) + : DaliQueryException(Code::busUnavailable, "Bus unavailable", addr, cmd) {} +}; + +class DaliGatewayTimeoutException : public DaliQueryException { + public: + explicit DaliGatewayTimeoutException(std::optional addr = std::nullopt, + std::optional cmd = std::nullopt) + : DaliQueryException(Code::gatewayTimeout, "Gateway no response", addr, cmd) {} +}; + +class DaliDeviceNoResponseException : public DaliQueryException { + public: + explicit DaliDeviceNoResponseException(std::optional addr = std::nullopt, + std::optional cmd = std::nullopt) + : DaliQueryException(Code::deviceNoResponse, "Device no response", addr, cmd) {} +}; + +class DaliInvalidFrameException : public DaliQueryException { + public: + explicit DaliInvalidFrameException(std::optional addr = std::nullopt, + std::optional cmd = std::nullopt) + : DaliQueryException(Code::invalidFrame, "Invalid frame", addr, cmd) {} +}; + +class DaliInvalidGatewayFrameException : public DaliQueryException { + public: + explicit DaliInvalidGatewayFrameException(std::optional addr = std::nullopt, + std::optional 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 +std::optional daliSafe(const std::function& action, + const std::function& 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 +} diff --git a/include/log.hpp b/include/log.hpp new file mode 100644 index 0000000..13d25f2 --- /dev/null +++ b/include/log.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include +#include + +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 lock(mu_); + level_ = level; + } + + LogLevel currentLevel() const { return level_; } + + void setSink(std::function sink) { + std::lock_guard 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 logMessages() const { + std::lock_guard lock(mu_); + return logs_; + } + + void clearLogs() { + std::lock_guard lock(mu_); + logs_.clear(); + } + + private: + DaliLog() = default; + + void log(LogLevel level, const char* tag, const std::string& message) { + std::lock_guard lock(mu_); + if (static_cast(level) < static_cast(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 logs_; + std::function sink_; +}; diff --git a/include/model_value.hpp b/include/model_value.hpp new file mode 100644 index 0000000..5f33519 --- /dev/null +++ b/include/model_value.hpp @@ -0,0 +1,144 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +class DaliValue { + public: + using Array = std::vector; + using Object = std::map; + + using Variant = std::variant; + + DaliValue() = default; + DaliValue(std::nullptr_t) : value_(std::monostate{}) {} + DaliValue(bool v) : value_(v) {} + DaliValue(int v) : value_(static_cast(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(value_); } + bool isBool() const { return std::holds_alternative(value_); } + bool isInt() const { return std::holds_alternative(value_); } + bool isDouble() const { return std::holds_alternative(value_); } + bool isString() const { return std::holds_alternative(value_); } + bool isArray() const { return std::holds_alternative(value_); } + bool isObject() const { return std::holds_alternative(value_); } + + std::optional asBool() const { + if (isBool()) return std::get(value_); + if (isInt()) return std::get(value_) != 0; + if (isString()) { + const auto& s = std::get(value_); + if (s == "true" || s == "TRUE" || s == "1") return true; + if (s == "false" || s == "FALSE" || s == "0") return false; + } + return std::nullopt; + } + + std::optional asInt() const { + if (isInt()) return static_cast(std::get(value_)); + if (isDouble()) return static_cast(std::get(value_)); + if (isString()) { + const auto& s = std::get(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(parsed); + } + return std::nullopt; + } + + std::optional asDouble() const { + if (isDouble()) return std::get(value_); + if (isInt()) return static_cast(std::get(value_)); + if (isString()) { + const auto& s = std::get(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 asString() const { + if (isString()) return std::get(value_); + if (isBool()) return std::get(value_) ? "true" : "false"; + if (isInt()) return std::to_string(std::get(value_)); + if (isDouble()) return std::to_string(std::get(value_)); + return std::nullopt; + } + + const Array* asArray() const { + if (!isArray()) return nullptr; + return &std::get(value_); + } + + const Object* asObject() const { + if (!isObject()) return nullptr; + return &std::get(value_); + } + + Array* asArray() { + if (!isArray()) return nullptr; + return &std::get(value_); + } + + Object* asObject() { + if (!isObject()) return nullptr; + return &std::get(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 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 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 getObjectString(const DaliValue::Object& obj, + const std::string& key) { + const auto* v = getObjectValue(obj, key); + if (!v) return std::nullopt; + return v->asString(); +} diff --git a/include/query_scheduler.hpp b/include/query_scheduler.hpp new file mode 100644 index 0000000..9ca1f15 --- /dev/null +++ b/include/query_scheduler.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +class DaliQueryScheduler { + public: + static DaliQueryScheduler& instance() { + static DaliQueryScheduler inst; + return inst; + } + + template + auto run(Fn&& action) -> decltype(action()) { + std::lock_guard lock(mu_); + return action(); + } + + private: + DaliQueryScheduler() = default; + std::mutex mu_; +}; diff --git a/include/sequence.hpp b/include/sequence.hpp new file mode 100644 index 0000000..eb2c3b2 --- /dev/null +++ b/include/sequence.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include "model_value.hpp" + +#include +#include +#include + +// 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 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 steps; + + DaliValue::Object toJson() const; + static CommandSequence fromJson(const DaliValue::Object& json); +}; diff --git a/include/sequence_store.hpp b/include/sequence_store.hpp new file mode 100644 index 0000000..defdbb8 --- /dev/null +++ b/include/sequence_store.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "model_value.hpp" +#include "sequence.hpp" + +#include +#include +#include + +constexpr const char* kSequencesKey = "command_sequences_v1"; + +class SequenceRepository { + public: + using LoadCallback = std::function; + using SaveCallback = std::function; + + 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& sequences() const { return sequences_; } + + void add(const CommandSequence& s); + void remove(const std::string& id); + void replace(const CommandSequence& s); + + private: + std::vector sequences_; + LoadCallback loadCallback_; + SaveCallback saveCallback_; +}; diff --git a/src/addr.cpp b/src/addr.cpp new file mode 100644 index 0000000..9244e97 --- /dev/null +++ b/src/addr.cpp @@ -0,0 +1,386 @@ +#include "addr.hpp" + +#include +#include +#include +#include + +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 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 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 DaliAddr::precompareNew(int typ, int m) { + int min = m; + int max = 255; + while ((max - min) > 6) { + const int mid = static_cast((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 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 /*minH*/, + std::optional /*minM*/, std::optional /*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(value)); +} + +std::optional DaliAddr::getSceneBright(int addr, int scene) { + const int value = scene + 176; + return base_.query(addr, static_cast(value)); +} + +bool DaliAddr::resetAndAllocAddr(int n, bool removeAddrFirst, bool closeLight) { + const int startTime = static_cast(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; +} diff --git a/src/base.cpp b/src/base.cpp new file mode 100644 index 0000000..0848304 --- /dev/null +++ b/src/base.cpp @@ -0,0 +1,396 @@ +#include "base.hpp" + +#include "dali_define.hpp" + +#include + +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::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(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(brightness) + 1.0) / std::log(256.0) * 255.0; + return static_cast(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(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(bright)); +} + +bool DaliBase::setBrightPercentage(int a, double b) { + int bright = static_cast(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(cmd), static_cast(value)); } + +bool DaliBase::setDTR(int value) { return comm_.sendCmd(DALI_CMD_SPECIAL_SET_DTR0, static_cast(value)); } + +bool DaliBase::setDTR1(int value) { return comm_.sendCmd(DALI_CMD_SPECIAL_SET_DTR_1, static_cast(value)); } + +bool DaliBase::setDTR2(int value) { return comm_.sendCmd(DALI_CMD_SPECIAL_SET_DTR_2, static_cast(value)); } + +std::optional DaliBase::getDTR(int a) { return query(a, DALI_CMD_QUERY_CONTENT_DTR); } + +std::optional DaliBase::getDTR1(int a) { return query(a, DALI_CMD_QUERY_CONTENT_DTR1); } + +std::optional 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 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 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 DaliBase::getDeviceType(int a) { return queryCmd(encodeCmdAddr(a), DALI_CMD_QUERY_DEVICE_TYPE); } + +std::optional DaliBase::getPhysicalMinLevel(int a) { + return queryCmd(encodeCmdAddr(a), DALI_CMD_QUERY_PHYSICAL_MINIMUM_LEVEL); +} + +std::optional 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(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> 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 DaliBase::getGradualChangeRate(int a) { + const auto rs = getGradualChange(a); + if (!rs.has_value()) return std::nullopt; + return rs->second; +} + +std::optional 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 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 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 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 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 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 DaliBase::getFadeRate(int a) { + const auto rs = getGradualChange(a); + if (!rs.has_value()) return std::nullopt; + return rs->second; +} + +std::optional DaliBase::getNextDeviceType(int a) { return query(a, DALI_CMD_QUERY_NEXT_DEVICE_TYPE); } + +std::optional DaliBase::getGroupH(int a) { return query(a, DALI_CMD_QUERY_GROUP_8_15); } + +std::optional DaliBase::getGroupL(int a) { return query(a, DALI_CMD_QUERY_GROUPS_0_7); } + +std::optional 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 DaliBase::getScene(int a, int b) { + return query(a, static_cast(DALI_CMD_QUERY_SCENE_LEVEL(b))); +} + +bool DaliBase::setScene(int a, int b) { return setDTR(b) && storeDTRAsSceneBright(a, b); } + +std::map DaliBase::getScenes(int a) { + std::map 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 DaliBase::getStatus(int a) { return query(a, DALI_CMD_QUERY_STATUS); } + +std::optional 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 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 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 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 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 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(addr)); +} + +bool DaliBase::queryAddressM(int addr) { + return comm_.sendCmd(DALI_CMD_SPECIAL_SEARCHADDRM, static_cast(addr)); +} + +bool DaliBase::queryAddressL(int addr) { + return comm_.sendCmd(DALI_CMD_SPECIAL_SEARCHADDRL, static_cast(addr)); +} + +bool DaliBase::programShortAddr(int a) { + return comm_.sendCmd(DALI_CMD_SPECIAL_PROGRAM_SHORT_ADDRESS, encodeCmdAddr(a)); +} + +std::optional 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 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 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 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 DaliBase::getRandomAddrH(int addr) { return query(addr, DALI_CMD_QUERY_RANDOM_ADDRESS_H); } + +std::optional DaliBase::getRandomAddrM(int addr) { return query(addr, DALI_CMD_QUERY_RANDOM_ADDRESS_M); } + +std::optional DaliBase::getRandomAddrL(int addr) { return query(addr, DALI_CMD_QUERY_RANDOM_ADDRESS_L); } + +std::optional DaliBase::query(int a, uint8_t cmd) { return comm_.queryCmd(encodeCmdAddr(a), cmd); } + +std::optional DaliBase::queryCmd(uint8_t addr, uint8_t cmd) { return comm_.queryCmd(addr, cmd); } diff --git a/src/color.cpp b/src/color.cpp new file mode 100644 index 0000000..0c64432 --- /dev/null +++ b/src/color.cpp @@ -0,0 +1,145 @@ +#include "color.hpp" + +#include +#include + +std::array DaliColor::toIntList(double a, double r, double g, double b) { + const int ai = static_cast(a * 255.0); + const int ri = static_cast(r * 255.0); + const int gi = static_cast(g * 255.0); + const int bi = static_cast(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 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 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 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(std::clamp(std::round(v * 255.0), 0.0, 255.0)); + }; + + return {toChannel(sr), toChannel(sg), toChannel(sb)}; +} + +std::array 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 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 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 DaliColor::xy2rgb(double xVal, double yVal) { + const auto xyz = xy2xyz(xVal, yVal); + return xyz2rgb(xyz[0], xyz[1], xyz[2]); +} + +std::array 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 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 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 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); +} diff --git a/src/dali_comm.cpp b/src/dali_comm.cpp new file mode 100644 index 0000000..fb5901a --- /dev/null +++ b/src/dali_comm.cpp @@ -0,0 +1,156 @@ +#include "dali_comm.hpp" + +#include +#include + +#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 DaliComm::checksum(const std::vector& data) { + std::vector out = data; + uint32_t sum = 0; + for (const auto b : out) { + sum += b; + } + out.push_back(static_cast(sum & 0xFF)); + return out; +} + +bool DaliComm::write(const std::vector& data) const { return writeFrame(data); } + +std::vector 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 usbProbe{0x01, 0x00, 0x00}; + const std::vector legacyProbe{0x28, 0x01, static_cast(gateway), 0x11, 0x00, 0x00, + 0xFF}; + const std::vector newProbe{0x28, 0x01, static_cast(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 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 DaliComm::queryRaw(uint8_t addr, uint8_t cmd) const { return queryCmd(addr, cmd); } + +std::optional 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 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(dec_addr * 2 + 1); } + +uint8_t DaliComm::toArcAddr(int dec_addr) { return static_cast(dec_addr * 2); } + +bool DaliComm::writeFrame(const std::vector& 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 frame{0x10, addr, cmd}; + return writeFrame(frame); +} + +bool DaliComm::sendExtCmd(uint8_t addr, uint8_t cmd) const { + const std::vector frame{0x11, addr, cmd}; + const bool ret = writeFrame(frame); + sleepMs(100); + return ret; +} + +std::optional DaliComm::queryCmd(uint8_t addr, uint8_t cmd) const { + if (!transact_) return std::nullopt; + const std::vector frame{0x12, addr, cmd}; + const auto resp = transact_(frame.data(), frame.size()); + if (resp.empty()) return std::nullopt; + + // Gateway type 1 returns: 0xFF 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(); +} diff --git a/src/decode.cpp b/src/decode.cpp new file mode 100644 index 0000000..bf08dd0 --- /dev/null +++ b/src/decode.cpp @@ -0,0 +1,418 @@ +#include "decode.hpp" + +#include "base.hpp" +#include "dali_define.hpp" + +#include +#include +#include +#include + +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(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(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::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 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 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 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; +} diff --git a/src/device.cpp b/src/device.cpp new file mode 100644 index 0000000..d424e03 --- /dev/null +++ b/src/device.cpp @@ -0,0 +1,352 @@ +#include "device.hpp" + +#include + +namespace { + +std::optional> asIntList(const DaliValue* value) { + if (!value) return std::nullopt; + const auto* arr = value->asArray(); + if (!arr) return std::nullopt; + std::vector 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>& 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{}); + 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; + } +} diff --git a/src/dt1.cpp b/src/dt1.cpp new file mode 100644 index 0000000..ce1bcb9 --- /dev/null +++ b/src/dt1.cpp @@ -0,0 +1,226 @@ +#include "dt1.hpp" + +#include "dali_define.hpp" + +#include +#include +#include + +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 DaliDT1::query(int a, int code) { + if (!enable()) return std::nullopt; + const auto v = base_.queryCmd(static_cast(addrOf(a)), static_cast(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 DaliDT1::getDT1EmergencyMode(int a) { return query(a, DALI_CMD_DT1_QUERY_EMERGENCY_MODE); } + +std::optional DaliDT1::getDT1Feature(int a) { return query(a, DALI_CMD_DT1_QUERY_FEATURE); } + +std::optional DaliDT1::getDT1FailureStatus(int a) { + return query(a, DALI_CMD_DT1_QUERY_FAILURE_STATUS); +} + +std::optional DaliDT1::getDT1Status(int a) { return query(a, DALI_CMD_DT1_QUERY_STATUS); } + +std::optional 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 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& dtr0, + const std::optional& 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 DaliDT1::getExtendedVersionDT1(int a) { + return query(a, DALI_CMD_DT1_QUERY_EXTENDED_VERSION); +} + +std::optional DaliDT1::getEmergencyLevel(int a) { return query(a, DALI_CMD_DT1_QUERY_EMERGENCY_LEVEL); } + +std::optional DaliDT1::getEmergencyMinLevel(int a) { + return query(a, DALI_CMD_DT1_QUERY_EMERGENCY_MIN_LEVEL); +} + +std::optional DaliDT1::getEmergencyMaxLevel(int a) { + return query(a, DALI_CMD_DT1_QUERY_EMERGENCY_MAX_LEVEL); +} + +std::optional DaliDT1::getProlongTimeMinutes(int a) { + return query(a, DALI_CMD_DT1_QUERY_PROLONG_TIME); +} + +std::optional DaliDT1::getFunctionTestIntervalDays(int a) { + return query(a, DALI_CMD_DT1_QUERY_FUNCTION_TEST_INTERVAL); +} + +std::optional DaliDT1::getDurationTestIntervalWeeks(int a) { + return query(a, DALI_CMD_DT1_QUERY_DURATION_TEST_INTERVAL); +} + +std::optional DaliDT1::getDurationTestResult(int a) { + return query(a, DALI_CMD_DT1_QUERY_DURATION_TEST_RESULT); +} + +std::optional DaliDT1::getLampEmergencyTimeMinutes(int a) { + return query(a, DALI_CMD_DT1_QUERY_LAMP_EMERGENCY_TIME); +} + +std::optional DaliDT1::getRatedDurationMinutes(int a) { + return query(a, DALI_CMD_DT1_QUERY_RATED_DURATION); +} + +std::optional DaliDT1::getDeviceStatus(int a) { + if (!enable()) return std::nullopt; + const auto raw = base_.queryCmd(static_cast(addrOf(a)), DALI_CMD_QUERY_STATUS); + if (!raw.has_value()) return std::nullopt; + return DaliDT1DeviceStatus(raw.value()); +} + +std::optional DaliDT1::getEmergencyStatusDecoded(int a) { + const auto v = getDT1Status(a); + if (!v.has_value()) return std::nullopt; + return DaliDT1EmergencyStatus(v.value()); +} diff --git a/src/dt8.cpp b/src/dt8.cpp new file mode 100644 index 0000000..eb4999c --- /dev/null +++ b/src/dt8.cpp @@ -0,0 +1,468 @@ +#include "dt8.hpp" + +#include "dali_define.hpp" + +#include +#include + +DaliDT8::DaliDT8(DaliBase& base) : base_(base) {} + +bool DaliDT8::enableDT8() { return base_.dtSelect(8); } + +std::optional 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 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 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(std::floor(1000000.0 / static_cast(v))); + return setColTempRaw(addr, mirek); +} + +std::optional 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(std::floor(1000000.0 / static_cast(mirek.value()))); + return kelvin; +} + +std::optional DaliDT8::getMinColorTemperature(int a) { + const auto mirek = getColTempRaw(a, 0); + if (!mirek.has_value() || mirek.value() == 0) return std::nullopt; + return static_cast(std::floor(1000000.0 / static_cast(mirek.value()))); +} + +std::optional DaliDT8::getMaxColorTemperature(int a) { + const auto mirek = getColTempRaw(a, 1); + if (!mirek.has_value() || mirek.value() == 0) return std::nullopt; + return static_cast(std::floor(1000000.0 / static_cast(mirek.value()))); +} + +std::optional DaliDT8::getPhysicalMinColorTemperature(int a) { + const auto mirek = getColTempRaw(a, 3); + if (!mirek.has_value() || mirek.value() == 0) return std::nullopt; + return static_cast(std::floor(1000000.0 / static_cast(mirek.value()))); +} + +std::optional DaliDT8::getPhysicalMaxColorTemperature(int a) { + const auto mirek = getColTempRaw(a, 4); + if (!mirek.has_value() || mirek.value() == 0) return std::nullopt; + return static_cast(std::floor(1000000.0 / static_cast(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(std::round(xClamped * 65535.0)); + const int y1 = static_cast(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(std::floor(1000000.0 / static_cast(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(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(std::round(xClamped * 65535.0)); + const int y1 = static_cast(std::round(yClamped * 65535.0)); + const int addr = a * 2 + 1; + return setColourRaw(addr, x1, y1); +} + +std::optional DaliDT8::getColourRaw(int a, int type) { + const uint8_t code = static_cast(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 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(R) / 255.0, static_cast(G) / 255.0, + static_cast(B) / 255.0); + return setColour(addr, xy[0], xy[1]); +} + +std::vector 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 DaliDT8::getPrimaryDimLevel(int a, int n) { + if (n < 0 || n > 5) return std::nullopt; + return getColourRaw(a, 3 + n); +} + +std::optional DaliDT8::getRedDimLevel(int a) { return getColourRaw(a, 9); } +std::optional DaliDT8::getGreenDimLevel(int a) { return getColourRaw(a, 10); } +std::optional DaliDT8::getBlueDimLevel(int a) { return getColourRaw(a, 11); } +std::optional DaliDT8::getWhiteDimLevel(int a) { return getColourRaw(a, 12); } +std::optional DaliDT8::getAmberDimLevel(int a) { return getColourRaw(a, 13); } +std::optional DaliDT8::getFreecolourDimLevel(int a) { return getColourRaw(a, 14); } +std::optional DaliDT8::getRGBWAFControl(int a) { return getColourRaw(a, 15); } + +std::optional DaliDT8::getTemporaryXRaw(int a) { return getColourRaw(a, 192); } +std::optional DaliDT8::getTemporaryYRaw(int a) { return getColourRaw(a, 193); } +std::optional DaliDT8::getTemporaryColourTemperatureRaw(int a) { return getColourRaw(a, 194); } + +std::optional DaliDT8::getTemporaryPrimaryDimLevel(int a, int n) { + if (n < 0 || n > 5) return std::nullopt; + return getColourRaw(a, 195 + n); +} + +std::optional DaliDT8::getTemporaryRedDimLevel(int a) { return getColourRaw(a, 201); } +std::optional DaliDT8::getTemporaryGreenDimLevel(int a) { return getColourRaw(a, 202); } +std::optional DaliDT8::getTemporaryBlueDimLevel(int a) { return getColourRaw(a, 203); } +std::optional DaliDT8::getTemporaryWhiteDimLevel(int a) { return getColourRaw(a, 204); } +std::optional DaliDT8::getTemporaryAmberDimLevel(int a) { return getColourRaw(a, 205); } +std::optional DaliDT8::getTemporaryFreecolourDimLevel(int a) { return getColourRaw(a, 206); } +std::optional DaliDT8::getTemporaryRGBWAFControl(int a) { return getColourRaw(a, 207); } +std::optional DaliDT8::getTemporaryColourType(int a) { return getColourRaw(a, 208); } + +std::vector 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 DaliDT8::getTemporaryColorTemperature(int a) { + const auto mirek = getTemporaryColourTemperatureRaw(a); + if (!mirek.has_value() || mirek.value() == 0) return std::nullopt; + return static_cast(std::floor(1000000.0 / static_cast(mirek.value()))); +} + +std::optional DaliDT8::getReportXRaw(int a) { return getColourRaw(a, 224); } +std::optional DaliDT8::getReportYRaw(int a) { return getColourRaw(a, 225); } +std::optional DaliDT8::getReportColourTemperatureRaw(int a) { return getColourRaw(a, 226); } + +std::optional DaliDT8::getReportPrimaryDimLevel(int a, int n) { + if (n < 0 || n > 5) return std::nullopt; + return getColourRaw(a, 227 + n); +} + +std::optional DaliDT8::getReportRedDimLevel(int a) { return getColourRaw(a, 233); } +std::optional DaliDT8::getReportGreenDimLevel(int a) { return getColourRaw(a, 234); } +std::optional DaliDT8::getReportBlueDimLevel(int a) { return getColourRaw(a, 235); } +std::optional DaliDT8::getReportWhiteDimLevel(int a) { return getColourRaw(a, 236); } +std::optional DaliDT8::getReportAmberDimLevel(int a) { return getColourRaw(a, 237); } +std::optional DaliDT8::getReportFreecolourDimLevel(int a) { return getColourRaw(a, 238); } +std::optional DaliDT8::getReportRGBWAFControl(int a) { return getColourRaw(a, 239); } +std::optional DaliDT8::getReportColourType(int a) { return getColourRaw(a, 240); } + +std::vector 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 DaliDT8::getReportColorTemperature(int a) { + const auto mirek = getReportColourTemperatureRaw(a); + if (!mirek.has_value() || mirek.value() == 0) return std::nullopt; + return static_cast(std::floor(1000000.0 / static_cast(mirek.value()))); +} + +std::optional DaliDT8::getNumberOfPrimaries(int a) { return getColourRaw(a, 82); } + +std::optional DaliDT8::getPrimaryXRaw(int a, int n) { + if (n < 0 || n > 5) return std::nullopt; + return getColourRaw(a, 64 + 3 * n); +} + +std::optional DaliDT8::getPrimaryYRaw(int a, int n) { + if (n < 0 || n > 5) return std::nullopt; + return getColourRaw(a, 65 + 3 * n); +} + +std::optional DaliDT8::getPrimaryTy(int a, int n) { + if (n < 0 || n > 5) return std::nullopt; + return getColourRaw(a, 66 + 3 * n); +} + +std::vector 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(std::floor(1000000.0 / static_cast(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 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 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 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 DaliDT8::getExtendedVersion(int a) { + const int addr = a * 2 + 1; + base_.dtSelect(8); + return base_.queryCmd(addr, DALI_CMD_DT8_QUERY_EXTENDED_VERSION); +} diff --git a/src/sequence.cpp b/src/sequence.cpp new file mode 100644 index 0000000..fbad5bf --- /dev/null +++ b/src/sequence.cpp @@ -0,0 +1,125 @@ +#include "sequence.hpp" + +#include + +std::string toString(DaliCommandType type) { + static const std::map 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 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; +} diff --git a/src/sequence_store.cpp b/src/sequence_store.cpp new file mode 100644 index 0000000..816b8e6 --- /dev/null +++ b/src/sequence_store.cpp @@ -0,0 +1,62 @@ +#include "sequence_store.hpp" + +#include + +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; + } + } +}