feat: enhance gateway channel management with serial command handling and gateway ID updates

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-06-11 21:04:20 +08:00
parent 6ffca719d9
commit dceede8602
8 changed files with 298 additions and 14 deletions
+21 -4
View File
@@ -48,9 +48,26 @@ commands, address allocation, live management reads, bridge traffic, and any raw
DALI bus activity from another master until the bus has been idle. Group and
broadcast targets are never queried for refresh.
Gateway feature opcode `0x06` advertises cache support with bit `0x40`. Gateway
opcode `0x39` returns cache summary and target snapshots so frontend clients can
read cached state without issuing live DALI queries on supported gateways.
Gateway feature opcode `0x06` keeps the Lua-compatible low-byte feature bits,
advertises cache support with bit `0x40`, and advertises the native C++ gateway
type with bit `0x0100`. Gateway opcode `0x39` returns cache summary and target
snapshots so frontend clients can read cached state without issuing live DALI
queries on supported gateways.
Gateway opcode `0x09` with address/data `0x00/0x00` is a chip-level channel-id
report. It returns the enabled DALI channel ids so clients do not need to probe
every possible gateway id one by one. For native C++ gateways, channel number is
the fixed 1-based Kconfig slot (`1` to `16`) and channel id is the persisted,
Lua-compatible gateway id used by normal `0x28 0x01 ...` command frames.
Gateway opcode `0x0B` is the serial-scoped channel command. The serial is the
last three bytes of the ESP base MAC. Operation `0x00` reports serial plus
`(channel number, channel id)` pairs. Operations `0x01` and `0x02` get and set
the channel id for the fixed channel number in the command frame gateway byte.
Operation `0x03` wraps an existing gateway command and dispatches it by fixed
channel number after the serial matches, so BLE/Wi-Fi configuration and DALI
send/query commands can target a channel even when its variable channel id is
unknown.
## Current status
@@ -113,4 +130,4 @@ BACnet/IP is owned by `gateway/components/gateway_bacnet` and is started through
Provisioned BACnet models still use generic `BridgeModel` fields such as object type, object instance, property, and optional `bitIndex`. Query-style models refresh BACnet `Present_Value` from live DALI reads, and binary models with `bitIndex` expose a single packed status bit.
For discovered DALI short addresses, the gateway also mirrors the generated Modbus discrete diagnostics as BACnet binary-input objects. Object instances are allocated in a gateway-owned generated range using the channel index plus the generated Modbus discrete-input offset, so generated objects stay deterministic while avoiding the provisioned-object address space in normal deployments.
For discovered DALI short addresses, the gateway also mirrors the generated Modbus discrete diagnostics as BACnet binary-input objects. Object instances are allocated in a gateway-owned generated range using the channel index plus the generated Modbus discrete-input offset, so generated objects stay deterministic while avoiding the provisioned-object address space in normal deployments.
+12 -4
View File
@@ -893,14 +893,22 @@ bool ValidateChannelBindings() {
esp_err_t BindConfiguredChannels(gateway::DaliDomainService& dali_domain,
const gateway::GatewayRuntime& runtime) {
std::array<bool, 256> used_gateway_ids{};
for (const auto& channel : BuildChannelBindings()) {
if (!channel.enabled) {
continue;
}
const uint8_t gateway_id =
runtime.gatewayIdForChannel(channel.channel_index, channel.gateway_id);
if (used_gateway_ids[gateway_id]) {
ESP_LOGE(kTag, "duplicate runtime gateway id configured: %u", gateway_id);
return ESP_ERR_INVALID_STATE;
}
used_gateway_ids[gateway_id] = true;
if (channel.native_phy) {
gateway::DaliHardwareBusConfig config{};
config.channel_index = channel.channel_index;
config.gateway_id = channel.gateway_id;
config.gateway_id = gateway_id;
config.bus_id = channel.native_bus_id;
config.tx_pin = static_cast<uint8_t>(channel.native_tx_pin);
config.rx_pin = static_cast<uint8_t>(channel.native_rx_pin);
@@ -914,7 +922,7 @@ esp_err_t BindConfiguredChannels(gateway::DaliDomainService& dali_domain,
} else if (channel.serial_phy) {
gateway::DaliSerialBusConfig config{};
config.channel_index = channel.channel_index;
config.gateway_id = channel.gateway_id;
config.gateway_id = gateway_id;
config.uart_port = channel.uart_port;
config.tx_pin = channel.serial_tx_pin;
config.rx_pin = channel.serial_rx_pin;
@@ -966,8 +974,8 @@ extern "C" void app_main(void) {
},
s_dali_domain.get());
ESP_ERROR_CHECK(s_runtime->start());
s_runtime->setGatewayCount(CONFIG_GATEWAY_CHANNEL_COUNT);
ESP_ERROR_CHECK(BindConfiguredChannels(*s_dali_domain, *s_runtime));
s_runtime->setGatewayCount(s_dali_domain->channelCount());
gateway::GatewayCacheConfig cache_config;
cache_config.cache_enabled = kCacheSupported && kCacheStartupEnabled && s_runtime->cacheEnabled();
@@ -1244,4 +1252,4 @@ extern "C" void app_main(void) {
std::printf("gateway_main: runtime device type=%s serial=%s project=%s version=%s\n",
device_info.type.c_str(), device_info.serial_id.c_str(),
device_info.project.c_str(), device_info.version.c_str());
}
}
@@ -192,6 +192,8 @@ class DaliDomainService {
std::optional<uint8_t> level) const;
bool applyAddressSettings(uint8_t gateway_id, int short_address,
const DaliAddressSettingsSnapshot& settings) const;
std::optional<uint8_t> gatewayIdForChannelIndex(uint8_t channel_index) const;
bool updateChannelGatewayId(uint8_t channel_index, uint8_t gateway_id);
bool updateChannelName(uint8_t gateway_id, std::string_view name);
bool allocateAllAddr(uint8_t gateway_id, int start_address = 0) const;
void stopAllocAddr(uint8_t gateway_id) const;
@@ -212,6 +214,7 @@ class DaliDomainService {
DaliChannel* findChannelByGateway(uint8_t gateway_id);
const DaliChannel* findChannelByGateway(uint8_t gateway_id) const;
DaliChannel* findChannelByIndex(uint8_t channel_index);
const DaliChannel* findChannelByIndex(uint8_t channel_index) const;
const DaliChannel* findChannelByHardwareBus(uint8_t bus_id) const;
bool hasSerialPort(int uart_port) const;
esp_err_t startSerialRxTask(DaliChannel& channel);
@@ -635,6 +635,31 @@ std::vector<DaliChannelInfo> DaliDomainService::channelInfo() const {
return info;
}
std::optional<uint8_t> DaliDomainService::gatewayIdForChannelIndex(uint8_t channel_index) const {
const auto* channel = findChannelByIndex(channel_index);
if (channel == nullptr) {
return std::nullopt;
}
return channel->config.gateway_id;
}
bool DaliDomainService::updateChannelGatewayId(uint8_t channel_index, uint8_t gateway_id) {
auto* channel = findChannelByIndex(channel_index);
if (channel == nullptr) {
return false;
}
for (const auto& other : channels_) {
if (other.get() != channel && other->config.gateway_id == gateway_id) {
return false;
}
}
channel->config.gateway_id = gateway_id;
if (channel->dali != nullptr) {
channel->dali = std::make_unique<Dali>(*channel->comm, gateway_id, channel->config.name);
}
return true;
}
void DaliDomainService::addRawFrameSink(std::function<void(const DaliRawFrame& frame)> sink) {
if (!sink) {
return;
@@ -1674,6 +1699,15 @@ DaliDomainService::DaliChannel* DaliDomainService::findChannelByIndex(uint8_t ch
return it == channels_.end() ? nullptr : it->get();
}
const DaliDomainService::DaliChannel* DaliDomainService::findChannelByIndex(
uint8_t channel_index) const {
const auto it = std::find_if(channels_.begin(), channels_.end(),
[channel_index](const auto& channel) {
return channel->config.channel_index == channel_index;
});
return it == channels_.end() ? nullptr : it->get();
}
const DaliDomainService::DaliChannel* DaliDomainService::findChannelByHardwareBus(
uint8_t bus_id) const {
const auto it = std::find_if(channels_.begin(), channels_.end(), [bus_id](const auto& channel) {
@@ -139,6 +139,7 @@ class GatewayController {
bool hasGateway(uint8_t gateway_id) const;
std::vector<uint8_t> gatewayIds() const;
std::optional<uint8_t> gatewayIdForChannelNumber(uint8_t channel_number) const;
std::string gatewayName(uint8_t gateway_id) const;
void refreshRuntimeGatewayNames();
void publishPayload(uint8_t gateway_id, const std::vector<uint8_t>& payload);
@@ -181,6 +182,11 @@ class GatewayController {
bool executeGroup(uint8_t gateway_id, uint8_t group_id);
void handleGatewayNameCommand(uint8_t gateway_id, const std::vector<uint8_t>& command);
void handleGatewaySerialCommand(uint8_t channel_number, const std::vector<uint8_t>& command);
void publishGatewaySerialReport();
void publishGatewaySerialResponse(uint8_t status, uint8_t op,
const std::vector<uint8_t>& data);
bool gatewaySerialMatches(const std::vector<uint8_t>& command) const;
void handleGatewayIdentityCommand(uint8_t gateway_id, uint8_t op);
void handleAllocationCommand(uint8_t gateway_id, const std::vector<uint8_t>& command);
void handleInternalSceneCommand(uint8_t gateway_id, const std::vector<uint8_t>& command);
@@ -28,13 +28,23 @@ constexpr uint8_t kBridgeTransportRequestOpcode = 0xB0;
constexpr uint8_t kBridgeTransportResponseOpcode = 0xB1;
constexpr uint8_t kBridgeTransportVersion = 1;
constexpr size_t kBridgeTransportMaxChunkBytes = 120;
constexpr uint8_t kGatewaySerialOpcode = 0x0B;
constexpr uint8_t kGatewaySerialOpReport = 0x00;
constexpr uint8_t kGatewaySerialOpGetChannelId = 0x01;
constexpr uint8_t kGatewaySerialOpSetChannelId = 0x02;
constexpr uint8_t kGatewaySerialOpDispatchByChannelNumber = 0x03;
constexpr uint8_t kGatewaySerialStatusOk = 0x00;
constexpr uint8_t kGatewaySerialStatusInvalidArgument = 0x02;
constexpr uint8_t kGatewaySerialStatusDuplicateChannelId = 0x03;
constexpr uint8_t kGatewaySerialStatusStorageError = 0x04;
constexpr uint8_t kDali103SendOpcode = 0x60;
constexpr uint8_t kDali103SendTwiceOpcode = 0x61;
constexpr uint8_t kDali103QueryOpcode = 0x62;
constexpr uint8_t kDali103QueryResponseOpcode = 0x63;
constexpr uint8_t kDali103NoResponseOpcode = 0x64;
constexpr uint8_t kDali103RawFrameOpcode = 0x65;
constexpr uint8_t kGatewayFeatureCache = 0x40;
constexpr uint16_t kGatewayFeatureCache = 0x0040;
constexpr uint16_t kGatewayFeatureNativeCpp = 0x0100;
constexpr uint8_t kGatewayCacheOpcode = 0x39;
constexpr uint8_t kGatewayCacheProtocolVersion = 1;
constexpr uint8_t kGatewayCacheOpSummary = 0x00;
@@ -165,6 +175,13 @@ void AppendLe32(std::vector<uint8_t>& out, uint32_t value) {
out.push_back(static_cast<uint8_t>((value >> 24) & 0xFF));
}
void AppendFeatureBits(std::vector<uint8_t>& out, uint16_t feature) {
if (feature > 0xFF) {
out.push_back(static_cast<uint8_t>((feature >> 8) & 0xFF));
}
out.push_back(static_cast<uint8_t>(feature & 0xFF));
}
uint8_t CacheByte(std::optional<uint8_t> value) {
return value.value_or(0xFF);
}
@@ -759,6 +776,10 @@ void GatewayController::dispatchCommand(const std::vector<uint8_t>& command) {
publishPayload(gateway_id, payload);
return;
}
if (opcode == kGatewaySerialOpcode) {
handleGatewaySerialCommand(gateway_id, command);
return;
}
if (!hasGateway(gateway_id)) {
ESP_LOGW(kTag, "command for unknown gateway=%u opcode=0x%02x", gateway_id, opcode);
return;
@@ -825,7 +846,7 @@ void GatewayController::dispatchCommand(const std::vector<uint8_t>& command) {
handleGatewayNameCommand(gateway_id, command);
break;
case 0x06: {
uint8_t feature = 0;
uint16_t feature = kGatewayFeatureNativeCpp;
if (setup_mode_ && config_.setup_supported) {
feature |= 0x01;
}
@@ -844,7 +865,9 @@ void GatewayController::dispatchCommand(const std::vector<uint8_t>& command) {
if (config_.cache_supported) {
feature |= kGatewayFeatureCache;
}
publishPayload(gateway_id, {0x03, gateway_id, feature});
std::vector<uint8_t> payload{0x03, gateway_id};
AppendFeatureBits(payload, feature);
publishPayload(gateway_id, payload);
break;
}
case 0x07:
@@ -995,6 +1018,23 @@ std::vector<uint8_t> GatewayController::gatewayIds() const {
return ids;
}
std::optional<uint8_t> GatewayController::gatewayIdForChannelNumber(
uint8_t channel_number) const {
if (channel_number == 0) {
return std::nullopt;
}
const uint8_t channel_index = static_cast<uint8_t>(channel_number - 1);
const auto channels = dali_domain_.channelInfo();
const auto it =
std::find_if(channels.begin(), channels.end(), [channel_index](const auto& channel) {
return channel.channel_index == channel_index;
});
if (it == channels.end()) {
return std::nullopt;
}
return it->gateway_id;
}
std::string GatewayController::gatewayName(uint8_t gateway_id) const {
const auto channels = dali_domain_.channelInfo();
const auto it = std::find_if(channels.begin(), channels.end(), [gateway_id](const auto& channel) {
@@ -1618,6 +1658,117 @@ void GatewayController::handleGatewayNameCommand(uint8_t gateway_id,
}
}
void GatewayController::publishGatewaySerialResponse(uint8_t status, uint8_t op,
const std::vector<uint8_t>& data) {
std::vector<uint8_t> payload{kGatewaySerialOpcode, status};
const auto serial = runtime_.serialNumberBytes();
payload.insert(payload.end(), serial.begin(), serial.end());
payload.push_back(op);
payload.insert(payload.end(), data.begin(), data.end());
publishPayload(0, payload);
}
bool GatewayController::gatewaySerialMatches(const std::vector<uint8_t>& command) const {
if (command.size() < 9) {
return false;
}
const auto serial = runtime_.serialNumberBytes();
return command[5] == serial[0] && command[6] == serial[1] && command[7] == serial[2];
}
void GatewayController::publishGatewaySerialReport() {
const auto serial = runtime_.serialNumberBytes();
std::vector<uint8_t> payload{kGatewaySerialOpcode,
kGatewaySerialStatusOk,
serial[0],
serial[1],
serial[2],
kGatewaySerialOpReport};
const auto channels = dali_domain_.channelInfo();
const auto count = std::min<size_t>(channels.size(), 16);
payload.push_back(static_cast<uint8_t>(count));
for (size_t index = 0; index < count; ++index) {
payload.push_back(static_cast<uint8_t>(channels[index].channel_index + 1));
payload.push_back(channels[index].gateway_id);
}
publishPayload(0, payload);
}
void GatewayController::handleGatewaySerialCommand(uint8_t channel_number,
const std::vector<uint8_t>& command) {
const uint8_t op = command.size() > 4 ? command[4] : kGatewaySerialOpReport;
if (op == kGatewaySerialOpReport) {
if (command.size() >= 9 && !gatewaySerialMatches(command)) {
return;
}
publishGatewaySerialReport();
return;
}
if (!gatewaySerialMatches(command)) {
return;
}
const auto gateway_id = gatewayIdForChannelNumber(channel_number);
if (!gateway_id.has_value()) {
publishGatewaySerialResponse(kGatewaySerialStatusInvalidArgument, op, {channel_number});
return;
}
switch (op) {
case kGatewaySerialOpGetChannelId:
publishGatewaySerialResponse(kGatewaySerialStatusOk, op,
{channel_number, gateway_id.value()});
return;
case kGatewaySerialOpSetChannelId: {
if (command.size() < 10) {
publishGatewaySerialResponse(kGatewaySerialStatusInvalidArgument, op, {channel_number});
return;
}
const uint8_t new_gateway_id = command[8];
const uint8_t channel_index = static_cast<uint8_t>(channel_number - 1);
const uint8_t old_gateway_id = gateway_id.value();
if (!dali_domain_.updateChannelGatewayId(channel_index, new_gateway_id)) {
publishGatewaySerialResponse(kGatewaySerialStatusDuplicateChannelId, op,
{channel_number, new_gateway_id});
return;
}
if (!runtime_.setGatewayIdForChannel(channel_index, new_gateway_id)) {
dali_domain_.updateChannelGatewayId(channel_index, old_gateway_id);
publishGatewaySerialResponse(kGatewaySerialStatusStorageError, op,
{channel_number, new_gateway_id});
return;
}
cache_.preloadChannel(new_gateway_id);
reconciliation_jobs_.erase(old_gateway_id);
cache_refresh_jobs_.erase(old_gateway_id);
refreshRuntimeGatewayNames();
publishGatewaySerialResponse(kGatewaySerialStatusOk, op,
{channel_number, new_gateway_id});
return;
}
case kGatewaySerialOpDispatchByChannelNumber: {
if (command.size() < 12) {
publishGatewaySerialResponse(kGatewaySerialStatusInvalidArgument, op, {channel_number});
return;
}
const uint8_t inner_opcode = command[8];
if (inner_opcode == kGatewaySerialOpcode) {
publishGatewaySerialResponse(kGatewaySerialStatusInvalidArgument, op,
{channel_number, inner_opcode});
return;
}
std::vector<uint8_t> inner{0x28, 0x01, gateway_id.value(), inner_opcode};
inner.insert(inner.end(), command.begin() + 9, command.end() - 1);
dispatchCommand(GatewayRuntime::checksum(std::move(inner)));
return;
}
default:
publishGatewaySerialResponse(kGatewaySerialStatusInvalidArgument, op, {channel_number});
return;
}
}
void GatewayController::handleGatewayIdentityCommand(uint8_t gateway_id, uint8_t op) {
std::string value;
if (op == 0x00) {
@@ -76,11 +76,14 @@ class GatewaySettingsStore {
std::string getGatewayName(uint8_t gateway_id, std::string_view fallback) const;
bool setGatewayName(uint8_t gateway_id, std::string_view name);
uint8_t getChannelGatewayId(uint8_t channel_index, uint8_t fallback) const;
bool setChannelGatewayId(uint8_t channel_index, uint8_t gateway_id);
private:
std::optional<std::string> readString(std::string_view key) const;
bool writeString(std::string_view key, std::string_view value);
std::string makeGatewayNameKey(uint8_t gateway_id) const;
std::string makeChannelGatewayIdKey(uint8_t channel_index) const;
mutable nvs_handle_t handle_{0};
};
@@ -135,8 +138,11 @@ class GatewayRuntime {
bool setBleEnabled(bool enabled);
bool cacheEnabled() const;
bool setCacheEnabled(bool enabled);
uint8_t gatewayIdForChannel(uint8_t channel_index, uint8_t fallback) const;
bool setGatewayIdForChannel(uint8_t channel_index, uint8_t gateway_id);
std::string gatewayName(uint8_t gateway_id) const;
bool setGatewayName(uint8_t gateway_id, std::string_view name);
std::vector<uint8_t> serialNumberBytes() const;
std::string gatewaySerialHex(uint8_t gateway_id) const;
std::string bleMacHex() const;
std::string bleGatewayName(uint8_t gateway_id, std::string_view gateway_name) const;
@@ -162,6 +168,7 @@ class GatewayRuntime {
std::deque<std::vector<uint8_t>> normal_commands_;
std::deque<std::vector<uint8_t>> maintenance_commands_;
mutable std::map<uint8_t, std::string> gateway_names_;
mutable std::map<uint8_t, uint8_t> channel_gateway_ids_;
size_t gateway_count_{0};
bool ble_enabled_{false};
bool cache_enabled_{true};
@@ -172,4 +179,4 @@ class GatewayRuntime {
SemaphoreHandle_t command_lock_{nullptr};
};
} // namespace gateway
} // namespace gateway
@@ -57,7 +57,8 @@ esp_err_t InitializeRuntimeNvs() {
std::string ReadRuntimeSerialId() {
uint8_t mac[6] = {0};
if (esp_read_mac(mac, ESP_MAC_WIFI_STA) != ESP_OK) {
if (esp_read_mac(mac, ESP_MAC_BASE) != ESP_OK &&
esp_read_mac(mac, ESP_MAC_WIFI_STA) != ESP_OK) {
return "DALIGW";
}
@@ -188,6 +189,31 @@ bool GatewaySettingsStore::setGatewayName(uint8_t gateway_id,
return writeString(makeGatewayNameKey(gateway_id), name);
}
uint8_t GatewaySettingsStore::getChannelGatewayId(uint8_t channel_index,
uint8_t fallback) const {
if (handle_ == 0) {
return fallback;
}
uint8_t gateway_id = fallback;
if (nvs_get_u8(handle_, makeChannelGatewayIdKey(channel_index).c_str(), &gateway_id) !=
ESP_OK) {
return fallback;
}
return gateway_id;
}
bool GatewaySettingsStore::setChannelGatewayId(uint8_t channel_index,
uint8_t gateway_id) {
if (handle_ == 0) {
return false;
}
return nvs_set_u8(handle_, makeChannelGatewayIdKey(channel_index).c_str(), gateway_id) ==
ESP_OK &&
nvs_commit(handle_) == ESP_OK;
}
std::optional<std::string> GatewaySettingsStore::readString(std::string_view key) const {
if (handle_ == 0) {
return std::nullopt;
@@ -222,6 +248,12 @@ std::string GatewaySettingsStore::makeGatewayNameKey(uint8_t gateway_id) const {
return std::string(key);
}
std::string GatewaySettingsStore::makeChannelGatewayIdKey(uint8_t channel_index) const {
char key[24] = {0};
std::snprintf(key, sizeof(key), "dali_ch_id_%u", channel_index);
return std::string(key);
}
GatewayRuntime::GatewayRuntime(BootProfile profile, GatewayRuntimeConfig config,
DaliDomainService* dali_domain)
: profile_(profile),
@@ -323,7 +355,7 @@ GatewayRuntime::CommandPriority GatewayRuntime::classifyCommandPriority(
}
if (opcode == 0x00 || opcode == 0x01 || opcode == 0x03 || opcode == 0x04 || opcode == 0x07 ||
opcode == 0x08 || opcode == 0x10 || opcode == 0x11 || opcode == 0x12 || opcode == 0x13 ||
opcode == 0x17 || opcode == 0x18 || opcode == 0x37 || opcode == 0x38 ||
opcode == 0x0B || opcode == 0x17 || opcode == 0x18 || opcode == 0x37 || opcode == 0x38 ||
opcode == 0x60 || opcode == 0x61 || opcode == 0x62 || (opcode == 0x30 && addr == 0)) {
return CommandPriority::kControl;
}
@@ -531,6 +563,27 @@ bool GatewayRuntime::setCacheEnabled(bool enabled) {
return true;
}
uint8_t GatewayRuntime::gatewayIdForChannel(uint8_t channel_index, uint8_t fallback) const {
LockGuard guard(command_lock_);
const auto cached = channel_gateway_ids_.find(channel_index);
if (cached != channel_gateway_ids_.end()) {
return cached->second;
}
const uint8_t gateway_id = settings_.getChannelGatewayId(channel_index, fallback);
channel_gateway_ids_[channel_index] = gateway_id;
return gateway_id;
}
bool GatewayRuntime::setGatewayIdForChannel(uint8_t channel_index, uint8_t gateway_id) {
if (!settings_.setChannelGatewayId(channel_index, gateway_id)) {
return false;
}
LockGuard guard(command_lock_);
channel_gateway_ids_[channel_index] = gateway_id;
return true;
}
std::string GatewayRuntime::gatewayName(uint8_t gateway_id) const {
LockGuard guard(command_lock_);
const auto cached = gateway_names_.find(gateway_id);
@@ -577,6 +630,11 @@ std::string GatewayRuntime::gatewaySerialHex(uint8_t gateway_id) const {
return toHex(serial);
}
std::vector<uint8_t> GatewayRuntime::serialNumberBytes() const {
const auto bytes = serialBytes();
return {bytes[3], bytes[4], bytes[5]};
}
std::string GatewayRuntime::bleMacHex() const {
return toHex(serialBytes());
}