#include "gateway_runtime.hpp" #include #include #include #include #include #include "dali_domain.hpp" #include "esp_mac.h" #include "esp_log.h" #include "nvs_flash.h" namespace gateway { namespace { constexpr const char* kTag = "gateway_runtime"; constexpr const char* kNamespace = "gateway_rt"; constexpr const char* kBleEnabledKey = "ble_enabled"; constexpr const char* kWifiSsidKey = "wifi_ssid"; constexpr const char* kWifiPasswordKey = "wifi_passwd"; constexpr size_t kMaxGatewayNameBytes = 32; constexpr uint8_t kCommandFramePrefix0 = 0x28; constexpr uint8_t kCommandFramePrefix1 = 0x01; constexpr uint8_t kNotifyFramePrefix = 0x22; class LockGuard { public: explicit LockGuard(SemaphoreHandle_t lock) : lock_(lock) { if (lock_ != nullptr) { xSemaphoreTakeRecursive(lock_, portMAX_DELAY); } } ~LockGuard() { if (lock_ != nullptr) { xSemaphoreGiveRecursive(lock_); } } private: SemaphoreHandle_t lock_; }; } // namespace esp_err_t InitializeRuntimeNvs() { esp_err_t err = nvs_flash_init(); if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { ESP_ERROR_CHECK(nvs_flash_erase()); err = nvs_flash_init(); } return err; } std::string ReadRuntimeSerialId() { uint8_t mac[6] = {0}; if (esp_read_mac(mac, ESP_MAC_WIFI_STA) != ESP_OK) { return "DALIGW"; } char serial[13] = {0}; std::snprintf(serial, sizeof(serial), "%02X%02X%02X%02X%02X%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); return std::string(serial); } GatewaySettingsStore::GatewaySettingsStore() = default; GatewaySettingsStore::~GatewaySettingsStore() { close(); } esp_err_t GatewaySettingsStore::open() { if (handle_ != 0) { return ESP_OK; } return nvs_open(kNamespace, NVS_READWRITE, &handle_); } void GatewaySettingsStore::close() { if (handle_ != 0) { nvs_close(handle_); handle_ = 0; } } bool GatewaySettingsStore::getBleEnabled(bool default_value) const { if (handle_ == 0) { return default_value; } uint8_t enabled = default_value ? 1 : 0; if (nvs_get_u8(handle_, kBleEnabledKey, &enabled) != ESP_OK) { return default_value; } return enabled != 0; } bool GatewaySettingsStore::setBleEnabled(bool enabled) { if (handle_ == 0) { return false; } return nvs_set_u8(handle_, kBleEnabledKey, enabled ? 1 : 0) == ESP_OK && nvs_commit(handle_) == ESP_OK; } std::optional GatewaySettingsStore::getWifiSsid() const { return readString(kWifiSsidKey); } std::optional GatewaySettingsStore::getWifiPassword() const { return readString(kWifiPasswordKey); } bool GatewaySettingsStore::setWifiCredentials(std::string_view ssid, std::string_view password) { return writeString(kWifiSsidKey, ssid) && writeString(kWifiPasswordKey, password); } std::string GatewaySettingsStore::getGatewayName(uint8_t gateway_id, std::string_view fallback) const { const auto value = readString(makeGatewayNameKey(gateway_id)); if (!value.has_value() || value->empty()) { return std::string(fallback); } return *value; } bool GatewaySettingsStore::setGatewayName(uint8_t gateway_id, std::string_view name) { return writeString(makeGatewayNameKey(gateway_id), name); } std::optional GatewaySettingsStore::readString(std::string_view key) const { if (handle_ == 0) { return std::nullopt; } size_t required_size = 0; const esp_err_t err = nvs_get_str(handle_, std::string(key).c_str(), nullptr, &required_size); if (err != ESP_OK || required_size == 0) { return std::nullopt; } std::string value(required_size - 1, '\0'); if (nvs_get_str(handle_, std::string(key).c_str(), value.data(), &required_size) != ESP_OK) { return std::nullopt; } return value; } bool GatewaySettingsStore::writeString(std::string_view key, std::string_view value) { if (handle_ == 0) { return false; } return nvs_set_str(handle_, std::string(key).c_str(), std::string(value).c_str()) == ESP_OK && nvs_commit(handle_) == ESP_OK; } std::string GatewaySettingsStore::makeGatewayNameKey(uint8_t gateway_id) const { char key[24] = {0}; std::snprintf(key, sizeof(key), "dali_gw_name_%u", gateway_id); return std::string(key); } GatewayRuntime::GatewayRuntime(BootProfile profile, GatewayRuntimeConfig config, DaliDomainService* dali_domain) : profile_(profile), config_(std::move(config)), dali_domain_(dali_domain), command_address_resolver_([](uint8_t, uint8_t raw_addr) { return raw_addr; }), command_lock_(xSemaphoreCreateRecursiveMutex()) {} GatewayRuntime::~GatewayRuntime() { if (command_lock_ != nullptr) { vSemaphoreDelete(command_lock_); command_lock_ = nullptr; } } esp_err_t GatewayRuntime::start() { const esp_err_t err = settings_.open(); if (err != ESP_OK) { ESP_LOGE(kTag, "failed to open settings store: %s", esp_err_to_name(err)); return err; } ble_enabled_ = settings_.getBleEnabled(profile_.enable_ble); if (!wireless_info_.has_value()) { WirelessInfo info; const auto ssid = settings_.getWifiSsid(); const auto password = settings_.getWifiPassword(); if (ssid.has_value()) { info.ssid = *ssid; } if (password.has_value()) { info.password = *password; } if (!info.ssid.empty() || !info.password.empty()) { wireless_info_ = std::move(info); } } ESP_LOGI(kTag, "runtime project=%.*s version=%.*s serial=%s ble=%d dali_bound=%d", static_cast(config_.project_name.size()), config_.project_name.data(), static_cast(config_.version.size()), config_.version.data(), config_.serial_id.c_str(), ble_enabled_, dali_domain_ != nullptr && dali_domain_->isBound()); return ESP_OK; } std::vector GatewayRuntime::checksum(std::vector frame) { uint32_t sum = 0; for (const auto byte : frame) { sum += byte; } frame.push_back(static_cast(sum & 0xFF)); return frame; } bool GatewayRuntime::hasValidChecksum(const std::vector& frame) { if (frame.empty()) { return false; } if (frame.back() == 0xFF) { return true; } uint32_t sum = 0; for (size_t i = 0; i + 1 < frame.size(); ++i) { sum += frame[i]; } return static_cast(sum & 0xFF) == frame.back(); } bool GatewayRuntime::isGatewayCommandFrame(const std::vector& frame) { return frame.size() >= 2 && frame[0] == kCommandFramePrefix0 && frame[1] == kCommandFramePrefix1; } std::vector GatewayRuntime::buildNotificationFrame(const std::vector& payload) { std::vector frame; frame.reserve(payload.size() + 2); frame.push_back(kNotifyFramePrefix); frame.insert(frame.end(), payload.begin(), payload.end()); return checksum(std::move(frame)); } bool GatewayRuntime::enqueueCommand(std::vector command) { LockGuard guard(command_lock_); last_enqueue_drop_reason_ = CommandDropReason::kNone; if (isQueryCommand(command) && hasPendingQueryCommand(command)) { last_enqueue_drop_reason_ = CommandDropReason::kDuplicate; return false; } if (pending_commands_.size() >= config_.command_queue_capacity) { last_enqueue_drop_reason_ = CommandDropReason::kQueueFull; return false; } pending_commands_.push_back(std::move(command)); return true; } std::optional> GatewayRuntime::popNextCommand() { LockGuard guard(command_lock_); if (pending_commands_.empty()) { current_command_.reset(); return std::nullopt; } current_command_ = std::move(pending_commands_.front()); pending_commands_.pop_front(); return current_command_; } void GatewayRuntime::completeCurrentCommand() { LockGuard guard(command_lock_); current_command_.reset(); } bool GatewayRuntime::hasPendingQueryCommand(const std::vector& command) const { LockGuard guard(command_lock_); const auto command_key = queryCommandKey(command); if (!command_key.has_value()) { return false; } if (current_command_.has_value() && queryCommandKey(*current_command_) == command_key) { return true; } return std::any_of(pending_commands_.begin(), pending_commands_.end(), [&](const std::vector& pending) { return queryCommandKey(pending) == command_key; }); } GatewayRuntime::CommandDropReason GatewayRuntime::lastEnqueueDropReason() const { LockGuard guard(command_lock_); return last_enqueue_drop_reason_; } void GatewayRuntime::setGatewayCount(size_t gateway_count) { gateway_count_ = gateway_count; } void GatewayRuntime::setWirelessInfo(WirelessInfo info) { if (!info.ssid.empty() || !info.password.empty()) { settings_.setWifiCredentials(info.ssid, info.password); } wireless_info_ = std::move(info); } void GatewayRuntime::setCommandAddressResolver( std::function resolver) { LockGuard guard(command_lock_); if (resolver) { command_address_resolver_ = std::move(resolver); return; } command_address_resolver_ = [](uint8_t, uint8_t raw_addr) { return raw_addr; }; } GatewayDeviceInfo GatewayRuntime::deviceInfo() const { GatewayDeviceInfo info; info.serial_id = config_.serial_id; info.type = GatewayCore::RoleToString(profile_.role); info.project = std::string(config_.project_name); info.version = std::string(config_.version); info.dali_gateway_count = gateway_count_; info.ble_enabled = ble_enabled_; info.wlan = wireless_info_; return info; } bool GatewayRuntime::bleEnabled() const { return ble_enabled_; } bool GatewayRuntime::setBleEnabled(bool enabled) { if (!settings_.setBleEnabled(enabled)) { return false; } ble_enabled_ = enabled; return true; } std::string GatewayRuntime::gatewayName(uint8_t gateway_id) const { return settings_.getGatewayName(gateway_id, defaultGatewayName(gateway_id)); } bool GatewayRuntime::setGatewayName(uint8_t gateway_id, std::string_view name) { std::string normalized(name); if (normalized.size() > kMaxGatewayNameBytes) { normalized.resize(kMaxGatewayNameBytes); } if (normalized.empty()) { normalized = defaultGatewayName(gateway_id); } return settings_.setGatewayName(gateway_id, normalized); } std::string GatewayRuntime::gatewaySerialHex(uint8_t gateway_id) const { const auto bytes = serialBytes(); std::vector serial(6, 0); for (size_t i = 0; i < 5; ++i) { serial[i] = i + 1 < bytes.size() ? bytes[i + 1] : 0; } serial[5] = gateway_id; return toHex(serial); } std::string GatewayRuntime::bleMacHex() const { return toHex(serialBytes()); } std::string GatewayRuntime::bleGatewayName(uint8_t gateway_id, std::string_view gateway_name) const { std::string normalized(gateway_name); if (normalized.size() > kMaxGatewayNameBytes) { normalized.resize(kMaxGatewayNameBytes); } if (!normalized.empty() && normalized != defaultGatewayName(gateway_id)) { return normalized; } return defaultBleGatewayName(); } std::string GatewayRuntime::defaultBleGatewayName() const { return "DALIGW_" + bleMacHex(); } bool GatewayRuntime::isQueryCommand(const std::vector& command) const { return command.size() >= 6 && isGatewayCommandFrame(command) && command[3] >= 0x14 && command[3] <= 0x16; } std::optional GatewayRuntime::queryCommandKey( const std::vector& command) const { if (!isQueryCommand(command)) { return std::nullopt; } const auto gw = command[2]; const auto cmd = command[3]; if (cmd == 0x16) { char key[16] = {0}; std::snprintf(key, sizeof(key), "%u:%u", gw, cmd); return std::string(key); } const auto target_addr = command_address_resolver_(gw, command[4]); char key[32] = {0}; std::snprintf(key, sizeof(key), "%u:%u:%u:%u", gw, cmd, target_addr, command[5]); return std::string(key); } std::string GatewayRuntime::defaultGatewayName(uint8_t) const { const std::string serial_hex = bleMacHex(); if (serial_hex.size() <= 6) { return "DALIGW_" + serial_hex; } return "DALIGW_" + serial_hex.substr(serial_hex.size() - 6); } std::vector GatewayRuntime::serialBytes() const { std::vector bytes; const std::string& serial = config_.serial_id; bytes.reserve(6); for (size_t i = 0; i + 1 < serial.size() && bytes.size() < 6; i += 2) { if (!std::isxdigit(static_cast(serial[i])) || !std::isxdigit(static_cast(serial[i + 1]))) { break; } char pair[3] = {serial[i], serial[i + 1], 0}; bytes.push_back(static_cast(std::strtoul(pair, nullptr, 16))); } while (bytes.size() < 6) { bytes.push_back(0); } return bytes; } std::string GatewayRuntime::toHex(const std::vector& bytes) { static constexpr char kHex[] = "0123456789ABCDEF"; std::string out; out.reserve(bytes.size() * 2); for (const auto byte : bytes) { out.push_back(kHex[(byte >> 4) & 0x0F]); out.push_back(kHex[byte & 0x0F]); } return out; } } // namespace gateway