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.
This commit is contained in:
Tony
2026-04-25 12:59:16 +08:00
parent bbcfcd11f6
commit 32e7329b3e
7 changed files with 225 additions and 14 deletions
+1 -1
View File
@@ -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.
+15
View File
@@ -23,6 +23,14 @@ struct DaliStatus {
static DaliStatus fromByte(uint8_t status);
};
struct DaliDeviceTypeDiscovery {
std::optional<int> rawQueryType;
std::vector<int> types;
std::optional<int> primaryType() const;
std::vector<int> extraTypes() const;
};
class DaliBase {
public:
explicit DaliBase(DaliComm& comm);
@@ -85,6 +93,8 @@ class DaliBase {
std::optional<bool> getOnlineStatus(int a);
std::optional<int> getBright(int a);
std::optional<int> getDeviceType(int a);
std::optional<DaliDeviceTypeDiscovery> discoverDeviceTypes(
int a, const std::vector<int>& fallbackTypes = {}, int maxNextTypes = 16);
std::optional<int> getPhysicalMinLevel(int a);
std::optional<int> getDeviceVersion(int a);
@@ -161,6 +171,11 @@ class DaliBase {
private:
DaliComm& comm_;
std::vector<int> discoverNextDeviceTypes(int a, int maxNextTypes);
std::vector<int> probeSupportedDeviceTypes(int a, const std::vector<int>& fallbackTypes);
std::optional<int> queryExtendedVersionForDeviceType(int a, int deviceType);
static bool isRealDeviceTypeValue(int value);
static uint8_t encodeCmdAddr(int dec_addr);
static uint8_t encodeArcAddr(int dec_addr);
};
+4 -1
View File
@@ -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<uint8_t> 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<uint8_t>& frame) const;
void sleepMs(uint32_t ms) const;
};
+1
View File
@@ -116,4 +116,5 @@ struct DaliDevice {
std::string displayName() const;
void merge(const DaliDevice& other);
void setDetectedDeviceTypes(const std::optional<int>& rawQueryType, const std::vector<int>& types);
};
+96
View File
@@ -4,6 +4,19 @@
#include <algorithm>
namespace {
constexpr int kDeviceTypeNone = 0xFE;
constexpr int kDeviceTypeMultiple = 0xFF;
constexpr uint8_t kQueryExtendedVersion = 0xFF;
const std::vector<int>& defaultDeviceTypeFallbackProbeOrder() {
static const std::vector<int> 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<int> DaliDeviceTypeDiscovery::primaryType() const {
if (types.empty()) return std::nullopt;
return types.front();
}
std::vector<int> DaliDeviceTypeDiscovery::extraTypes() const {
if (types.size() <= 1) return {};
return std::vector<int>(types.begin() + 1, types.end());
}
DaliBase::DaliBase(DaliComm& comm) : comm_(comm) {}
int64_t DaliBase::mcuTicks() const {
@@ -154,6 +177,40 @@ std::optional<int> DaliBase::getBright(int a) {
std::optional<int> DaliBase::getDeviceType(int a) { return queryCmd(encodeCmdAddr(a), DALI_CMD_QUERY_DEVICE_TYPE); }
std::optional<DaliDeviceTypeDiscovery> DaliBase::discoverDeviceTypes(
int a, const std::vector<int>& 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<int> DaliBase::getPhysicalMinLevel(int a) {
return queryCmd(encodeCmdAddr(a), DALI_CMD_QUERY_PHYSICAL_MINIMUM_LEVEL);
}
@@ -245,6 +302,45 @@ std::optional<int> DaliBase::getFadeRate(int a) {
std::optional<int> DaliBase::getNextDeviceType(int a) { return query(a, DALI_CMD_QUERY_NEXT_DEVICE_TYPE); }
std::vector<int> DaliBase::discoverNextDeviceTypes(int a, int maxNextTypes) {
std::vector<int> 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<int> DaliBase::probeSupportedDeviceTypes(int a, const std::vector<int>& fallbackTypes) {
std::vector<int> 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<int> 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<int> DaliBase::getGroupH(int a) { return query(a, DALI_CMD_QUERY_GROUP_8_15); }
std::optional<int> DaliBase::getGroupL(int a) { return query(a, DALI_CMD_QUERY_GROUPS_0_7); }
+78 -12
View File
@@ -1,5 +1,7 @@
#include "dali_comm.hpp"
#include "dali_define.hpp"
#include <chrono>
#include <thread>
@@ -8,6 +10,61 @@
#include "freertos/task.h"
#endif
namespace {
constexpr uint32_t kQueryTimeoutMs = 100;
std::vector<uint8_t> 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<uint8_t> collected;
while (std::chrono::steady_clock::now() < deadline && collected.size() < 2) {
const auto remaining =
std::chrono::duration_cast<std::chrono::milliseconds>(deadline - std::chrono::steady_clock::now())
.count();
if (remaining <= 0) break;
const auto chunk = read_cb(2 - collected.size(), static_cast<uint32_t>(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<std::ptrdiff_t>(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<uint8_t> parseQueryResponse(const std::vector<uint8_t>& resp, uint8_t addr) {
if (resp.empty()) return std::nullopt;
// Gateway type 1 returns: 0xFF <data> 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<uint8_t>(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<uint8_t> newProbe{0x28, 0x01, static_cast<uint8_t>(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<uint8_t>& 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<uint8_t> DaliComm::queryCmd(uint8_t addr, uint8_t cmd) const {
if (!transact_) return std::nullopt;
const std::vector<uint8_t> frame{0x12, addr, cmd};
const auto resp = transact_(frame.data(), frame.size());
if (resp.empty()) return std::nullopt;
if (send_ && read_) {
prepareForQuery();
if (!writeFrame(frame)) return std::nullopt;
return parseQueryResponse(readQueryResponse(read_, kQueryTimeoutMs), addr);
}
// Gateway type 1 returns: 0xFF <data> on success; 0xFE no response; 0xFD invalid frame.
if (resp.size() == 1) {
if (resp[0] == 0xFE || resp[0] == 0xFD) return std::nullopt;
return resp[0];
}
if (resp[0] == 0xFF && resp.size() >= 2) {
return resp[1];
}
return resp.back();
prepareForQuery();
if (!transact_) return std::nullopt;
return parseQueryResponse(transact_(frame.data(), frame.size()), addr);
}
+30
View File
@@ -1,5 +1,6 @@
#include "device.hpp"
#include <algorithm>
#include <utility>
namespace {
@@ -359,3 +360,32 @@ void DaliDevice::merge(const DaliDevice& other) {
metadata[kv.first] = kv.second;
}
}
void DaliDevice::setDetectedDeviceTypes(
const std::optional<int>& rawQueryType, const std::vector<int>& types) {
deviceType = types.empty() ? std::nullopt : std::optional<int>(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);
}