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:
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user