#include "ConfigManager.hpp" #include "../../src/Logging/Logging.hpp" #include // For MAC address generation #include // For timestamp generation #include // For std::sort // NVS namespace for device identity storage const char* ConfigManager::NVS_NAMESPACE = "device_id"; // NVS keys for device identity static const char* NVS_DEVICE_UID_KEY = "device_uid"; static const char* NVS_HW_TYPE_KEY = "hw_type"; static const char* NVS_HW_VERSION_KEY = "hw_version"; ConfigManager::ConfigManager() { // Initialize with empty defaults - everything will be loaded/generated in begin() createDefaultBellConfig(); } void ConfigManager::initializeCleanDefaults() { // This method is called after NVS loading to set up clean defaults // and auto-generate identifiers from loaded deviceUID // Generate network identifiers from deviceUID generateNetworkIdentifiers(); // Set MQTT user to deviceUID for unique identification mqttConfig.user = deviceConfig.deviceUID; LOG_DEBUG("ConfigManager - Clean defaults initialized with auto-generated identifiers"); } void ConfigManager::generateNetworkIdentifiers() { networkConfig.hostname = "BellSystems-" + deviceConfig.deviceUID; networkConfig.apSsid = "BellSystems-Setup-" + deviceConfig.deviceUID; LOG_DEBUG("ConfigManager - Generated hostname: %s, AP SSID: %s", networkConfig.hostname.c_str(), networkConfig.apSsid.c_str()); } void ConfigManager::createDefaultBellConfig() { // Initialize default durations (90ms for all bells) for (uint8_t i = 0; i < 16; i++) { bellConfig.durations[i] = 90; bellConfig.outputs[i] = i; // 0-indexed mapping } } bool ConfigManager::begin() { LOG_INFO("ConfigManager - ✅ Initializing..."); // Step 1: Initialize NVS for device identity (factory-set, permanent) if (!initializeNVS()) { LOG_ERROR("ConfigManager - ❌ NVS initialization failed, using empty defaults"); } else { // Load device identity from NVS (deviceUID, hwType, hwVersion) loadDeviceIdentityFromNVS(); } // Step 2: Initialize clean defaults and auto-generate identifiers initializeCleanDefaults(); // Step 3: Initialize SD card for user-configurable settings if (!ensureSDCard()) { LOG_ERROR("ConfigManager - ❌ SD Card initialization failed, using defaults"); return false; } // Step 5: Load update servers list if (!loadUpdateServers()) { LOG_WARNING("ConfigManager - ⚠️ Could not load update servers - using fallback only"); } // Load network config, save defaults if not found if (!loadNetworkConfig()) { LOG_WARNING("ConfigManager - ⚠️ Creating default network config file"); saveNetworkConfig(); } // Load time config, save defaults if not found if (!loadTimeConfig()) { LOG_WARNING("ConfigManager - ⚠️ Creating default time config file (GMT+2)"); saveTimeConfig(); } // Load bell durations, save defaults if not found if (!loadBellDurations()) { LOG_WARNING("ConfigManager - ⚠️ Creating default bell durations file"); saveBellDurations(); } // Load bell outputs, save defaults if not found if (!loadBellOutputs()) { LOG_WARNING("ConfigManager - ⚠️ Creating default bell outputs file"); saveBellOutputs(); } // Load clock config, save defaults if not found if (!loadClockConfig()) { LOG_WARNING("ConfigManager - ⚠️ Creating default clock config file"); saveClockConfig(); } // Load clock state, save defaults if not found if (!loadClockState()) { LOG_WARNING("ConfigManager - ⚠️ Creating default clock state file"); saveClockState(); } if (!loadGeneralConfig()) { LOG_WARNING("ConfigManager - ⚠️ Creating default general config file"); saveGeneralConfig(); } LOG_INFO("ConfigManager - ✅ Initialization Complete ! UID: %s, Hostname: %s", deviceConfig.deviceUID.c_str(), networkConfig.hostname.c_str()); return true; } // ════════════════════════════════════════════════════════════════════════════ // NVS (NON-VOLATILE STORAGE) IMPLEMENTATION // ════════════════════════════════════════════════════════════════════════════ bool ConfigManager::initializeNVS() { esp_err_t err = nvs_flash_init(); if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { LOG_WARNING("ConfigManager - ⚠️ NVS partition truncated, erasing..."); ESP_ERROR_CHECK(nvs_flash_erase()); err = nvs_flash_init(); } if (err != ESP_OK) { LOG_ERROR("ConfigManager - ❌ Failed to initialize NVS flash: %s", esp_err_to_name(err)); return false; } err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvsHandle); if (err != ESP_OK) { LOG_ERROR("ConfigManager - ❌ Failed to open NVS handle: %s", esp_err_to_name(err)); return false; } LOG_DEBUG("ConfigManager - NVS initialized successfully"); return true; } bool ConfigManager::loadDeviceIdentityFromNVS() { if (nvsHandle == 0) { LOG_ERROR("ConfigManager - ❌ NVS not initialized, cannot load device identity"); return false; } // Read factory-set device identity from NVS (READ-ONLY) deviceConfig.deviceUID = readNVSString(NVS_DEVICE_UID_KEY, ""); deviceConfig.hwType = readNVSString(NVS_HW_TYPE_KEY, ""); deviceConfig.hwVersion = readNVSString(NVS_HW_VERSION_KEY, ""); // Validate that factory identity exists if (deviceConfig.deviceUID.isEmpty() || deviceConfig.hwType.isEmpty() || deviceConfig.hwVersion.isEmpty()) { LOG_ERROR("═══════════════════════════════════════════════════════════════════════════"); LOG_ERROR(" ⚠️ CRITICAL: DEVICE IDENTITY NOT FOUND IN NVS"); LOG_ERROR(" ⚠️ This device has NOT been factory-programmed!"); LOG_ERROR(" ⚠️ Please flash factory firmware to set device identity"); LOG_ERROR("═══════════════════════════════════════════════════════════════════════════"); return false; } LOG_INFO("═══════════════════════════════════════════════════════════════════════════"); LOG_INFO(" 🏭 FACTORY DEVICE IDENTITY LOADED FROM NVS (READ-ONLY)"); LOG_INFO(" 🆔 Device UID: %s", deviceConfig.deviceUID.c_str()); LOG_INFO(" 🔧 Hardware Type: %s", deviceConfig.hwType.c_str()); LOG_INFO(" 📐 Hardware Version: %s", deviceConfig.hwVersion.c_str()); LOG_INFO(" 🔒 These values are PERMANENT and cannot be changed by production firmware"); LOG_INFO("═══════════════════════════════════════════════════════════════════════════"); return true; } // REMOVED: saveDeviceIdentityToNVS() - Production firmware MUST NOT write device identity // Device identity (UID, hwType, hwVersion) is factory-set ONLY and stored in NVS by factory firmware // Production firmware reads these values once at boot and keeps them in RAM String ConfigManager::readNVSString(const char* key, const String& defaultValue) { if (nvsHandle == 0) { LOG_ERROR("ConfigManager - ❌ NVS not initialized, returning default for key: %s", key); return defaultValue; } size_t required_size = 0; esp_err_t err = nvs_get_str(nvsHandle, key, NULL, &required_size); if (err == ESP_ERR_NVS_NOT_FOUND) { LOG_DEBUG("ConfigManager - NVS key '%s' not found, using default: %s", key, defaultValue.c_str()); return defaultValue; } if (err != ESP_OK) { LOG_ERROR("ConfigManager - ❌ Error reading NVS key '%s': %s", key, esp_err_to_name(err)); return defaultValue; } char* buffer = new char[required_size]; err = nvs_get_str(nvsHandle, key, buffer, &required_size); if (err != ESP_OK) { LOG_ERROR("ConfigManager - ❌ Error reading NVS value for key '%s': %s", key, esp_err_to_name(err)); delete[] buffer; return defaultValue; } String result = String(buffer); delete[] buffer; LOG_VERBOSE("ConfigManager - Read NVS key '%s': %s", key, result.c_str()); return result; } // ════════════════════════════════════════════════════════════════════════════ // STANDARD SD CARD FUNCTIONALITY // ════════════════════════════════════════════════════════════════════════════ bool ConfigManager::ensureSDCard() { if (!sdInitialized) { sdInitialized = SD.begin(hardwareConfig.sdChipSelect); if (!sdInitialized) { LOG_ERROR("ConfigManager - ❌ SD Card not available"); } } return sdInitialized; } bool ConfigManager::saveToSD() { if (!ensureSDCard()) { return false; } bool success = true; success &= saveBellDurations(); success &= saveClockConfig(); success &= saveClockState(); return success; } // Device configuration now only handles firmware version (identity is in NVS) bool ConfigManager::saveDeviceConfig() { if (!ensureSDCard()) { return false; } StaticJsonDocument<256> doc; doc["fwVersion"] = deviceConfig.fwVersion; char buffer[256]; size_t len = serializeJson(doc, buffer, sizeof(buffer)); if (len == 0 || len >= sizeof(buffer)) { LOG_ERROR("ConfigManager - ❌ Failed to serialize device config JSON"); return false; } saveFileToSD("/settings", "deviceConfig.json", buffer); LOG_DEBUG("ConfigManager - Device config saved - FwVer: %s", deviceConfig.fwVersion.c_str()); return true; } bool ConfigManager::loadDeviceConfig() { if (!ensureSDCard()) return false; File file = SD.open("/settings/deviceConfig.json", FILE_READ); if (!file) { LOG_WARNING("ConfigManager - ⚠️ Device config file not found - using firmware version default"); return false; } StaticJsonDocument<512> doc; DeserializationError error = deserializeJson(doc, file); file.close(); if (error) { LOG_ERROR("ConfigManager - ❌ Failed to parse device config from SD: %s", error.c_str()); return false; } if (doc.containsKey("fwVersion")) { deviceConfig.fwVersion = doc["fwVersion"].as(); LOG_VERBOSE("ConfigManager - Firmware version loaded from SD: %s", deviceConfig.fwVersion.c_str()); } return true; } bool ConfigManager::isHealthy() const { if (!sdInitialized) { LOG_VERBOSE("ConfigManager - ⚠️ Unhealthy - SD card not initialized"); return false; } if (deviceConfig.deviceUID.isEmpty()) { LOG_VERBOSE("ConfigManager - ⚠️ Unhealthy - Device UID not set (factory configuration required)"); return false; } if (deviceConfig.hwType.isEmpty()) { LOG_VERBOSE("ConfigManager - ⚠️ Unhealthy - Hardware type not set (factory configuration required)"); return false; } if (networkConfig.hostname.isEmpty()) { LOG_VERBOSE("ConfigManager - ⚠️ Unhealthy - Hostname not generated (initialization issue)"); return false; } // Note: WiFi credentials are handled by WiFiManager, not checked here return true; } // Bell configuration methods remain unchanged... bool ConfigManager::loadBellDurations() { if (!ensureSDCard()) { return false; } File file = SD.open("/settings/relayTimings.json", FILE_READ); if (!file) { LOG_WARNING("ConfigManager - ⚠️ Settings file not found on SD. Using default bell durations."); return false; } StaticJsonDocument<512> doc; DeserializationError error = deserializeJson(doc, file); file.close(); if (error) { LOG_ERROR("ConfigManager - ❌ Failed to parse settings from SD. Using default bell durations."); return false; } for (uint8_t i = 0; i < 16; i++) { String key = String("b") + (i + 1); if (doc.containsKey(key)) { bellConfig.durations[i] = doc[key].as(); } } LOG_DEBUG("ConfigManager - Bell durations loaded from SD"); return true; } bool ConfigManager::saveBellDurations() { if (!ensureSDCard()) return false; StaticJsonDocument<512> doc; for (uint8_t i = 0; i < 16; i++) { String key = String("b") + (i + 1); doc[key] = bellConfig.durations[i]; } char buffer[512]; size_t len = serializeJson(doc, buffer, sizeof(buffer)); if (len == 0 || len >= sizeof(buffer)) { LOG_ERROR("ConfigManager - ❌ Failed to serialize bell durations JSON"); return false; } saveFileToSD("/settings", "relayTimings.json", buffer); LOG_DEBUG("ConfigManager - Bell durations saved to SD"); return true; } bool ConfigManager::loadBellOutputs() { if (!ensureSDCard()) { return false; } File file = SD.open("/settings/bellOutputs.json", FILE_READ); if (!file) { LOG_WARNING("ConfigManager - ⚠️ Bell outputs file not found on SD. Using default 1:1 mapping."); return false; } StaticJsonDocument<512> doc; DeserializationError error = deserializeJson(doc, file); file.close(); if (error) { LOG_ERROR("ConfigManager - ❌ Failed to parse bell outputs from SD. Using defaults."); return false; } for (uint8_t i = 0; i < 16; i++) { String key = String("b") + (i + 1); if (doc.containsKey(key)) { bellConfig.outputs[i] = doc[key].as(); // Already 0-indexed in file } } LOG_DEBUG("ConfigManager - Bell outputs loaded from SD"); return true; } bool ConfigManager::saveBellOutputs() { if (!ensureSDCard()) return false; StaticJsonDocument<512> doc; for (uint8_t i = 0; i < 16; i++) { String key = String("b") + (i + 1); doc[key] = bellConfig.outputs[i]; // Save 0-indexed outputs } char buffer[512]; size_t len = serializeJson(doc, buffer, sizeof(buffer)); if (len == 0 || len >= sizeof(buffer)) { LOG_ERROR("ConfigManager - ❌ Failed to serialize bell outputs JSON"); return false; } saveFileToSD("/settings", "bellOutputs.json", buffer); LOG_DEBUG("ConfigManager - Bell outputs saved to SD"); return true; } void ConfigManager::updateBellDurations(JsonVariant doc) { for (uint8_t i = 0; i < 16; i++) { String key = String("b") + (i + 1); if (doc.containsKey(key)) { bellConfig.durations[i] = doc[key].as(); } } LOG_DEBUG("ConfigManager - Updated bell durations"); } void ConfigManager::updateBellOutputs(JsonVariant doc) { for (uint8_t i = 0; i < 16; i++) { String key = String("b") + (i + 1); if (doc.containsKey(key)) { bellConfig.outputs[i] = doc[key].as() - 1; LOG_VERBOSE("ConfigManager - Bell %d output set to %d", i + 1, bellConfig.outputs[i]); } } LOG_DEBUG("ConfigManager - Updated bell outputs"); } uint16_t ConfigManager::getBellDuration(uint8_t bellIndex) const { if (bellIndex >= 16) return 90; return bellConfig.durations[bellIndex]; } uint16_t ConfigManager::getBellOutput(uint8_t bellIndex) const { if (bellIndex >= 16) return bellIndex; return bellConfig.outputs[bellIndex]; } void ConfigManager::setBellDuration(uint8_t bellIndex, uint16_t duration) { if (bellIndex < 16) { bellConfig.durations[bellIndex] = duration; } } void ConfigManager::setBellOutput(uint8_t bellIndex, uint16_t output) { if (bellIndex < 16) { bellConfig.outputs[bellIndex] = output; } } void ConfigManager::saveFileToSD(const char* dirPath, const char* filename, const char* data) { if (!ensureSDCard()) { return; } if (!SD.exists(dirPath)) { SD.mkdir(dirPath); } String fullPath = String(dirPath); if (!fullPath.endsWith("/")) fullPath += "/"; fullPath += filename; File file = SD.open(fullPath.c_str(), FILE_WRITE); if (!file) { LOG_ERROR("ConfigManager - ❌ Failed to open file: %s", fullPath.c_str()); return; } file.print(data); file.close(); LOG_VERBOSE("ConfigManager - File %s saved successfully", fullPath.c_str()); } // Clock configuration methods and other remaining methods follow the same pattern... // (Implementation would continue with all the clock config methods, update servers, etc.) // ════════════════════════════════════════════════════════════════════════════ // MISSING CLOCK CONFIGURATION METHODS // ════════════════════════════════════════════════════════════════════════════ void ConfigManager::updateClockOutputs(JsonVariant doc) { if (doc.containsKey("c1")) { clockConfig.c1output = doc["c1"].as(); } if (doc.containsKey("c2")) { clockConfig.c2output = doc["c2"].as(); } if (doc.containsKey("pulseDuration")) { clockConfig.pulseDuration = doc["pulseDuration"].as(); } if (doc.containsKey("pauseDuration")) { clockConfig.pauseDuration = doc["pauseDuration"].as(); } LOG_DEBUG("ConfigManager - Updated Clock outputs to: C1: %d / C2: %d, Pulse: %dms, Pause: %dms", clockConfig.c1output, clockConfig.c2output, clockConfig.pulseDuration, clockConfig.pauseDuration); } void ConfigManager::updateClockAlerts(JsonVariant doc) { if (doc.containsKey("alertType")) { clockConfig.alertType = doc["alertType"].as(); } if (doc.containsKey("alertRingInterval")) { clockConfig.alertRingInterval = doc["alertRingInterval"].as(); } if (doc.containsKey("hourBell")) { clockConfig.hourBell = doc["hourBell"].as(); } if (doc.containsKey("halfBell")) { clockConfig.halfBell = doc["halfBell"].as(); } if (doc.containsKey("quarterBell")) { clockConfig.quarterBell = doc["quarterBell"].as(); } LOG_DEBUG("ConfigManager - Updated Clock alerts"); } void ConfigManager::updateClockBacklight(JsonVariant doc) { if (doc.containsKey("enabled")) { clockConfig.backlight = doc["enabled"].as(); } if (doc.containsKey("output")) { clockConfig.backlightOutput = doc["output"].as(); } if (doc.containsKey("onTime")) { clockConfig.backlightOnTime = doc["onTime"].as(); } if (doc.containsKey("offTime")) { clockConfig.backlightOffTime = doc["offTime"].as(); } LOG_DEBUG("ConfigManager - Updated Clock backlight"); } void ConfigManager::updateClockSilence(JsonVariant doc) { if (doc.containsKey("daytime")) { JsonObject daytime = doc["daytime"]; if (daytime.containsKey("enabled")) { clockConfig.daytimeSilenceEnabled = daytime["enabled"].as(); } if (daytime.containsKey("onTime")) { clockConfig.daytimeSilenceOnTime = daytime["onTime"].as(); } if (daytime.containsKey("offTime")) { clockConfig.daytimeSilenceOffTime = daytime["offTime"].as(); } } if (doc.containsKey("nighttime")) { JsonObject nighttime = doc["nighttime"]; if (nighttime.containsKey("enabled")) { clockConfig.nighttimeSilenceEnabled = nighttime["enabled"].as(); } if (nighttime.containsKey("onTime")) { clockConfig.nighttimeSilenceOnTime = nighttime["onTime"].as(); } if (nighttime.containsKey("offTime")) { clockConfig.nighttimeSilenceOffTime = nighttime["offTime"].as(); } } LOG_DEBUG("ConfigManager - Updated Clock silence"); } bool ConfigManager::loadClockConfig() { if (!ensureSDCard()) return false; File file = SD.open("/settings/clockConfig.json", FILE_READ); if (!file) { LOG_WARNING("ConfigManager - ⚠️ Clock config file not found - using defaults"); return false; } StaticJsonDocument<512> doc; DeserializationError error = deserializeJson(doc, file); file.close(); if (error) { LOG_ERROR("ConfigManager - ❌ Failed to parse clock config from SD: %s", error.c_str()); return false; } if (doc.containsKey("enabled")) clockConfig.enabled = doc["enabled"].as(); if (doc.containsKey("c1output")) clockConfig.c1output = doc["c1output"].as(); if (doc.containsKey("c2output")) clockConfig.c2output = doc["c2output"].as(); if (doc.containsKey("pulseDuration")) clockConfig.pulseDuration = doc["pulseDuration"].as(); if (doc.containsKey("pauseDuration")) clockConfig.pauseDuration = doc["pauseDuration"].as(); LOG_DEBUG("ConfigManager - Clock config loaded"); return true; } bool ConfigManager::saveClockConfig() { if (!ensureSDCard()) return false; StaticJsonDocument<512> doc; doc["enabled"] = clockConfig.enabled; doc["c1output"] = clockConfig.c1output; doc["c2output"] = clockConfig.c2output; doc["pulseDuration"] = clockConfig.pulseDuration; doc["pauseDuration"] = clockConfig.pauseDuration; char buffer[512]; size_t len = serializeJson(doc, buffer, sizeof(buffer)); if (len == 0 || len >= sizeof(buffer)) { LOG_ERROR("ConfigManager - ❌ Failed to serialize clock config JSON"); return false; } saveFileToSD("/settings", "clockConfig.json", buffer); LOG_DEBUG("ConfigManager - Clock config saved"); return true; } bool ConfigManager::loadClockState() { if (!ensureSDCard()) return false; File file = SD.open("/settings/clockState.json", FILE_READ); if (!file) { LOG_WARNING("ConfigManager - ⚠️ Clock state file not found - using defaults"); clockConfig.physicalHour = 0; clockConfig.physicalMinute = 0; clockConfig.nextOutputIsC1 = true; clockConfig.lastSyncTime = 0; return false; } StaticJsonDocument<256> doc; DeserializationError error = deserializeJson(doc, file); file.close(); if (error) { LOG_ERROR("ConfigManager - ❌ Failed to parse clock state from SD: %s", error.c_str()); return false; } clockConfig.physicalHour = doc["hour"].as() % 12; clockConfig.physicalMinute = doc["minute"].as() % 60; clockConfig.nextOutputIsC1 = doc["nextIsC1"].as(); clockConfig.lastSyncTime = doc["lastSyncTime"].as(); LOG_DEBUG("ConfigManager - Clock state loaded"); return true; } bool ConfigManager::saveClockState() { if (!ensureSDCard()) return false; StaticJsonDocument<256> doc; doc["hour"] = clockConfig.physicalHour; doc["minute"] = clockConfig.physicalMinute; doc["nextIsC1"] = clockConfig.nextOutputIsC1; doc["lastSyncTime"] = clockConfig.lastSyncTime; char buffer[256]; size_t len = serializeJson(doc, buffer, sizeof(buffer)); if (len == 0 || len >= sizeof(buffer)) { LOG_ERROR("ConfigManager - ❌ Failed to serialize clock state JSON"); return false; } saveFileToSD("/settings", "clockState.json", buffer); LOG_VERBOSE("ConfigManager - Clock state saved"); return true; } bool ConfigManager::loadUpdateServers() { if (!ensureSDCard()) return false; File file = SD.open("/settings/updateServers.json", FILE_READ); if (!file) { LOG_DEBUG("ConfigManager - Update servers file not found - using fallback only"); return false; } StaticJsonDocument<1024> doc; DeserializationError error = deserializeJson(doc, file); file.close(); if (error) { LOG_ERROR("ConfigManager - ❌ Failed to parse update servers JSON: %s", error.c_str()); return false; } updateServers.clear(); if (doc.containsKey("servers") && doc["servers"].is()) { JsonArray serversArray = doc["servers"]; for (JsonObject server : serversArray) { if (server.containsKey("url")) { String url = server["url"].as(); if (!url.isEmpty()) { updateServers.push_back(url); } } } } LOG_DEBUG("ConfigManager - Loaded %d update servers from SD card", updateServers.size()); return true; } std::vector ConfigManager::getUpdateServers() const { std::vector servers; for (const String& server : updateServers) { servers.push_back(server); } servers.push_back(updateConfig.fallbackServerUrl); return servers; } void ConfigManager::updateTimeConfig(long gmtOffsetSec, int daylightOffsetSec) { timeConfig.gmtOffsetSec = gmtOffsetSec; timeConfig.daylightOffsetSec = daylightOffsetSec; saveTimeConfig(); // Save time config specifically LOG_DEBUG("ConfigManager - TimeConfig updated - GMT offset %ld sec, DST offset %d sec", gmtOffsetSec, daylightOffsetSec); } void ConfigManager::updateNetworkConfig(const String& hostname, bool useStaticIP, IPAddress ip, IPAddress gateway, IPAddress subnet, IPAddress dns1, IPAddress dns2) { networkConfig.hostname = hostname; networkConfig.useStaticIP = useStaticIP; networkConfig.ip = ip; networkConfig.gateway = gateway; networkConfig.subnet = subnet; networkConfig.dns1 = dns1; networkConfig.dns2 = dns2; saveNetworkConfig(); // Save immediately to SD LOG_DEBUG("ConfigManager - NetworkConfig updated - Hostname: %s, Static IP: %s, IP: %s", hostname.c_str(), useStaticIP ? "enabled" : "disabled", ip.toString().c_str()); } // ════════════════════════════════════════════════════════════════════════════ // NETWORK CONFIGURATION PERSISTENCE // ════════════════════════════════════════════════════════════════════════════ bool ConfigManager::loadNetworkConfig() { if (!ensureSDCard()) return false; File file = SD.open("/settings/networkConfig.json", FILE_READ); if (!file) { LOG_DEBUG("ConfigManager - Network config file not found - using auto-generated hostname and DHCP"); return false; } StaticJsonDocument<512> doc; DeserializationError error = deserializeJson(doc, file); file.close(); if (error) { LOG_ERROR("ConfigManager - ❌ Failed to parse network config from SD: %s", error.c_str()); return false; } // Load hostname if present (overrides auto-generated) if (doc.containsKey("hostname")) { String customHostname = doc["hostname"].as(); if (!customHostname.isEmpty()) { networkConfig.hostname = customHostname; LOG_DEBUG("ConfigManager - Custom hostname loaded from SD: %s", customHostname.c_str()); } } // Load static IP configuration if (doc.containsKey("useStaticIP")) { networkConfig.useStaticIP = doc["useStaticIP"].as(); } if (doc.containsKey("ip")) { String ipStr = doc["ip"].as(); if (!ipStr.isEmpty() && ipStr != "0.0.0.0") { networkConfig.ip.fromString(ipStr); } } if (doc.containsKey("gateway")) { String gwStr = doc["gateway"].as(); if (!gwStr.isEmpty() && gwStr != "0.0.0.0") { networkConfig.gateway.fromString(gwStr); } } if (doc.containsKey("subnet")) { String subnetStr = doc["subnet"].as(); if (!subnetStr.isEmpty() && subnetStr != "0.0.0.0") { networkConfig.subnet.fromString(subnetStr); } } if (doc.containsKey("dns1")) { String dns1Str = doc["dns1"].as(); if (!dns1Str.isEmpty() && dns1Str != "0.0.0.0") { networkConfig.dns1.fromString(dns1Str); } } if (doc.containsKey("dns2")) { String dns2Str = doc["dns2"].as(); if (!dns2Str.isEmpty() && dns2Str != "0.0.0.0") { networkConfig.dns2.fromString(dns2Str); } } LOG_DEBUG("ConfigManager - Network config loaded - Hostname: %s, Static IP: %s", networkConfig.hostname.c_str(), networkConfig.useStaticIP ? "enabled" : "disabled"); return true; } bool ConfigManager::saveNetworkConfig() { if (!ensureSDCard()) return false; StaticJsonDocument<512> doc; // Save hostname (user can customize) doc["hostname"] = networkConfig.hostname; // Save static IP configuration doc["useStaticIP"] = networkConfig.useStaticIP; doc["ip"] = networkConfig.ip.toString(); doc["gateway"] = networkConfig.gateway.toString(); doc["subnet"] = networkConfig.subnet.toString(); doc["dns1"] = networkConfig.dns1.toString(); doc["dns2"] = networkConfig.dns2.toString(); char buffer[512]; size_t len = serializeJson(doc, buffer, sizeof(buffer)); if (len == 0 || len >= sizeof(buffer)) { LOG_ERROR("ConfigManager - ❌ Failed to serialize network config JSON"); return false; } saveFileToSD("/settings", "networkConfig.json", buffer); LOG_DEBUG("ConfigManager - Network config saved to SD"); return true; } // ════════════════════════════════════════════════════════════════════════════ // TIME CONFIGURATION PERSISTENCE // ════════════════════════════════════════════════════════════════════════════ bool ConfigManager::loadTimeConfig() { if (!ensureSDCard()) return false; File file = SD.open("/settings/timeConfig.json", FILE_READ); if (!file) { LOG_DEBUG("ConfigManager - Time config file not found - using defaults (GMT+2)"); return false; } StaticJsonDocument<256> doc; DeserializationError error = deserializeJson(doc, file); file.close(); if (error) { LOG_ERROR("ConfigManager - ❌ Failed to parse time config from SD: %s", error.c_str()); return false; } // Load NTP server if present if (doc.containsKey("ntpServer")) { String ntpServer = doc["ntpServer"].as(); if (!ntpServer.isEmpty()) { timeConfig.ntpServer = ntpServer; } } // Load GMT offset if (doc.containsKey("gmtOffsetSec")) { timeConfig.gmtOffsetSec = doc["gmtOffsetSec"].as(); } // Load daylight saving offset if (doc.containsKey("daylightOffsetSec")) { timeConfig.daylightOffsetSec = doc["daylightOffsetSec"].as(); } LOG_DEBUG("ConfigManager - Time config loaded - NTP: %s, GMT offset: %ld, DST offset: %d", timeConfig.ntpServer.c_str(), timeConfig.gmtOffsetSec, timeConfig.daylightOffsetSec); return true; } bool ConfigManager::saveTimeConfig() { if (!ensureSDCard()) return false; StaticJsonDocument<256> doc; // Save NTP server doc["ntpServer"] = timeConfig.ntpServer; // Save timezone offsets doc["gmtOffsetSec"] = timeConfig.gmtOffsetSec; doc["daylightOffsetSec"] = timeConfig.daylightOffsetSec; char buffer[256]; size_t len = serializeJson(doc, buffer, sizeof(buffer)); if (len == 0 || len >= sizeof(buffer)) { LOG_ERROR("ConfigManager - ❌ Failed to serialize time config JSON"); return false; } saveFileToSD("/settings", "timeConfig.json", buffer); LOG_DEBUG("ConfigManager - Time config saved to SD"); return true; } // ═══════════════════════════════════════════════════════════════════════════════ // SETTINGS RESET IMPLEMENTATION // ═══════════════════════════════════════════════════════════════════════════════ bool ConfigManager::resetAllToDefaults() { LOG_INFO("═══════════════════════════════════════════════════════════════════════════"); LOG_INFO(" 🏭 RESET SETTINGS TO DEFAULTS INITIATED"); LOG_INFO("═══════════════════════════════════════════════════════════════════════════"); if (!ensureSDCard()) { return false; } bool allDeleted = true; int filesDeleted = 0; int filesFailed = 0; // ════════════════════════════════════════════════════════════════════════════ // STEP 1: Delete all configuration files // ════════════════════════════════════════════════════════════════════════════ const char* settingsFiles[] = { "/settings/deviceConfig.json", "/settings/networkConfig.json", "/settings/timeConfig.json", "/settings/relayTimings.json", "/settings/bellOutputs.json", "/settings/clockConfig.json", "/settings/clockState.json", "/settings/updateServers.json" }; int numFiles = sizeof(settingsFiles) / sizeof(settingsFiles[0]); LOG_DEBUG("ConfigManager - Step 1: Deleting %d configuration files...", numFiles); for (int i = 0; i < numFiles; i++) { const char* filepath = settingsFiles[i]; if (SD.exists(filepath)) { if (SD.remove(filepath)) { LOG_VERBOSE("ConfigManager - ✅ Deleted: %s", filepath); filesDeleted++; } else { LOG_ERROR("ConfigManager - ❌ Failed to delete: %s", filepath); filesFailed++; allDeleted = false; } } else { LOG_VERBOSE("ConfigManager - Skip (not found): %s", filepath); } } // ════════════════════════════════════════════════════════════════════════════ // STEP 2: Delete all melodies recursively // ════════════════════════════════════════════════════════════════════════════ if (SD.exists("/melodies")) { LOG_DEBUG("ConfigManager - Step 2: Deleting melody files..."); File melodiesDir = SD.open("/melodies"); if (melodiesDir && melodiesDir.isDirectory()) { int melodiesDeleted = 0; int melodiesFailed = 0; File entry = melodiesDir.openNextFile(); while (entry) { String entryPath = String("/melodies/") + entry.name(); if (!entry.isDirectory()) { if (SD.remove(entryPath.c_str())) { LOG_VERBOSE("ConfigManager - ✅ Deleted melody: %s", entryPath.c_str()); melodiesDeleted++; } else { LOG_ERROR("ConfigManager - ❌ Failed to delete melody: %s", entryPath.c_str()); melodiesFailed++; allDeleted = false; } } entry.close(); entry = melodiesDir.openNextFile(); } melodiesDir.close(); // Try to remove the empty directory if (SD.rmdir("/melodies")) { LOG_VERBOSE("ConfigManager - ✅ Deleted /melodies directory"); } else { LOG_WARNING("ConfigManager - ⚠️ Could not delete /melodies directory (may not be empty)"); } LOG_DEBUG("ConfigManager - Melodies deleted: %d, failed: %d", melodiesDeleted, melodiesFailed); filesDeleted += melodiesDeleted; filesFailed += melodiesFailed; } } else { LOG_VERBOSE("ConfigManager - /melodies directory not found"); } // ════════════════════════════════════════════════════════════════════════════ // SUMMARY // ════════════════════════════════════════════════════════════════════════════ LOG_INFO("═══════════════════════════════════════════════════════════════════════════"); LOG_INFO("ConfigManager - Full reset summary:"); LOG_INFO("ConfigManager - ✅ Files deleted: %d", filesDeleted); LOG_INFO("ConfigManager - ❌ Files failed: %d", filesFailed); LOG_INFO("ConfigManager - 🔄 Total processed: %d", filesDeleted + filesFailed); LOG_INFO("═══════════════════════════════════════════════════════════════════════════"); LOG_INFO("ConfigManager - ✅ RESET TO DEFAULT COMPLETE"); LOG_INFO("ConfigManager - 🔄 Device will boot with default settings on next restart"); LOG_INFO("ConfigManager - 🆔 Device identity (UID) preserved"); return allDeleted; } // ═══════════════════════════════════════════════════════════════════════════════ // GET ALL SETTINGS AS JSON // ═══════════════════════════════════════════════════════════════════════════════ String ConfigManager::getAllSettingsAsJson() const { // Use a large document to hold everything DynamicJsonDocument doc(4096); // Device info JsonObject device = doc.createNestedObject("device"); device["uid"] = deviceConfig.deviceUID; device["hwType"] = deviceConfig.hwType; device["hwVersion"] = deviceConfig.hwVersion; device["fwVersion"] = deviceConfig.fwVersion; // Network config JsonObject network = doc.createNestedObject("network"); network["hostname"] = networkConfig.hostname; network["useStaticIP"] = networkConfig.useStaticIP; network["ip"] = networkConfig.ip.toString(); network["gateway"] = networkConfig.gateway.toString(); network["subnet"] = networkConfig.subnet.toString(); network["dns1"] = networkConfig.dns1.toString(); network["dns2"] = networkConfig.dns2.toString(); // Time config JsonObject time = doc.createNestedObject("time"); time["ntpServer"] = timeConfig.ntpServer; time["gmtOffsetSec"] = timeConfig.gmtOffsetSec; time["daylightOffsetSec"] = timeConfig.daylightOffsetSec; // Bell durations (relay timings) JsonObject bells = doc.createNestedObject("bells"); for (uint8_t i = 0; i < 16; i++) { String key = String("b") + (i + 1); bells[key] = bellConfig.durations[i]; } // Clock configuration JsonObject clock = doc.createNestedObject("clock"); clock["enabled"] = clockConfig.enabled; clock["c1output"] = clockConfig.c1output; clock["c2output"] = clockConfig.c2output; clock["pulseDuration"] = clockConfig.pulseDuration; clock["pauseDuration"] = clockConfig.pauseDuration; // Clock state JsonObject clockState = doc.createNestedObject("clockState"); clockState["physicalHour"] = clockConfig.physicalHour; clockState["physicalMinute"] = clockConfig.physicalMinute; clockState["nextOutputIsC1"] = clockConfig.nextOutputIsC1; clockState["lastSyncTime"] = clockConfig.lastSyncTime; // Clock alerts JsonObject alerts = doc.createNestedObject("alerts"); alerts["alertType"] = clockConfig.alertType; alerts["alertRingInterval"] = clockConfig.alertRingInterval; alerts["hourBell"] = clockConfig.hourBell; alerts["halfBell"] = clockConfig.halfBell; alerts["quarterBell"] = clockConfig.quarterBell; // Clock backlight JsonObject backlight = doc.createNestedObject("backlight"); backlight["enabled"] = clockConfig.backlight; backlight["output"] = clockConfig.backlightOutput; backlight["onTime"] = clockConfig.backlightOnTime; backlight["offTime"] = clockConfig.backlightOffTime; // Silence periods JsonObject silence = doc.createNestedObject("silence"); JsonObject daytime = silence.createNestedObject("daytime"); daytime["enabled"] = clockConfig.daytimeSilenceEnabled; daytime["onTime"] = clockConfig.daytimeSilenceOnTime; daytime["offTime"] = clockConfig.daytimeSilenceOffTime; JsonObject nighttime = silence.createNestedObject("nighttime"); nighttime["enabled"] = clockConfig.nighttimeSilenceEnabled; nighttime["onTime"] = clockConfig.nighttimeSilenceOnTime; nighttime["offTime"] = clockConfig.nighttimeSilenceOffTime; // Serialize to string String output; serializeJson(doc, output); return output; } // ═══════════════════════════════════════════════════════════════════════════════ // GENERAL CONFIGURATION - LOG LEVELS // ═══════════════════════════════════════════════════════════════════════════════ bool ConfigManager::setSerialLogLevel(uint8_t level) { if (level > 5) { // Max level is VERBOSE (5) LOG_WARNING("ConfigManager - ⚠️ Invalid serial log level %d, valid range is 0-5", level); return false; } generalConfig.serialLogLevel = level; LOG_DEBUG("ConfigManager - Serial log level set to %d", level); return true; } bool ConfigManager::setSdLogLevel(uint8_t level) { if (level > 5) { // Max level is VERBOSE (5) LOG_WARNING("ConfigManager - ⚠️ Invalid SD log level %d, valid range is 0-5", level); return false; } generalConfig.sdLogLevel = level; LOG_DEBUG("ConfigManager - SD log level set to %d", level); return true; } bool ConfigManager::setMqttLogLevel(uint8_t level) { if (level > 5) { // Max level is VERBOSE (5) LOG_WARNING("ConfigManager - ⚠️ Invalid MQTT log level %d, valid range is 0-5", level); return false; } generalConfig.mqttLogLevel = level; LOG_DEBUG("ConfigManager - MQTT log level set to %d", level); return true; } bool ConfigManager::loadGeneralConfig() { if (!ensureSDCard()) return false; File file = SD.open("/settings/generalConfig.json", FILE_READ); if (!file) { LOG_WARNING("ConfigManager - ⚠️ General config file not found - using defaults"); return false; } StaticJsonDocument<256> doc; DeserializationError error = deserializeJson(doc, file); file.close(); if (error) { LOG_ERROR("ConfigManager - ❌ Failed to parse general config from SD: %s", error.c_str()); return false; } if (doc.containsKey("serialLogLevel")) { generalConfig.serialLogLevel = doc["serialLogLevel"].as(); } if (doc.containsKey("sdLogLevel")) { generalConfig.sdLogLevel = doc["sdLogLevel"].as(); } if (doc.containsKey("mqttLogLevel")) { generalConfig.mqttLogLevel = doc["mqttLogLevel"].as(); } if (doc.containsKey("mqttEnabled")) { generalConfig.mqttEnabled = doc["mqttEnabled"].as(); mqttConfig.enabled = generalConfig.mqttEnabled; // Sync with mqttConfig } LOG_DEBUG("ConfigManager - General config loaded - Serial log level: %d, SD log level: %d, MQTT log level: %d, MQTT enabled: %s", generalConfig.serialLogLevel, generalConfig.sdLogLevel, generalConfig.mqttLogLevel, generalConfig.mqttEnabled ? "true" : "false"); return true; } bool ConfigManager::saveGeneralConfig() { if (!ensureSDCard()) return false; StaticJsonDocument<256> doc; doc["serialLogLevel"] = generalConfig.serialLogLevel; doc["sdLogLevel"] = generalConfig.sdLogLevel; doc["mqttLogLevel"] = generalConfig.mqttLogLevel; doc["mqttEnabled"] = generalConfig.mqttEnabled; char buffer[256]; size_t len = serializeJson(doc, buffer, sizeof(buffer)); if (len == 0 || len >= sizeof(buffer)) { LOG_ERROR("ConfigManager - ❌ Failed to serialize general config JSON"); return false; } saveFileToSD("/settings", "generalConfig.json", buffer); LOG_DEBUG("ConfigManager - General config saved (MQTT enabled: %s)", generalConfig.mqttEnabled ? "true" : "false"); return true; }