From 32e7329b3e8091e86b8c925c116a62611c939a60 Mon Sep 17 00:00:00 2001 From: Tony Date: Sat, 25 Apr 2026 12:59:16 +0800 Subject: [PATCH] Enhance DALI device type discovery and query handling - Introduce DaliDeviceTypeDiscovery struct for managing device type queries. - Implement discoverDeviceTypes method in DaliBase for improved device type detection. - Update DaliComm to handle query responses more robustly, including handling multiple device replies. - Add setDetectedDeviceTypes method in DaliDevice to manage detected device types and capabilities. --- README.md | 2 +- include/base.hpp | 15 +++++++ include/dali_comm.hpp | 5 ++- include/device.hpp | 1 + src/base.cpp | 96 +++++++++++++++++++++++++++++++++++++++++++ src/dali_comm.cpp | 90 ++++++++++++++++++++++++++++++++++------ src/device.cpp | 30 ++++++++++++++ 7 files changed, 225 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 244a55b..4827ba8 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ Query-style operations return `data` when available and may include decoded flag ## Notes -- Optional query support: provide a `transact` callback that returns the gateway reply; otherwise, query methods return `std::nullopt`. +- Query support works with either a `read` callback or a `transact` callback. When `read` is available, queries return as soon as the reply bytes arrive instead of waiting behind a fixed pre-read delay. - `Dali` facade in `include/dali.hpp` mirrors `lib/dali/dali.dart` and wires `base`, `decode`, `dt1`, `dt8`, and `addr` together. - The `t`, `d`, and `g` parameters in Dart are not required here; timing/gateway selection is driven by your callbacks. - `DaliCloudBridge` now uses the shared bridge engine internally, so MQTT downlinks can target either raw DALI frames or registered bridge models. diff --git a/include/base.hpp b/include/base.hpp index bbf9349..651c1b3 100644 --- a/include/base.hpp +++ b/include/base.hpp @@ -23,6 +23,14 @@ struct DaliStatus { static DaliStatus fromByte(uint8_t status); }; +struct DaliDeviceTypeDiscovery { + std::optional rawQueryType; + std::vector types; + + std::optional primaryType() const; + std::vector extraTypes() const; +}; + class DaliBase { public: explicit DaliBase(DaliComm& comm); @@ -85,6 +93,8 @@ class DaliBase { std::optional getOnlineStatus(int a); std::optional getBright(int a); std::optional getDeviceType(int a); + std::optional discoverDeviceTypes( + int a, const std::vector& fallbackTypes = {}, int maxNextTypes = 16); std::optional getPhysicalMinLevel(int a); std::optional getDeviceVersion(int a); @@ -161,6 +171,11 @@ class DaliBase { private: DaliComm& comm_; + std::vector discoverNextDeviceTypes(int a, int maxNextTypes); + std::vector probeSupportedDeviceTypes(int a, const std::vector& fallbackTypes); + std::optional queryExtendedVersionForDeviceType(int a, int deviceType); + static bool isRealDeviceTypeValue(int value); + static uint8_t encodeCmdAddr(int dec_addr); static uint8_t encodeArcAddr(int dec_addr); }; diff --git a/include/dali_comm.hpp b/include/dali_comm.hpp index 196982e..8b7c256 100644 --- a/include/dali_comm.hpp +++ b/include/dali_comm.hpp @@ -11,6 +11,7 @@ // - Send: [0x10, addr, cmd] // - Ext: [0x11, addr, cmd] (sent twice with a small delay) // - Query: [0x12, addr, cmd] -> expects [0xFF, data] on success +// COMPARE may also return [0xFD, data] when multiple devices reply together // Callers provide UART callbacks; this class only builds frames and parses basic responses. class DaliComm { public: @@ -50,7 +51,8 @@ class DaliComm { 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. + // Send query frame (0x12) and parse single-byte response. Returns nullopt on no/invalid response, + // except COMPARE where 0xFD collisions are treated as a positive match. std::optional queryCmd(uint8_t addr, uint8_t cmd) const; // Helpers to mirror Dart address conversion (DEC short address -> DALI odd/even encoded). @@ -63,6 +65,7 @@ class DaliComm { TransactCallback transact_; DelayCallback delay_; + void prepareForQuery() const; bool writeFrame(const std::vector& frame) const; void sleepMs(uint32_t ms) const; }; diff --git a/include/device.hpp b/include/device.hpp index 449d19f..37a97c9 100644 --- a/include/device.hpp +++ b/include/device.hpp @@ -116,4 +116,5 @@ struct DaliDevice { std::string displayName() const; void merge(const DaliDevice& other); + void setDetectedDeviceTypes(const std::optional& rawQueryType, const std::vector& types); }; diff --git a/src/base.cpp b/src/base.cpp index 0848304..fb25091 100644 --- a/src/base.cpp +++ b/src/base.cpp @@ -4,6 +4,19 @@ #include +namespace { + +constexpr int kDeviceTypeNone = 0xFE; +constexpr int kDeviceTypeMultiple = 0xFF; +constexpr uint8_t kQueryExtendedVersion = 0xFF; + +const std::vector& defaultDeviceTypeFallbackProbeOrder() { + static const std::vector types = {1, 4, 5, 6, 8}; + return types; +} + +} // namespace + DaliStatus DaliStatus::fromByte(uint8_t status) { DaliStatus s; s.controlGearPresent = (status & 0x01) != 0; @@ -17,6 +30,16 @@ DaliStatus DaliStatus::fromByte(uint8_t status) { return s; } +std::optional DaliDeviceTypeDiscovery::primaryType() const { + if (types.empty()) return std::nullopt; + return types.front(); +} + +std::vector DaliDeviceTypeDiscovery::extraTypes() const { + if (types.size() <= 1) return {}; + return std::vector(types.begin() + 1, types.end()); +} + DaliBase::DaliBase(DaliComm& comm) : comm_(comm) {} int64_t DaliBase::mcuTicks() const { @@ -154,6 +177,40 @@ std::optional DaliBase::getBright(int a) { std::optional DaliBase::getDeviceType(int a) { return queryCmd(encodeCmdAddr(a), DALI_CMD_QUERY_DEVICE_TYPE); } +std::optional DaliBase::discoverDeviceTypes( + int a, const std::vector& fallbackTypes, int maxNextTypes) { + const auto rawQueryType = getDeviceType(a); + if (!rawQueryType.has_value()) return std::nullopt; + + DaliDeviceTypeDiscovery discovery; + discovery.rawQueryType = rawQueryType; + + const auto addType = [&discovery](int value) { + if (!isRealDeviceTypeValue(value)) return; + if (std::find(discovery.types.begin(), discovery.types.end(), value) == discovery.types.end()) { + discovery.types.push_back(value); + } + }; + + addType(rawQueryType.value()); + + if (rawQueryType.value() == kDeviceTypeMultiple) { + const auto enumerated = discoverNextDeviceTypes(a, maxNextTypes); + for (const int type : enumerated) { + addType(type); + } + if (discovery.types.empty()) { + const auto& probeTypes = fallbackTypes.empty() ? defaultDeviceTypeFallbackProbeOrder() : fallbackTypes; + const auto probed = probeSupportedDeviceTypes(a, probeTypes); + for (const int type : probed) { + addType(type); + } + } + } + + return discovery; +} + std::optional DaliBase::getPhysicalMinLevel(int a) { return queryCmd(encodeCmdAddr(a), DALI_CMD_QUERY_PHYSICAL_MINIMUM_LEVEL); } @@ -245,6 +302,45 @@ std::optional DaliBase::getFadeRate(int a) { std::optional DaliBase::getNextDeviceType(int a) { return query(a, DALI_CMD_QUERY_NEXT_DEVICE_TYPE); } +std::vector DaliBase::discoverNextDeviceTypes(int a, int maxNextTypes) { + std::vector types; + types.reserve(std::max(maxNextTypes, 0)); + + for (int i = 0; i < maxNextTypes; i++) { + const auto next = getNextDeviceType(a); + if (!next.has_value() || !isRealDeviceTypeValue(next.value())) { + break; + } + if (std::find(types.begin(), types.end(), next.value()) != types.end()) { + break; + } + types.push_back(next.value()); + } + + return types; +} + +std::vector DaliBase::probeSupportedDeviceTypes(int a, const std::vector& fallbackTypes) { + std::vector types; + + for (const int type : fallbackTypes) { + if (!isRealDeviceTypeValue(type)) continue; + if (std::find(types.begin(), types.end(), type) != types.end()) continue; + if (queryExtendedVersionForDeviceType(a, type).has_value()) { + types.push_back(type); + } + } + + return types; +} + +std::optional DaliBase::queryExtendedVersionForDeviceType(int a, int deviceType) { + if (!dtSelect(deviceType)) return std::nullopt; + return queryCmd(encodeCmdAddr(a), kQueryExtendedVersion); +} + +bool DaliBase::isRealDeviceTypeValue(int value) { return value >= 0 && value < kDeviceTypeNone; } + 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); } diff --git a/src/dali_comm.cpp b/src/dali_comm.cpp index fb5901a..a703fad 100644 --- a/src/dali_comm.cpp +++ b/src/dali_comm.cpp @@ -1,5 +1,7 @@ #include "dali_comm.hpp" +#include "dali_define.hpp" + #include #include @@ -8,6 +10,61 @@ #include "freertos/task.h" #endif +namespace { + +constexpr uint32_t kQueryTimeoutMs = 100; + +std::vector readQueryResponse(const DaliComm::ReadCallback& read_cb, uint32_t timeout_ms) { + if (!read_cb) return {}; + + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms); + std::vector collected; + while (std::chrono::steady_clock::now() < deadline && collected.size() < 2) { + const auto remaining = + std::chrono::duration_cast(deadline - std::chrono::steady_clock::now()) + .count(); + if (remaining <= 0) break; + + const auto chunk = read_cb(2 - collected.size(), static_cast(remaining)); + if (chunk.empty()) break; + + const size_t needed = 2 - collected.size(); + const size_t take = std::min(needed, chunk.size()); + collected.insert(collected.end(), chunk.begin(), chunk.begin() + static_cast(take)); + + // Non-prefixed one-byte replies are complete as soon as they arrive. + if (collected.size() == 1 && collected[0] != 0xFF && collected[0] != 0xFE && collected[0] != 0xFD) { + break; + } + } + return collected; +} + +std::optional parseQueryResponse(const std::vector& resp, uint8_t addr) { + if (resp.empty()) return std::nullopt; + + // Gateway type 1 returns: 0xFF on success; 0xFE 0x00 on no response; + // 0xFD xx on invalid frame. COMPARE is special: collisions still mean a match. + if (resp[0] == 0xFE) { + return std::nullopt; + } + if (resp[0] == 0xFD) { + if (addr == DALI_CMD_SPECIAL_COMPARE) { + return static_cast(resp.size() >= 2 ? resp[1] : 0xFF); + } + return std::nullopt; + } + if (resp.size() == 1) { + return resp[0]; + } + if (resp[0] == 0xFF && resp.size() >= 2) { + return resp[1]; + } + return resp.back(); +} + +} // namespace + DaliComm::DaliComm(SendCallback send_cb, ReadCallback read_cb, TransactCallback transact_cb, @@ -51,12 +108,15 @@ int DaliComm::checkGatewayType(int gateway) const { const std::vector newProbe{0x28, 0x01, static_cast(gateway), 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF}; + prepareForQuery(); const auto usbResp = transact_(usbProbe.data(), usbProbe.size()); if (!usbResp.empty() && usbResp[0] > 0) return 1; + prepareForQuery(); const auto legacyResp = transact_(legacyProbe.data(), legacyProbe.size()); if (legacyResp.size() >= 2 && legacyResp[0] == gateway) return 2; + prepareForQuery(); const auto newResp = transact_(newProbe.data(), newProbe.size()); if (newResp.size() >= 2 && newResp[0] == gateway) return 3; @@ -114,6 +174,16 @@ bool DaliComm::writeFrame(const std::vector& frame) const { return send_(frame.data(), frame.size()); } +void DaliComm::prepareForQuery() const { + if (!read_) return; + for (int i = 0; i < 8; ++i) { + const auto data = read_(64, 1); + if (data.empty()) { + break; + } + } +} + void DaliComm::sleepMs(uint32_t ms) const { if (delay_) { delay_(ms); @@ -139,18 +209,14 @@ bool DaliComm::sendExtCmd(uint8_t addr, uint8_t cmd) const { } 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; + if (send_ && read_) { + prepareForQuery(); + if (!writeFrame(frame)) return std::nullopt; + return parseQueryResponse(readQueryResponse(read_, kQueryTimeoutMs), addr); + } - // 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(); + prepareForQuery(); + if (!transact_) return std::nullopt; + return parseQueryResponse(transact_(frame.data(), frame.size()), addr); } diff --git a/src/device.cpp b/src/device.cpp index 40e7338..1f93677 100644 --- a/src/device.cpp +++ b/src/device.cpp @@ -1,5 +1,6 @@ #include "device.hpp" +#include #include namespace { @@ -359,3 +360,32 @@ void DaliDevice::merge(const DaliDevice& other) { metadata[kv.first] = kv.second; } } + +void DaliDevice::setDetectedDeviceTypes( + const std::optional& rawQueryType, const std::vector& types) { + deviceType = types.empty() ? std::nullopt : std::optional(types.front()); + + extType.clear(); + if (types.size() > 1) { + extType.assign(types.begin() + 1, types.end()); + } + + capabilities.supportsDt1 = std::find(types.begin(), types.end(), 1) != types.end(); + capabilities.supportsDt4 = std::find(types.begin(), types.end(), 4) != types.end(); + capabilities.supportsDt5 = std::find(types.begin(), types.end(), 5) != types.end(); + capabilities.supportsDt6 = std::find(types.begin(), types.end(), 6) != types.end(); + capabilities.supportsDt8 = std::find(types.begin(), types.end(), 8) != types.end(); + + if (rawQueryType.has_value()) { + metadata["deviceTypeQueryRaw"] = rawQueryType.value(); + } else { + metadata.erase("deviceTypeQueryRaw"); + } + + DaliValue::Array detectedTypes; + detectedTypes.reserve(types.size()); + for (const int type : types) { + detectedTypes.emplace_back(type); + } + metadata["deviceTypes"] = std::move(detectedTypes); +}