#include "gateway_controller.hpp" #include "dali_domain.hpp" #include "esp_log.h" #include "esp_system.h" #include "gateway_runtime.hpp" #include #include #include #include namespace gateway { namespace { constexpr const char* kTag = "gateway_controller"; constexpr const char* kStorageNamespace = "gateway_rt"; constexpr size_t kMaxNameBytes = 32; std::string ShortKey(const char* prefix, uint8_t gateway_id, uint8_t slot) { char key[16] = {0}; std::snprintf(key, sizeof(key), "%s%u_%u", prefix, gateway_id, slot); return std::string(key); } std::vector ParseCsv(std::string_view raw) { std::vector values; size_t start = 0; while (start < raw.size()) { const size_t comma = raw.find(',', start); const size_t end = comma == std::string_view::npos ? raw.size() : comma; if (end > start) { values.push_back(static_cast(std::strtol(std::string(raw.substr(start, end - start)).c_str(), nullptr, 10))); } if (comma == std::string_view::npos) { break; } start = comma + 1; } return values; } 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& out, std::string_view value) { for (const auto ch : value) { out.push_back(static_cast(ch)); } } void AppendPaddedName(std::vector& out, std::string_view name) { const auto normalized = NormalizeName(name); out.push_back(static_cast(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, GatewayControllerConfig config) : runtime_(runtime), dali_domain_(dali_domain), config_(config) {} GatewayController::~GatewayController() { closeStorage(); } esp_err_t GatewayController::start() { const esp_err_t err = openStorage() ? ESP_OK : ESP_FAIL; if (err != ESP_OK) { return err; } 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()) { sceneStore(channel.gateway_id); groupStore(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(dali_domain_.channelCount())); return ESP_OK; } bool GatewayController::enqueueCommandFrame(const std::vector& frame) { if (!GatewayRuntime::isGatewayCommandFrame(frame) || !GatewayRuntime::hasValidChecksum(frame)) { ESP_LOGW(kTag, "dropped invalid command frame len=%u", static_cast(frame.size())); return false; } if (!runtime_.enqueueCommand(frame)) { if (runtime_.lastEnqueueDropReason() != GatewayRuntime::CommandDropReason::kDuplicate) { ESP_LOGW(kTag, "dropped command frame reason=%d", static_cast(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(arg)->taskLoop(); } void GatewayController::taskLoop() { while (true) { bool drained = false; while (auto command = runtime_.popNextCommand()) { drained = true; dispatchCommand(*command); runtime_.completeCurrentCommand(); } if (!drained) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); } } } void GatewayController::dispatchCommand(const std::vector& command) { if (command.size() < 7) { ESP_LOGW(kTag, "command too short len=%u", static_cast(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(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(ble_enabled_ ? 1 : 0)}); } } else if (addr == 2) { publishPayload(gateway_id, {0x03, gateway_id, static_cast(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: dali_domain_.sendRaw(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: dali_domain_.sendRaw(gateway_id, resolveInternalGroupRawAddress(gateway_id, addr), data); break; case 0x12: if (addr == 0xff && data >= 0x10 && data <= 0x1f) { const uint8_t scene_id = static_cast(data - 0x10); if (!executeScene(gateway_id, shortAddressFromRaw(addr), scene_id)) { dali_domain_.sendRaw(gateway_id, addr, data); } } else { dali_domain_.sendRaw(gateway_id, resolveInternalGroupRawAddress(gateway_id, addr), data); } break; case 0x13: dali_domain_.sendExtRaw(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) { dali_domain_.off(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 GatewayController::gatewayIds() const { std::vector 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 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& payload) { publishFrame(GatewayRuntime::buildNotificationFrame(payload)); } void GatewayController::publishFrame(const std::vector& 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; } if (setup_mode_ || dali_domain_.isAllocAddr(frame.gateway_id) || runtime_.hasActiveQueryCommand(frame.gateway_id)) { 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]; } publishPayload(frame.gateway_id, {0x01, frame.gateway_id, addr, data}); } 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((raw_addr - 0x80) / 2); auto* group_data = group(gateway_id, slot); if (group_data == nullptr || !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(target_value, 63); } if (normalized_type == 1) { return std::min(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(normalized_value * 2 + lsb); } if (normalized_type == 1) { return static_cast(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; } 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; } GatewayController::SceneStore& GatewayController::sceneStore(uint8_t gateway_id) { auto [it, inserted] = scenes_.try_emplace(gateway_id); if (inserted) { loadSceneStore(gateway_id, it->second); } return it->second; } GatewayController::GroupStore& GatewayController::groupStore(uint8_t gateway_id) { auto [it, inserted] = groups_.try_emplace(gateway_id); if (inserted) { loadGroupStore(gateway_id, it->second); } return it->second; } GatewayController::InternalScene* GatewayController::scene(uint8_t gateway_id, uint8_t scene_id) { if (scene_id >= 16) { return nullptr; } return &sceneStore(gateway_id)[scene_id]; } GatewayController::InternalGroup* GatewayController::group(uint8_t gateway_id, uint8_t group_id) { if (group_id >= 16) { return nullptr; } return &groupStore(gateway_id)[group_id]; } bool GatewayController::setSceneEnabled(uint8_t gateway_id, uint8_t scene_id, bool enabled) { auto* scene_data = scene(gateway_id, scene_id); if (scene_data == nullptr) { return false; } if (scene_data->enabled == enabled) { return true; } scene_data->enabled = enabled; return saveScene(gateway_id, scene_id); } 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) { auto* scene_data = scene(gateway_id, scene_id); if (scene_data == nullptr) { return false; } const uint8_t next_brightness = std::min(brightness, 254); const uint8_t next_color_mode = std::min(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; } if (scene_data->brightness == next_brightness && scene_data->color_mode == next_color_mode && scene_data->data1 == next_data1 && scene_data->data2 == next_data2 && scene_data->data3 == next_data3) { return true; } scene_data->brightness = next_brightness; scene_data->color_mode = next_color_mode; scene_data->data1 = next_data1; scene_data->data2 = next_data2; scene_data->data3 = next_data3; return saveScene(gateway_id, scene_id); } bool GatewayController::setSceneName(uint8_t gateway_id, uint8_t scene_id, std::string_view name) { auto* scene_data = scene(gateway_id, scene_id); if (scene_data == nullptr) { return false; } const auto normalized = NormalizeName(name); if (scene_data->name == normalized) { return true; } scene_data->name = normalized; return saveSceneName(gateway_id, scene_id, scene_data->name); } bool GatewayController::deleteScene(uint8_t gateway_id, uint8_t scene_id) { auto* scene_data = scene(gateway_id, scene_id); if (scene_data == nullptr) { return false; } const bool already_default = !scene_data->enabled && scene_data->brightness == 254 && scene_data->color_mode == 2 && scene_data->data1 == 0 && scene_data->data2 == 0 && scene_data->data3 == 0 && scene_data->name.empty(); if (already_default) { return true; } *scene_data = InternalScene{}; deleteSceneStorage(gateway_id, scene_id); deleteSceneNameStorage(gateway_id, scene_id); return true; } std::pair GatewayController::sceneMask(uint8_t gateway_id) { const auto& scenes = sceneStore(gateway_id); uint16_t mask = 0; for (size_t index = 0; index < scenes.size(); ++index) { if (scenes[index].enabled) { mask |= static_cast(1U << index); } } return {static_cast(mask & 0xff), static_cast((mask >> 8) & 0xff)}; } bool GatewayController::executeScene(uint8_t gateway_id, int short_address, uint8_t scene_id) { auto* scene_data = scene(gateway_id, scene_id); if (scene_data == nullptr || !scene_data->enabled) { return false; } if (scene_data->brightness <= 0) { dali_domain_.off(gateway_id, short_address); } else { dali_domain_.setBright(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) { dali_domain_.off(gateway_id, short_address); } } return true; } bool GatewayController::setGroupEnabled(uint8_t gateway_id, uint8_t group_id, bool enabled) { auto* group_data = group(gateway_id, group_id); if (group_data == nullptr) { return false; } if (group_data->enabled == enabled) { return true; } group_data->enabled = enabled; return saveGroup(gateway_id, group_id); } bool GatewayController::setGroupDetail(uint8_t gateway_id, uint8_t group_id, uint8_t target_type, uint8_t target_value) { auto* group_data = group(gateway_id, group_id); if (group_data == nullptr) { return false; } const uint8_t next_target_type = normalizeGroupTargetType(target_type); const uint8_t next_target_value = normalizeGroupTargetValue(next_target_type, target_value); if (group_data->target_type == next_target_type && group_data->target_value == next_target_value) { return true; } group_data->target_type = next_target_type; group_data->target_value = next_target_value; return saveGroup(gateway_id, group_id); } bool GatewayController::setGroupName(uint8_t gateway_id, uint8_t group_id, std::string_view name) { auto* group_data = group(gateway_id, group_id); if (group_data == nullptr) { return false; } const auto normalized = NormalizeName(name); if (group_data->name == normalized) { return true; } group_data->name = normalized; return saveGroupName(gateway_id, group_id, group_data->name); } bool GatewayController::deleteGroup(uint8_t gateway_id, uint8_t group_id) { auto* group_data = group(gateway_id, group_id); if (group_data == nullptr) { return false; } const bool already_default = !group_data->enabled && group_data->target_type == 2 && group_data->target_value == 0 && group_data->name.empty(); if (already_default) { return true; } *group_data = InternalGroup{}; deleteGroupStorage(gateway_id, group_id); deleteGroupNameStorage(gateway_id, group_id); return true; } std::pair GatewayController::groupMask(uint8_t gateway_id) { const auto& groups = groupStore(gateway_id); uint16_t mask = 0; for (size_t index = 0; index < groups.size(); ++index) { if (groups[index].enabled) { mask |= static_cast(1U << index); } } return {static_cast(mask & 0xff), static_cast((mask >> 8) & 0xff)}; } bool GatewayController::executeGroup(uint8_t gateway_id, uint8_t group_id) { auto* group_data = group(gateway_id, group_id); if (group_data == nullptr || !group_data->enabled) { return false; } return dali_domain_.on(gateway_id, internalGroupDecTargetAddress(group_data->target_type, group_data->target_value)); } void GatewayController::handleGatewayNameCommand(uint8_t gateway_id, const std::vector& command) { const uint8_t op = command[4]; if (op == 0x00) { const auto name = gatewayName(gateway_id); std::vector payload{0x05, gateway_id, op, static_cast(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(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(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 payload{0x0A, gateway_id, op, static_cast(std::min(value.size(), kMaxNameBytes))}; AppendStringBytes(payload, value); publishPayload(gateway_id, payload); } void GatewayController::handleAllocationCommand(uint8_t gateway_id, const std::vector& 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(dali_domain_.isAllocAddr(gateway_id) ? 1 : 0), static_cast(dali_domain_.lastAllocAddr(gateway_id) & 0xff)}); } } void GatewayController::handleInternalSceneCommand(uint8_t gateway_id, const std::vector& 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; } auto* scene_data = scene(gateway_id, scene_id); switch (op) { case 0x00: publishPayload(gateway_id, setSceneEnabled(gateway_id, scene_id, true) ? std::vector{0xA0, gateway_id, op, scene_id, 0x01} : std::vector{0xA1, gateway_id, op, scene_id, 0x02}); break; case 0x01: publishPayload(gateway_id, setSceneEnabled(gateway_id, scene_id, false) ? std::vector{0xA0, gateway_id, op, scene_id, 0x01} : std::vector{0xA1, gateway_id, op, scene_id, 0x02}); break; case 0x02: publishPayload(gateway_id, {0xA0, gateway_id, op, scene_id, static_cast(scene_data->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: 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{0xA0, gateway_id, op, scene_id, 0x01} : std::vector{0xA1, gateway_id, op, scene_id, 0x02}); break; case 0x07: { std::vector 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(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(command[7 + index])); } publishPayload(gateway_id, setSceneName(gateway_id, scene_id, name) ? std::vector{0xA0, gateway_id, op, scene_id, 0x01} : std::vector{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& 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; } auto* group_data = group(gateway_id, group_id); switch (op) { case 0x00: publishPayload(gateway_id, setGroupEnabled(gateway_id, group_id, true) ? std::vector{0xA2, gateway_id, op, group_id, 0x01} : std::vector{0xA3, gateway_id, op, group_id, 0x02}); break; case 0x01: publishPayload(gateway_id, setGroupEnabled(gateway_id, group_id, false) ? std::vector{0xA2, gateway_id, op, group_id, 0x01} : std::vector{0xA3, gateway_id, op, group_id, 0x02}); break; case 0x02: publishPayload(gateway_id, {0xA2, gateway_id, op, group_id, static_cast(group_data->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: 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{0xA2, gateway_id, op, group_id, 0x01} : std::vector{0xA3, gateway_id, op, group_id, 0x02}); break; case 0x07: { std::vector 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(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(command[7 + index])); } publishPayload(gateway_id, setGroupName(gateway_id, group_id, name) ? std::vector{0xA2, gateway_id, op, group_id, 0x01} : std::vector{0xA3, gateway_id, op, group_id, 0x02}); break; } case 0x09: publishPayload(gateway_id, executeGroup(gateway_id, group_id) ? std::vector{0xA2, gateway_id, op, group_id, 0x01} : std::vector{0xA3, gateway_id, op, group_id, 0x02}); break; default: publishPayload(gateway_id, {0xA3, gateway_id, op, group_id, 0x03}); break; } } bool GatewayController::openStorage() { if (storage_ != 0) { return true; } const esp_err_t err = nvs_open(kStorageNamespace, NVS_READWRITE, &storage_); if (err != ESP_OK) { ESP_LOGE(kTag, "failed to open controller storage: %s", esp_err_to_name(err)); return false; } return true; } void GatewayController::closeStorage() { if (storage_ != 0) { nvs_close(storage_); storage_ = 0; } } void GatewayController::loadSceneStore(uint8_t gateway_id, SceneStore& scenes) { for (uint8_t scene_id = 0; scene_id < scenes.size(); ++scene_id) { const auto raw = readString(ShortKey("sc", gateway_id, scene_id)); const auto values = ParseCsv(raw); if (values.size() >= 6) { scenes[scene_id].enabled = values[0] != 0; scenes[scene_id].brightness = static_cast(std::clamp(values[1], 0, 254)); scenes[scene_id].color_mode = static_cast(std::clamp(values[2], 0, 2)); scenes[scene_id].data1 = static_cast(std::clamp(values[3], 0, 255)); scenes[scene_id].data2 = static_cast(std::clamp(values[4], 0, 255)); scenes[scene_id].data3 = static_cast(std::clamp(values[5], 0, 255)); } scenes[scene_id].name = NormalizeName(readString(ShortKey("sn", gateway_id, scene_id))); } } void GatewayController::loadGroupStore(uint8_t gateway_id, GroupStore& groups) { for (uint8_t group_id = 0; group_id < groups.size(); ++group_id) { const auto raw = readString(ShortKey("gr", gateway_id, group_id)); const auto values = ParseCsv(raw); if (values.size() >= 2) { groups[group_id].enabled = values[0] != 0; uint8_t target_type = static_cast(std::clamp(values[1], 0, 255)); uint8_t target_value = values.size() >= 3 ? static_cast(std::clamp(values[2], 0, 255)) : target_type; if (values.size() < 3) { target_type = 2; } groups[group_id].target_type = normalizeGroupTargetType(target_type); groups[group_id].target_value = normalizeGroupTargetValue(groups[group_id].target_type, target_value); } groups[group_id].name = NormalizeName(readString(ShortKey("gn", gateway_id, group_id))); } } bool GatewayController::saveScene(uint8_t gateway_id, uint8_t scene_id) { const auto* scene_data = scene(gateway_id, scene_id); if (scene_data == nullptr) { return false; } char payload[32] = {0}; std::snprintf(payload, sizeof(payload), "%u,%u,%u,%u,%u,%u", scene_data->enabled ? 1 : 0, scene_data->brightness, scene_data->color_mode, scene_data->data1, scene_data->data2, scene_data->data3); return writeString(ShortKey("sc", gateway_id, scene_id), payload); } bool GatewayController::deleteSceneStorage(uint8_t gateway_id, uint8_t scene_id) { return eraseKey(ShortKey("sc", gateway_id, scene_id)); } bool GatewayController::saveSceneName(uint8_t gateway_id, uint8_t scene_id, std::string_view name) { if (name.empty()) { return deleteSceneNameStorage(gateway_id, scene_id); } return writeString(ShortKey("sn", gateway_id, scene_id), NormalizeName(name)); } bool GatewayController::deleteSceneNameStorage(uint8_t gateway_id, uint8_t scene_id) { return eraseKey(ShortKey("sn", gateway_id, scene_id)); } bool GatewayController::saveGroup(uint8_t gateway_id, uint8_t group_id) { const auto* group_data = group(gateway_id, group_id); if (group_data == nullptr) { return false; } char payload[24] = {0}; std::snprintf(payload, sizeof(payload), "%u,%u,%u", group_data->enabled ? 1 : 0, normalizeGroupTargetType(group_data->target_type), normalizeGroupTargetValue(group_data->target_type, group_data->target_value)); return writeString(ShortKey("gr", gateway_id, group_id), payload); } bool GatewayController::deleteGroupStorage(uint8_t gateway_id, uint8_t group_id) { return eraseKey(ShortKey("gr", gateway_id, group_id)); } bool GatewayController::saveGroupName(uint8_t gateway_id, uint8_t group_id, std::string_view name) { if (name.empty()) { return deleteGroupNameStorage(gateway_id, group_id); } return writeString(ShortKey("gn", gateway_id, group_id), NormalizeName(name)); } bool GatewayController::deleteGroupNameStorage(uint8_t gateway_id, uint8_t group_id) { return eraseKey(ShortKey("gn", gateway_id, group_id)); } std::string GatewayController::readString(std::string_view key) const { if (storage_ == 0) { return {}; } size_t required_size = 0; if (nvs_get_str(storage_, std::string(key).c_str(), nullptr, &required_size) != ESP_OK || required_size == 0) { return {}; } std::string value(required_size - 1, '\0'); if (nvs_get_str(storage_, std::string(key).c_str(), value.data(), &required_size) != ESP_OK) { return {}; } return value; } bool GatewayController::writeString(std::string_view key, std::string_view value) { if (storage_ == 0) { return false; } return nvs_set_str(storage_, std::string(key).c_str(), std::string(value).c_str()) == ESP_OK && nvs_commit(storage_) == ESP_OK; } bool GatewayController::eraseKey(std::string_view key) { if (storage_ == 0) { return false; } const esp_err_t err = nvs_erase_key(storage_, std::string(key).c_str()); if (err == ESP_ERR_NVS_NOT_FOUND) { return true; } if (err != ESP_OK) { return false; } return nvs_commit(storage_) == ESP_OK; } } // namespace gateway