Files
gateway/components/gateway_controller/src/gateway_controller.cpp
T

1234 lines
43 KiB
C++

#include "gateway_controller.hpp"
#include "dali_domain.hpp"
#include "esp_log.h"
#include "esp_system.h"
#include "gateway_runtime.hpp"
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <utility>
namespace gateway {
namespace {
constexpr const char* kTag = "gateway_controller";
constexpr size_t kMaxNameBytes = 32;
constexpr uint8_t kDaliShortAddressCount = 64;
constexpr uint8_t kDaliSceneCount = 16;
constexpr uint8_t kDaliCmdOff = 0x00;
constexpr uint8_t kDaliCmdRecallMax = 0x05;
constexpr TickType_t kMaintenancePollTicks = pdMS_TO_TICKS(20);
class LockGuard {
public:
explicit LockGuard(SemaphoreHandle_t lock) : lock_(lock) {
if (lock_ != nullptr) {
xSemaphoreTake(lock_, portMAX_DELAY);
}
}
~LockGuard() {
if (lock_ != nullptr) {
xSemaphoreGive(lock_);
}
}
private:
SemaphoreHandle_t lock_;
};
bool AnyFlagSet(const GatewayCacheChannelFlags& flags) {
return flags.need_update_group || flags.need_update_scene || flags.need_update_settings;
}
std::string NormalizeName(std::string_view name) {
std::string normalized(name);
if (normalized.size() > kMaxNameBytes) {
normalized.resize(kMaxNameBytes);
}
return normalized;
}
std::string AppendGatewaySuffix(std::string_view name, uint8_t gateway_id) {
std::string normalized = NormalizeName(name);
const std::string suffix = "_" + std::to_string(gateway_id);
if (normalized.size() > suffix.size() &&
normalized.compare(normalized.size() - suffix.size(), suffix.size(), suffix) == 0) {
return normalized;
}
const size_t max_base_len = kMaxNameBytes > suffix.size() ? kMaxNameBytes - suffix.size() : 0;
if (normalized.size() > max_base_len) {
normalized.resize(max_base_len);
}
normalized += suffix;
return normalized;
}
void AppendStringBytes(std::vector<uint8_t>& out, std::string_view value) {
for (const auto ch : value) {
out.push_back(static_cast<uint8_t>(ch));
}
}
void AppendPaddedName(std::vector<uint8_t>& out, std::string_view name) {
const auto normalized = NormalizeName(name);
out.push_back(static_cast<uint8_t>(normalized.size()));
AppendStringBytes(out, normalized);
while (out.size() < 5 + kMaxNameBytes) {
out.push_back(0x00);
}
}
const char* PhyKindToString(DaliPhyKind phy_kind) {
switch (phy_kind) {
case DaliPhyKind::kNativeHardware:
return "native";
case DaliPhyKind::kSerialUart:
return "serial";
case DaliPhyKind::kCustom:
default:
return "custom";
}
}
} // namespace
GatewayController::GatewayController(GatewayRuntime& runtime, DaliDomainService& dali_domain,
GatewayCache& cache, GatewayControllerConfig config)
: runtime_(runtime),
dali_domain_(dali_domain),
cache_(cache),
config_(config),
maintenance_lock_(xSemaphoreCreateMutex()) {}
GatewayController::~GatewayController() {
if (maintenance_lock_ != nullptr) {
vSemaphoreDelete(maintenance_lock_);
maintenance_lock_ = nullptr;
}
}
esp_err_t GatewayController::start() {
const auto device_info = runtime_.deviceInfo();
ble_enabled_ = device_info.ble_enabled;
refreshRuntimeGatewayNames();
runtime_.setCommandAddressResolver([this](uint8_t gateway_id, uint8_t raw_addr) {
return resolveInternalGroupRawAddress(gateway_id, raw_addr);
});
dali_domain_.addRawFrameSink([this](const DaliRawFrame& frame) { handleDaliRawFrame(frame); });
for (const auto& channel : dali_domain_.channelInfo()) {
cache_.preloadChannel(channel.gateway_id);
dali_domain_.resetBus(channel.gateway_id);
publishPayload(channel.gateway_id, {0x02, channel.gateway_id, 0x88});
}
if (task_handle_ != nullptr) {
return ESP_OK;
}
const BaseType_t created = xTaskCreate(&GatewayController::TaskEntry, "gateway_ctrl",
config_.task_stack_size, this, config_.task_priority,
&task_handle_);
if (created != pdPASS) {
task_handle_ = nullptr;
ESP_LOGE(kTag, "failed to create controller task");
return ESP_ERR_NO_MEM;
}
ESP_LOGI(kTag, "controller started channels=%u", static_cast<unsigned>(dali_domain_.channelCount()));
return ESP_OK;
}
bool GatewayController::enqueueCommandFrame(const std::vector<uint8_t>& frame) {
if (!GatewayRuntime::isGatewayCommandFrame(frame) || !GatewayRuntime::hasValidChecksum(frame)) {
ESP_LOGW(kTag, "dropped invalid command frame len=%u", static_cast<unsigned>(frame.size()));
return false;
}
if (!runtime_.enqueueCommand(frame, GatewayRuntime::classifyCommandPriority(frame))) {
if (runtime_.lastEnqueueDropReason() != GatewayRuntime::CommandDropReason::kDuplicate) {
ESP_LOGW(kTag, "dropped command frame reason=%d",
static_cast<int>(runtime_.lastEnqueueDropReason()));
}
return false;
}
if (task_handle_ != nullptr) {
xTaskNotifyGive(task_handle_);
}
return true;
}
void GatewayController::addNotificationSink(NotificationSink sink) {
if (sink) {
notification_sinks_.push_back(std::move(sink));
}
}
void GatewayController::addBleStateSink(BleStateSink sink) {
if (sink) {
ble_state_sinks_.push_back(std::move(sink));
}
}
void GatewayController::addWifiStateSink(WifiStateSink sink) {
if (sink) {
wifi_state_sinks_.push_back(std::move(sink));
}
}
void GatewayController::addGatewayNameSink(GatewayNameSink sink) {
if (sink) {
gateway_name_sinks_.push_back(std::move(sink));
}
}
bool GatewayController::setupMode() const {
return setup_mode_;
}
bool GatewayController::wirelessSetupMode() const {
return wireless_setup_mode_;
}
void GatewayController::setWirelessSetupMode(bool enabled) {
wireless_setup_mode_ = enabled;
}
bool GatewayController::bleEnabled() const {
return ble_enabled_;
}
bool GatewayController::wifiEnabled() const {
return wifi_enabled_;
}
bool GatewayController::ipRouterEnabled() const {
return ip_router_enabled_;
}
GatewayControllerSnapshot GatewayController::snapshot() {
GatewayControllerSnapshot out;
out.setup_mode = setup_mode_;
out.wireless_setup_mode = wireless_setup_mode_;
out.ble_enabled = ble_enabled_;
out.wifi_enabled = wifi_enabled_;
out.ip_router_enabled = ip_router_enabled_;
out.internal_scene_supported = config_.internal_scene_supported;
out.internal_group_supported = config_.internal_group_supported;
const auto channels = dali_domain_.channelInfo();
out.channels.reserve(channels.size());
for (const auto& channel : channels) {
const auto [scene_low, scene_high] = sceneMask(channel.gateway_id);
const auto [group_low, group_high] = groupMask(channel.gateway_id);
GatewayChannelSnapshot channel_snapshot;
channel_snapshot.channel_index = channel.channel_index;
channel_snapshot.gateway_id = channel.gateway_id;
channel_snapshot.name = channel.name;
channel_snapshot.phy = PhyKindToString(channel.phy_kind);
channel_snapshot.scene_mask_low = scene_low;
channel_snapshot.scene_mask_high = scene_high;
channel_snapshot.group_mask_low = group_low;
channel_snapshot.group_mask_high = group_high;
channel_snapshot.allocating = dali_domain_.isAllocAddr(channel.gateway_id);
channel_snapshot.last_alloc_addr = dali_domain_.lastAllocAddr(channel.gateway_id);
out.channels.push_back(std::move(channel_snapshot));
}
return out;
}
void GatewayController::TaskEntry(void* arg) {
static_cast<GatewayController*>(arg)->taskLoop();
}
void GatewayController::taskLoop() {
while (true) {
bool worked = false;
if (auto command = runtime_.popNextCommand()) {
worked = true;
dispatchCommand(*command);
runtime_.completeCurrentCommand();
}
if (!worked) {
worked = runMaintenanceStep();
}
if (!worked) {
ulTaskNotifyTake(pdTRUE, hasPendingReconciliation() ? kMaintenancePollTicks : portMAX_DELAY);
}
}
}
void GatewayController::scheduleReconciliation(uint8_t gateway_id) {
if (!cache_.reconciliationEnabled()) {
return;
}
auto flags = cache_.pendingChannelFlags(gateway_id);
if (cache_.fullStateMirrorEnabled() && AnyFlagSet(flags)) {
flags = {true, true, true};
}
if (!AnyFlagSet(flags)) {
return;
}
{
LockGuard guard(maintenance_lock_);
reconciliation_jobs_.try_emplace(gateway_id);
}
if (task_handle_ != nullptr) {
xTaskNotifyGive(task_handle_);
}
}
bool GatewayController::hasPendingReconciliation() const {
LockGuard guard(maintenance_lock_);
return !reconciliation_jobs_.empty();
}
bool GatewayController::runMaintenanceStep() {
if (!cache_.reconciliationEnabled()) {
return false;
}
uint8_t gateway_id = 0;
ReconciliationJob job;
{
LockGuard guard(maintenance_lock_);
if (reconciliation_jobs_.empty()) {
return false;
}
const auto it = reconciliation_jobs_.begin();
gateway_id = it->first;
job = it->second;
}
if (runtime_.shouldYieldMaintenance(gateway_id)) {
return false;
}
const bool keep_job = runReconciliationStep(gateway_id, job);
{
LockGuard guard(maintenance_lock_);
auto it = reconciliation_jobs_.find(gateway_id);
if (it == reconciliation_jobs_.end()) {
return true;
}
if (keep_job) {
it->second = job;
} else {
reconciliation_jobs_.erase(it);
}
}
return true;
}
bool GatewayController::runReconciliationStep(uint8_t gateway_id, ReconciliationJob& job) {
if (job.phase == ReconciliationJob::Phase::kReloadFlags) {
job.flags = cache_.pendingChannelFlags(gateway_id);
if (cache_.fullStateMirrorEnabled() && AnyFlagSet(job.flags)) {
job.flags = {true, true, true};
}
if (!AnyFlagSet(job.flags)) {
return false;
}
job.short_address = 0;
job.scene_id = 0;
if (job.flags.need_update_group) {
job.phase = ReconciliationJob::Phase::kGroups;
} else if (job.flags.need_update_scene) {
job.phase = ReconciliationJob::Phase::kScenes;
} else {
job.phase = ReconciliationJob::Phase::kSettings;
}
}
switch (job.phase) {
case ReconciliationJob::Phase::kGroups:
reconcileGroupStep(gateway_id, job.short_address++);
if (job.short_address >= kDaliShortAddressCount) {
job.short_address = 0;
if (job.flags.need_update_scene) {
job.phase = ReconciliationJob::Phase::kScenes;
} else if (job.flags.need_update_settings) {
job.phase = ReconciliationJob::Phase::kSettings;
} else if (!cache_.clearChannelFlagsIfMatched(gateway_id, job.flags)) {
job.phase = ReconciliationJob::Phase::kReloadFlags;
} else {
return false;
}
}
return true;
case ReconciliationJob::Phase::kScenes:
reconcileSceneStep(gateway_id, job.short_address, job.scene_id);
++job.scene_id;
if (job.scene_id >= kDaliSceneCount) {
job.scene_id = 0;
++job.short_address;
}
if (job.short_address >= kDaliShortAddressCount) {
job.short_address = 0;
if (job.flags.need_update_settings) {
job.phase = ReconciliationJob::Phase::kSettings;
} else if (!cache_.clearChannelFlagsIfMatched(gateway_id, job.flags)) {
job.phase = ReconciliationJob::Phase::kReloadFlags;
} else {
return false;
}
}
return true;
case ReconciliationJob::Phase::kSettings:
reconcileSettingsStep(gateway_id, job.short_address++);
if (job.short_address >= kDaliShortAddressCount) {
job.short_address = 0;
if (!cache_.clearChannelFlagsIfMatched(gateway_id, job.flags)) {
job.phase = ReconciliationJob::Phase::kReloadFlags;
} else {
return false;
}
}
return true;
case ReconciliationJob::Phase::kReloadFlags:
default:
return true;
}
}
void GatewayController::reconcileGroupStep(uint8_t gateway_id, uint8_t short_address) {
const auto policy = cache_.priorityMode();
const auto state = cache_.daliAddressState(gateway_id, short_address);
if (policy == GatewayCachePriorityMode::kLocalGatewayFirst && state.group_mask_known) {
maintenance_activity_gateway_.store(gateway_id);
const bool applied = dali_domain_.applyGroupMask(gateway_id, short_address, state.group_mask);
maintenance_activity_gateway_.store(-1);
const auto verified_mask = dali_domain_.queryGroupMask(gateway_id, short_address);
cache_.setDaliGroupMask(gateway_id, short_address, verified_mask);
if (!applied && verified_mask.has_value()) {
ESP_LOGW(kTag, "group reconcile fallback gateway=%u short=%u", gateway_id, short_address);
}
return;
}
cache_.setDaliGroupMask(gateway_id, short_address,
dali_domain_.queryGroupMask(gateway_id, short_address));
}
void GatewayController::reconcileSceneStep(uint8_t gateway_id, uint8_t short_address,
uint8_t scene_id) {
const auto policy = cache_.priorityMode();
const auto state = cache_.daliAddressState(gateway_id, short_address);
if (policy == GatewayCachePriorityMode::kLocalGatewayFirst &&
state.scene_levels[scene_id].has_value()) {
maintenance_activity_gateway_.store(gateway_id);
dali_domain_.applySceneLevel(gateway_id, short_address, scene_id, state.scene_levels[scene_id]);
maintenance_activity_gateway_.store(-1);
}
cache_.setDaliSceneLevel(gateway_id, short_address, scene_id,
dali_domain_.querySceneLevel(gateway_id, short_address, scene_id));
}
void GatewayController::reconcileSettingsStep(uint8_t gateway_id, uint8_t short_address) {
const auto policy = cache_.priorityMode();
const auto state = cache_.daliAddressState(gateway_id, short_address);
if (policy == GatewayCachePriorityMode::kLocalGatewayFirst && state.settings.anyKnown()) {
maintenance_activity_gateway_.store(gateway_id);
dali_domain_.applyAddressSettings(gateway_id, short_address, {
state.settings.power_on_level,
state.settings.system_failure_level,
state.settings.min_level,
state.settings.max_level,
state.settings.fade_time,
state.settings.fade_rate,
});
maintenance_activity_gateway_.store(-1);
}
const auto settings = dali_domain_.queryAddressSettings(gateway_id, short_address);
if (settings.has_value()) {
cache_.setDaliSettings(gateway_id, short_address,
GatewayCacheDaliSettingsSnapshot{settings->power_on_level,
settings->system_failure_level,
settings->min_level,
settings->max_level,
settings->fade_time,
settings->fade_rate});
} else {
cache_.setDaliSettings(gateway_id, short_address, std::nullopt);
}
}
void GatewayController::dispatchCommand(const std::vector<uint8_t>& command) {
if (command.size() < 7) {
ESP_LOGW(kTag, "command too short len=%u", static_cast<unsigned>(command.size()));
return;
}
const uint8_t gateway_id = command[2];
const uint8_t opcode = command[3];
const uint8_t addr = command[4];
const uint8_t data = command[5];
if (!hasGateway(gateway_id)) {
ESP_LOGW(kTag, "command for unknown gateway=%u opcode=0x%02x", gateway_id, opcode);
return;
}
switch (opcode) {
case 0x00:
esp_restart();
break;
case 0x01:
setup_mode_ = true;
break;
case 0x02:
ESP_LOGI(kTag, "legacy opcode 0x02 requested for gateway=%u", gateway_id);
break;
case 0x03:
if (!config_.ble_supported) {
publishPayload(gateway_id, {0x03, gateway_id, 0xff});
} else if (addr == 0) {
if (runtime_.setBleEnabled(false)) {
ble_enabled_ = runtime_.bleEnabled();
for (const auto& sink : ble_state_sinks_) {
sink(ble_enabled_);
}
publishPayload(gateway_id, {0x03, gateway_id, 0x00});
} else {
publishPayload(gateway_id, {0x03, gateway_id,
static_cast<uint8_t>(ble_enabled_ ? 1 : 0)});
}
} else if (addr == 1) {
if (runtime_.setBleEnabled(true)) {
ble_enabled_ = runtime_.bleEnabled();
for (const auto& sink : ble_state_sinks_) {
sink(ble_enabled_);
}
publishPayload(gateway_id, {0x03, gateway_id, 0x01});
} else {
publishPayload(gateway_id, {0x03, gateway_id,
static_cast<uint8_t>(ble_enabled_ ? 1 : 0)});
}
} else if (addr == 2) {
publishPayload(gateway_id, {0x03, gateway_id, static_cast<uint8_t>(ble_enabled_ ? 1 : 0)});
}
break;
case 0x04:
if (addr == 0) {
wifi_enabled_ = false;
wireless_setup_mode_ = false;
} else if (addr == 1) {
wifi_enabled_ = true;
wireless_setup_mode_ = false;
} else if (addr == 100 || addr == 101) {
wifi_enabled_ = true;
wireless_setup_mode_ = true;
}
for (const auto& sink : wifi_state_sinks_) {
sink(addr);
}
break;
case 0x05:
handleGatewayNameCommand(gateway_id, command);
break;
case 0x06: {
uint8_t feature = 0;
if (setup_mode_ && config_.setup_supported) {
feature |= 0x01;
}
if (wireless_setup_mode_ && config_.wifi_supported) {
feature |= 0x02;
}
if (config_.ip_router_supported && ip_router_enabled_) {
feature |= 0x08;
}
if (config_.internal_scene_supported) {
feature |= 0x10;
}
if (config_.internal_group_supported) {
feature |= 0x20;
}
publishPayload(gateway_id, {0x03, gateway_id, feature});
break;
}
case 0x07:
case 0x08:
sendRawAndMirror(gateway_id, addr, data);
break;
case 0x09: {
const auto ids = gatewayIds();
if (addr >= 1 && addr <= ids.size()) {
const auto selected_gateway = ids[addr - 1];
publishPayload(gateway_id, {0x03, selected_gateway, selected_gateway});
} else {
publishPayload(gateway_id, {0x04, 0xff, 0x00});
}
break;
}
case 0x0A:
handleGatewayIdentityCommand(gateway_id, addr);
break;
case 0x10:
case 0x11:
sendRawAndMirror(gateway_id, resolveInternalGroupRawAddress(gateway_id, addr), data);
break;
case 0x12:
if (addr == 0xff && data >= 0x10 && data <= 0x1f) {
const uint8_t scene_id = static_cast<uint8_t>(data - 0x10);
if (!executeScene(gateway_id, shortAddressFromRaw(addr), scene_id)) {
sendRawAndMirror(gateway_id, addr, data);
}
} else {
sendRawAndMirror(gateway_id, resolveInternalGroupRawAddress(gateway_id, addr), data);
}
break;
case 0x13:
sendExtRawAndMirror(gateway_id, resolveInternalGroupRawAddress(gateway_id, addr), data);
break;
case 0x14: {
const auto result = dali_domain_.queryRaw(gateway_id, resolveInternalGroupRawAddress(gateway_id, addr), data);
if (result.has_value()) {
publishPayload(gateway_id, {0x03, gateway_id, result.value()});
} else {
publishPayload(gateway_id, {0x04, gateway_id, 0x00});
}
break;
}
case 0x15:
dali_domain_.queryRaw(gateway_id, resolveInternalGroupRawAddress(gateway_id, addr), data);
break;
case 0x16:
dali_domain_.queryRaw(gateway_id, 0, 0);
break;
case 0x17:
if (command.size() >= 8) {
const int mirek = command[5] * 256 + command[6];
const uint8_t target = resolveInternalGroupRawAddress(gateway_id, addr);
dali_domain_.setColTempRaw(gateway_id, shortAddressFromRaw(target), mirek);
}
break;
case 0x18:
if (command.size() >= 10) {
const int x = command[5] * 256 + command[6];
const int y = command[7] * 256 + command[8];
dali_domain_.setColourRaw(gateway_id, resolveInternalGroupRawAddress(gateway_id, addr), x, y);
}
break;
case 0x30:
handleAllocationCommand(gateway_id, command);
break;
case 0x31:
break;
case 0x32:
dali_domain_.resetAndAllocAddr(gateway_id);
break;
case 0x37:
if (command.size() >= 8) {
int kelvin = command[5] * 256 + command[6];
const uint8_t target = resolveInternalGroupRawAddress(gateway_id, addr);
if (config_.color_temperature_max < config_.color_temperature_min) {
kelvin = reverseInRange(kelvin, config_.color_temperature_min,
config_.color_temperature_max);
}
dali_domain_.setColTemp(gateway_id, shortAddressFromRaw(target), kelvin);
}
break;
case 0x38:
if (command.size() >= 9) {
const uint8_t r = command[5];
const uint8_t g = command[6];
const uint8_t b = command[7];
const int target = shortAddressFromRaw(resolveInternalGroupRawAddress(gateway_id, addr));
if (r == 0 && g == 0 && b == 0) {
offAndMirror(gateway_id, target);
} else {
dali_domain_.setColourRGB(gateway_id, target, r, g, b);
}
}
break;
case 0xA0:
handleInternalSceneCommand(gateway_id, command);
break;
case 0xA2:
handleInternalGroupCommand(gateway_id, command);
break;
default:
ESP_LOGW(kTag, "unhandled opcode=0x%02x gateway=%u", opcode, gateway_id);
break;
}
}
bool GatewayController::hasGateway(uint8_t gateway_id) const {
const auto channels = dali_domain_.channelInfo();
return std::any_of(channels.begin(), channels.end(), [gateway_id](const DaliChannelInfo& channel) {
return channel.gateway_id == gateway_id;
});
}
std::vector<uint8_t> GatewayController::gatewayIds() const {
std::vector<uint8_t> ids;
const auto channels = dali_domain_.channelInfo();
ids.reserve(channels.size());
for (const auto& channel : channels) {
ids.push_back(channel.gateway_id);
}
return ids;
}
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) {
return channel.gateway_id == gateway_id;
});
if (it != channels.end()) {
return it->name;
}
return runtime_.gatewayName(gateway_id);
}
void GatewayController::refreshRuntimeGatewayNames() {
std::vector<std::string> assigned_names;
assigned_names.reserve(dali_domain_.channelCount());
for (const auto& channel : dali_domain_.channelInfo()) {
std::string runtime_name = NormalizeName(runtime_.gatewayName(channel.gateway_id));
if (std::any_of(assigned_names.begin(), assigned_names.end(),
[&runtime_name](const std::string& assigned_name) {
return assigned_name == runtime_name;
})) {
runtime_name = AppendGatewaySuffix(runtime_name, channel.gateway_id);
}
dali_domain_.updateChannelName(channel.gateway_id, runtime_name);
assigned_names.push_back(runtime_name);
}
}
void GatewayController::publishPayload(uint8_t, const std::vector<uint8_t>& payload) {
publishFrame(GatewayRuntime::buildNotificationFrame(payload));
}
void GatewayController::publishFrame(const std::vector<uint8_t>& frame) {
for (const auto& sink : notification_sinks_) {
sink(frame);
}
}
void GatewayController::handleDaliRawFrame(const DaliRawFrame& frame) {
if (frame.data.size() != 2 && frame.data.size() != 3) {
return;
}
uint8_t addr = 0;
uint8_t data = 0;
if (frame.data.size() == 2) {
addr = frame.data[0];
data = frame.data[1];
if (addr == 0xBE) {
return;
}
} else {
addr = frame.data[1];
data = frame.data[2];
}
const bool maintenance_activity = maintenance_activity_gateway_.load() == frame.gateway_id;
const bool local_activity = maintenance_activity || runtime_.hasActiveCommand(frame.gateway_id) ||
dali_domain_.isAllocAddr(frame.gateway_id);
const bool flagged = cache_.observeDaliCommand(frame.gateway_id, addr, data,
local_activity
? GatewayCacheRawFrameOrigin::kLocalGateway
: GatewayCacheRawFrameOrigin::kOutsideBus);
if (flagged) {
scheduleReconciliation(frame.gateway_id);
}
if (setup_mode_ || dali_domain_.isAllocAddr(frame.gateway_id) || maintenance_activity ||
runtime_.hasActiveQueryCommand(frame.gateway_id)) {
return;
}
publishPayload(frame.gateway_id, {0x01, frame.gateway_id, addr, data});
}
bool GatewayController::sendRawAndMirror(uint8_t gateway_id, uint8_t raw_addr, uint8_t command) {
const bool sent = dali_domain_.sendRaw(gateway_id, raw_addr, command);
if (sent) {
cache_.mirrorDaliCommand(gateway_id, raw_addr, command);
}
return sent;
}
bool GatewayController::sendExtRawAndMirror(uint8_t gateway_id, uint8_t raw_addr,
uint8_t command) {
const bool sent = dali_domain_.sendExtRaw(gateway_id, raw_addr, command);
if (sent) {
cache_.mirrorDaliCommand(gateway_id, raw_addr, command);
}
return sent;
}
bool GatewayController::setBrightAndMirror(uint8_t gateway_id, int dec_address, uint8_t level) {
const bool sent = dali_domain_.setBright(gateway_id, dec_address, level);
if (sent) {
cache_.mirrorDaliCommand(gateway_id, rawArcAddressFromDec(dec_address), level);
}
return sent;
}
bool GatewayController::offAndMirror(uint8_t gateway_id, int dec_address) {
const bool sent = dali_domain_.off(gateway_id, dec_address);
if (sent) {
cache_.mirrorDaliCommand(gateway_id, rawCommandAddressFromDec(dec_address), kDaliCmdOff);
}
return sent;
}
bool GatewayController::onAndMirror(uint8_t gateway_id, int dec_address) {
const bool sent = dali_domain_.on(gateway_id, dec_address);
if (sent) {
cache_.mirrorDaliCommand(gateway_id, rawCommandAddressFromDec(dec_address),
kDaliCmdRecallMax);
}
return sent;
}
uint8_t GatewayController::resolveInternalGroupRawAddress(uint8_t gateway_id, uint8_t raw_addr) {
if (raw_addr < 0x80 || raw_addr > 0x9f) {
return raw_addr;
}
const uint8_t slot = static_cast<uint8_t>((raw_addr - 0x80) / 2);
const auto group_data = cache_.group(gateway_id, slot);
if (!group_data.enabled) {
return raw_addr;
}
return internalGroupRawTargetAddress(group_data.target_type, group_data.target_value, raw_addr);
}
uint8_t GatewayController::normalizeGroupTargetType(uint8_t target_type) {
return target_type <= 2 ? target_type : 2;
}
uint8_t GatewayController::normalizeGroupTargetValue(uint8_t target_type, uint8_t target_value) {
const uint8_t normalized_type = normalizeGroupTargetType(target_type);
if (normalized_type == 0) {
return std::min<uint8_t>(target_value, 63);
}
if (normalized_type == 1) {
return std::min<uint8_t>(target_value, 15);
}
return 0;
}
uint8_t GatewayController::internalGroupRawTargetAddress(uint8_t target_type, uint8_t target_value,
uint8_t raw_addr) {
const uint8_t normalized_type = normalizeGroupTargetType(target_type);
const uint8_t normalized_value = normalizeGroupTargetValue(normalized_type, target_value);
const uint8_t lsb = raw_addr & 0x01;
if (normalized_type == 0) {
return static_cast<uint8_t>(normalized_value * 2 + lsb);
}
if (normalized_type == 1) {
return static_cast<uint8_t>(0x80 + normalized_value * 2 + lsb);
}
return lsb == 0 ? 0xfe : 0xff;
}
int GatewayController::internalGroupDecTargetAddress(uint8_t target_type, uint8_t target_value) {
const uint8_t normalized_type = normalizeGroupTargetType(target_type);
const uint8_t normalized_value = normalizeGroupTargetValue(normalized_type, target_value);
if (normalized_type == 0) {
return normalized_value;
}
if (normalized_type == 1) {
return 64 + normalized_value;
}
return 127;
}
uint8_t GatewayController::rawArcAddressFromDec(int dec_address) {
if (dec_address >= 0 && dec_address < 64) {
return static_cast<uint8_t>(dec_address * 2);
}
if (dec_address >= 64 && dec_address < 80) {
return static_cast<uint8_t>(0x80 + (dec_address - 64) * 2);
}
return 0xfe;
}
uint8_t GatewayController::rawCommandAddressFromDec(int dec_address) {
if (dec_address >= 0 && dec_address < 64) {
return static_cast<uint8_t>(dec_address * 2 + 1);
}
if (dec_address >= 64 && dec_address < 80) {
return static_cast<uint8_t>(0x80 + (dec_address - 64) * 2 + 1);
}
return 0xff;
}
int GatewayController::shortAddressFromRaw(uint8_t raw_addr) {
return raw_addr / 2;
}
int GatewayController::reverseInRange(int value, int min_value, int max_value) {
return min_value + max_value - value;
}
bool GatewayController::setSceneEnabled(uint8_t gateway_id, uint8_t scene_id, bool enabled) {
return cache_.setSceneEnabled(gateway_id, scene_id, enabled);
}
bool GatewayController::setSceneDetail(uint8_t gateway_id, uint8_t scene_id, uint8_t brightness,
uint8_t color_mode, uint8_t data1, uint8_t data2,
uint8_t data3) {
const uint8_t next_brightness = std::min<uint8_t>(brightness, 254);
const uint8_t next_color_mode = std::min<uint8_t>(color_mode, 2);
uint8_t next_data1 = 0;
uint8_t next_data2 = 0;
uint8_t next_data3 = 0;
if (next_color_mode == 0) {
next_data1 = data1;
next_data2 = data2;
} else if (next_color_mode == 1) {
next_data1 = data1;
next_data2 = data2;
next_data3 = data3;
}
return cache_.setSceneDetail(gateway_id, scene_id, next_brightness, next_color_mode,
next_data1, next_data2, next_data3);
}
bool GatewayController::setSceneName(uint8_t gateway_id, uint8_t scene_id, std::string_view name) {
const auto normalized = NormalizeName(name);
return cache_.setSceneName(gateway_id, scene_id, normalized);
}
bool GatewayController::deleteScene(uint8_t gateway_id, uint8_t scene_id) {
return cache_.deleteScene(gateway_id, scene_id);
}
std::pair<uint8_t, uint8_t> GatewayController::sceneMask(uint8_t gateway_id) {
return cache_.sceneMask(gateway_id);
}
bool GatewayController::executeScene(uint8_t gateway_id, int short_address, uint8_t scene_id) {
const auto scene_data = cache_.scene(gateway_id, scene_id);
if (!scene_data.enabled) {
return false;
}
if (scene_data.brightness <= 0) {
offAndMirror(gateway_id, short_address);
} else {
setBrightAndMirror(gateway_id, short_address, scene_data.brightness);
}
if (scene_data.color_mode == 0) {
int kelvin = scene_data.data1 * 256 + scene_data.data2;
if (kelvin > 0) {
if (config_.color_temperature_max < config_.color_temperature_min) {
kelvin = reverseInRange(kelvin, config_.color_temperature_min,
config_.color_temperature_max);
}
dali_domain_.setColTemp(gateway_id, short_address, kelvin);
}
} else if (scene_data.color_mode == 1) {
if (scene_data.data1 != 0 || scene_data.data2 != 0 || scene_data.data3 != 0) {
dali_domain_.setColourRGB(gateway_id, short_address, scene_data.data1, scene_data.data2,
scene_data.data3);
} else if (scene_data.brightness <= 0) {
offAndMirror(gateway_id, short_address);
}
}
return true;
}
bool GatewayController::setGroupEnabled(uint8_t gateway_id, uint8_t group_id, bool enabled) {
return cache_.setGroupEnabled(gateway_id, group_id, enabled);
}
bool GatewayController::setGroupDetail(uint8_t gateway_id, uint8_t group_id, uint8_t target_type,
uint8_t target_value) {
const uint8_t next_target_type = normalizeGroupTargetType(target_type);
const uint8_t next_target_value = normalizeGroupTargetValue(next_target_type, target_value);
return cache_.setGroupDetail(gateway_id, group_id, next_target_type, next_target_value);
}
bool GatewayController::setGroupName(uint8_t gateway_id, uint8_t group_id, std::string_view name) {
const auto normalized = NormalizeName(name);
return cache_.setGroupName(gateway_id, group_id, normalized);
}
bool GatewayController::deleteGroup(uint8_t gateway_id, uint8_t group_id) {
return cache_.deleteGroup(gateway_id, group_id);
}
std::pair<uint8_t, uint8_t> GatewayController::groupMask(uint8_t gateway_id) {
return cache_.groupMask(gateway_id);
}
bool GatewayController::executeGroup(uint8_t gateway_id, uint8_t group_id) {
const auto group_data = cache_.group(gateway_id, group_id);
if (!group_data.enabled) {
return false;
}
return onAndMirror(gateway_id,
internalGroupDecTargetAddress(group_data.target_type,
group_data.target_value));
}
void GatewayController::handleGatewayNameCommand(uint8_t gateway_id,
const std::vector<uint8_t>& command) {
const uint8_t op = command[4];
if (op == 0x00) {
const auto name = gatewayName(gateway_id);
std::vector<uint8_t> payload{0x05, gateway_id, op,
static_cast<uint8_t>(std::min(name.size(), kMaxNameBytes))};
AppendStringBytes(payload, NormalizeName(name));
publishPayload(gateway_id, payload);
return;
}
if (op != 0x01) {
return;
}
if (command.size() < 7) {
publishPayload(gateway_id, {0x05, gateway_id, op, 0x00});
return;
}
const size_t payload_end = command.size() - 1;
const size_t requested_len = std::min<size_t>(command[5], kMaxNameBytes);
const size_t available = payload_end > 6 ? payload_end - 6 : 0;
const size_t actual_len = std::min(requested_len, available);
std::string name;
name.reserve(actual_len);
for (size_t index = 0; index < actual_len; ++index) {
name.push_back(static_cast<char>(command[6 + index]));
}
if (runtime_.setGatewayName(gateway_id, name)) {
refreshRuntimeGatewayNames();
for (const auto& sink : gateway_name_sinks_) {
sink(gateway_id);
}
publishPayload(gateway_id, {0x05, gateway_id, op, 0x01});
} else {
publishPayload(gateway_id, {0x05, gateway_id, op, 0x00});
}
}
void GatewayController::handleGatewayIdentityCommand(uint8_t gateway_id, uint8_t op) {
std::string value;
if (op == 0x00) {
value = gatewayName(gateway_id);
} else if (op == 0x01) {
value = runtime_.gatewaySerialHex(gateway_id);
} else if (op == 0x02) {
value = runtime_.bleMacHex();
} else {
publishPayload(gateway_id, {0x0A, gateway_id, op, 0x00, 0x00});
return;
}
std::vector<uint8_t> payload{0x0A, gateway_id, op,
static_cast<uint8_t>(std::min(value.size(), kMaxNameBytes))};
AppendStringBytes(payload, value);
publishPayload(gateway_id, payload);
}
void GatewayController::handleAllocationCommand(uint8_t gateway_id,
const std::vector<uint8_t>& command) {
const uint8_t mode = command[4];
const uint8_t start_addr = command[5];
if (mode == 0) {
dali_domain_.stopAllocAddr(gateway_id);
} else if (mode == 1) {
dali_domain_.allocateAllAddr(gateway_id, start_addr);
} else if (mode == 2) {
dali_domain_.resetAndAllocAddr(gateway_id);
} else if (mode == 3) {
publishPayload(gateway_id, {0x30, gateway_id,
static_cast<uint8_t>(dali_domain_.isAllocAddr(gateway_id) ? 1 : 0),
static_cast<uint8_t>(dali_domain_.lastAllocAddr(gateway_id) & 0xff)});
}
}
void GatewayController::handleInternalSceneCommand(uint8_t gateway_id,
const std::vector<uint8_t>& command) {
if (command.size() < 7) {
publishPayload(gateway_id, {0xA1, gateway_id, 0xff, 0xff, 0x01});
return;
}
const uint8_t op = command[4];
const uint8_t scene_id = command[5];
if (scene_id > 15) {
publishPayload(gateway_id, {0xA1, gateway_id, op, scene_id, 0x01});
return;
}
switch (op) {
case 0x00:
publishPayload(gateway_id, setSceneEnabled(gateway_id, scene_id, true)
? std::vector<uint8_t>{0xA0, gateway_id, op, scene_id, 0x01}
: std::vector<uint8_t>{0xA1, gateway_id, op, scene_id, 0x02});
break;
case 0x01:
publishPayload(gateway_id, setSceneEnabled(gateway_id, scene_id, false)
? std::vector<uint8_t>{0xA0, gateway_id, op, scene_id, 0x01}
: std::vector<uint8_t>{0xA1, gateway_id, op, scene_id, 0x02});
break;
case 0x02:
publishPayload(gateway_id, {0xA0, gateway_id, op, scene_id,
static_cast<uint8_t>(cache_.scene(gateway_id, scene_id).enabled ? 1 : 0)});
break;
case 0x03: {
const auto [low, high] = sceneMask(gateway_id);
publishPayload(gateway_id, {0xA0, gateway_id, op, low, high});
break;
}
case 0x04:
{
const auto scene_data = cache_.scene(gateway_id, scene_id);
publishPayload(gateway_id, {0xA0, gateway_id, op, scene_id, scene_data.brightness,
scene_data.color_mode, scene_data.data1, scene_data.data2,
scene_data.data3});
break;
}
case 0x05:
if (command.size() >= 11 && setSceneDetail(gateway_id, scene_id, command[6], command[7],
command[8], command[9], command[10])) {
publishPayload(gateway_id, {0xA0, gateway_id, op, scene_id, 0x01});
} else {
publishPayload(gateway_id, {0xA1, gateway_id, op, scene_id, 0x02});
}
break;
case 0x06:
publishPayload(gateway_id, deleteScene(gateway_id, scene_id)
? std::vector<uint8_t>{0xA0, gateway_id, op, scene_id, 0x01}
: std::vector<uint8_t>{0xA1, gateway_id, op, scene_id, 0x02});
break;
case 0x07: {
const auto scene_data = cache_.scene(gateway_id, scene_id);
std::vector<uint8_t> payload{0xA0, gateway_id, op, scene_id};
AppendPaddedName(payload, scene_data.name);
publishPayload(gateway_id, payload);
break;
}
case 0x08: {
if (command.size() < 8) {
publishPayload(gateway_id, {0xA1, gateway_id, op, scene_id, 0x02});
break;
}
const size_t payload_end = command.size() - 1;
const size_t requested_len = std::min<size_t>(command[6], kMaxNameBytes);
const size_t available = payload_end > 7 ? payload_end - 7 : 0;
const size_t actual_len = std::min(requested_len, available);
std::string name;
for (size_t index = 0; index < actual_len; ++index) {
name.push_back(static_cast<char>(command[7 + index]));
}
publishPayload(gateway_id, setSceneName(gateway_id, scene_id, name)
? std::vector<uint8_t>{0xA0, gateway_id, op, scene_id, 0x01}
: std::vector<uint8_t>{0xA1, gateway_id, op, scene_id, 0x02});
break;
}
default:
publishPayload(gateway_id, {0xA1, gateway_id, op, scene_id, 0x03});
break;
}
}
void GatewayController::handleInternalGroupCommand(uint8_t gateway_id,
const std::vector<uint8_t>& command) {
if (command.size() < 7) {
publishPayload(gateway_id, {0xA3, gateway_id, 0xff, 0xff, 0x01});
return;
}
const uint8_t op = command[4];
const uint8_t group_id = command[5];
if (group_id > 15) {
publishPayload(gateway_id, {0xA3, gateway_id, op, group_id, 0x01});
return;
}
switch (op) {
case 0x00:
publishPayload(gateway_id, setGroupEnabled(gateway_id, group_id, true)
? std::vector<uint8_t>{0xA2, gateway_id, op, group_id, 0x01}
: std::vector<uint8_t>{0xA3, gateway_id, op, group_id, 0x02});
break;
case 0x01:
publishPayload(gateway_id, setGroupEnabled(gateway_id, group_id, false)
? std::vector<uint8_t>{0xA2, gateway_id, op, group_id, 0x01}
: std::vector<uint8_t>{0xA3, gateway_id, op, group_id, 0x02});
break;
case 0x02:
publishPayload(gateway_id, {0xA2, gateway_id, op, group_id,
static_cast<uint8_t>(cache_.group(gateway_id, group_id).enabled ? 1 : 0)});
break;
case 0x03: {
const auto [low, high] = groupMask(gateway_id);
publishPayload(gateway_id, {0xA2, gateway_id, op, low, high});
break;
}
case 0x04:
{
const auto group_data = cache_.group(gateway_id, group_id);
publishPayload(gateway_id, {0xA2, gateway_id, op, group_id,
normalizeGroupTargetType(group_data.target_type),
normalizeGroupTargetValue(group_data.target_type,
group_data.target_value)});
break;
}
case 0x05:
if (command.size() >= 9 && setGroupDetail(gateway_id, group_id, command[6], command[7])) {
publishPayload(gateway_id, {0xA2, gateway_id, op, group_id, 0x01});
} else {
publishPayload(gateway_id, {0xA3, gateway_id, op, group_id, 0x02});
}
break;
case 0x06:
publishPayload(gateway_id, deleteGroup(gateway_id, group_id)
? std::vector<uint8_t>{0xA2, gateway_id, op, group_id, 0x01}
: std::vector<uint8_t>{0xA3, gateway_id, op, group_id, 0x02});
break;
case 0x07: {
const auto group_data = cache_.group(gateway_id, group_id);
std::vector<uint8_t> payload{0xA2, gateway_id, op, group_id};
AppendPaddedName(payload, group_data.name);
publishPayload(gateway_id, payload);
break;
}
case 0x08: {
if (command.size() < 8) {
publishPayload(gateway_id, {0xA3, gateway_id, op, group_id, 0x02});
break;
}
const size_t payload_end = command.size() - 1;
const size_t requested_len = std::min<size_t>(command[6], kMaxNameBytes);
const size_t available = payload_end > 7 ? payload_end - 7 : 0;
const size_t actual_len = std::min(requested_len, available);
std::string name;
for (size_t index = 0; index < actual_len; ++index) {
name.push_back(static_cast<char>(command[7 + index]));
}
publishPayload(gateway_id, setGroupName(gateway_id, group_id, name)
? std::vector<uint8_t>{0xA2, gateway_id, op, group_id, 0x01}
: std::vector<uint8_t>{0xA3, gateway_id, op, group_id, 0x02});
break;
}
case 0x09:
publishPayload(gateway_id, executeGroup(gateway_id, group_id)
? std::vector<uint8_t>{0xA2, gateway_id, op, group_id, 0x01}
: std::vector<uint8_t>{0xA3, gateway_id, op, group_id, 0x02});
break;
default:
publishPayload(gateway_id, {0xA3, gateway_id, op, group_id, 0x03});
break;
}
}
} // namespace gateway