Complete Rebuild, with Subsystems for each component. RTOS Tasks. (help by Claude)

This commit is contained in:
2025-10-01 12:42:00 +03:00
parent 104c1d04d4
commit f696984cd1
57 changed files with 11757 additions and 2290 deletions

View File

@@ -0,0 +1,944 @@
#include "ConfigManager.hpp"
#include "../../src/Logging/Logging.hpp"
#include <WiFi.h> // For MAC address generation
#include <time.h> // For timestamp generation
#include <algorithm> // 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_INFO("ConfigManager: Clean defaults initialized with auto-generated identifiers");
}
void ConfigManager::generateNetworkIdentifiers() {
networkConfig.hostname = "BellSystems-" + deviceConfig.deviceUID;
networkConfig.apSsid = "BellSystems-Setup-" + deviceConfig.deviceUID;
LOG_INFO("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; // Direct mapping by default
}
}
bool ConfigManager::begin() {
LOG_INFO("ConfigManager: Starting clean deployment-ready initialization");
// 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 4: Load device configuration from SD card (firmware version only)
if (!loadDeviceConfig()) {
LOG_WARNING("ConfigManager: Could not load device config from SD card - using defaults");
}
// Step 5: Load update servers list
if (!loadUpdateServers()) {
LOG_WARNING("ConfigManager: Could not load update servers - using fallback only");
}
// Step 6: Load user-configurable settings from SD
loadFromSD();
loadNetworkConfig(); // Load network configuration (hostname, static IP settings)
loadBellDurations();
loadClockConfig(); // Load clock configuration (C1/C2 outputs, pulse durations)
loadClockState(); // Load physical clock state (hour, minute, position)
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_INFO("ConfigManager: NVS initialized successfully");
return true;
}
bool ConfigManager::loadDeviceIdentityFromNVS() {
if (nvsHandle == 0) {
LOG_ERROR("ConfigManager: NVS not initialized, cannot load device identity");
return false;
}
deviceConfig.deviceUID = readNVSString(NVS_DEVICE_UID_KEY, "PV000000000000");
deviceConfig.hwType = readNVSString(NVS_HW_TYPE_KEY, "BellSystems");
deviceConfig.hwVersion = readNVSString(NVS_HW_VERSION_KEY, "0");
LOG_INFO("ConfigManager: Device identity loaded from NVS - UID: %s, Type: %s, Version: %s",
deviceConfig.deviceUID.c_str(),
deviceConfig.hwType.c_str(),
deviceConfig.hwVersion.c_str());
return true;
}
bool ConfigManager::saveDeviceIdentityToNVS() {
if (nvsHandle == 0) {
LOG_ERROR("ConfigManager: NVS not initialized, cannot save device identity");
return false;
}
bool success = true;
success &= writeNVSString(NVS_DEVICE_UID_KEY, deviceConfig.deviceUID);
success &= writeNVSString(NVS_HW_TYPE_KEY, deviceConfig.hwType);
success &= writeNVSString(NVS_HW_VERSION_KEY, deviceConfig.hwVersion);
if (success) {
esp_err_t err = nvs_commit(nvsHandle);
if (err != ESP_OK) {
LOG_ERROR("ConfigManager: Failed to commit NVS changes: %s", esp_err_to_name(err));
return false;
}
LOG_INFO("ConfigManager: Device identity saved to NVS");
} else {
LOG_ERROR("ConfigManager: Failed to save device identity to NVS");
}
return success;
}
String ConfigManager::readNVSString(const char* key, const String& defaultValue) {
if (nvsHandle == 0) {
LOG_WARNING("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_DEBUG("ConfigManager: Read NVS key '%s': %s", key, result.c_str());
return result;
}
bool ConfigManager::writeNVSString(const char* key, const String& value) {
if (nvsHandle == 0) {
LOG_ERROR("ConfigManager: NVS not initialized, cannot write key: %s", key);
return false;
}
esp_err_t err = nvs_set_str(nvsHandle, key, value.c_str());
if (err != ESP_OK) {
LOG_ERROR("ConfigManager: Failed to write NVS key '%s': %s", key, esp_err_to_name(err));
return false;
}
LOG_DEBUG("ConfigManager: Written NVS key '%s': %s", key, value.c_str());
return true;
}
// ════════════════════════════════════════════════════════════════════════════
// STANDARD SD CARD FUNCTIONALITY
// ════════════════════════════════════════════════════════════════════════════
bool ConfigManager::ensureSDCard() {
if (!sdInitialized) {
sdInitialized = SD.begin(hardwareConfig.sdChipSelect);
}
return sdInitialized;
}
void ConfigManager::loadFromSD() {
if (!ensureSDCard()) {
LOG_ERROR("ConfigManager: Cannot load from SD - SD not available");
return;
}
LOG_INFO("ConfigManager: Using default configuration");
}
bool ConfigManager::saveToSD() {
if (!ensureSDCard()) {
LOG_ERROR("ConfigManager: Cannot save to SD - SD not available");
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()) {
LOG_ERROR("ConfigManager: Cannot save device config - SD not available");
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_INFO("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<String>();
LOG_INFO("ConfigManager: Firmware version loaded from SD: %s", deviceConfig.fwVersion.c_str());
}
return true;
}
bool ConfigManager::isHealthy() const {
if (!sdInitialized) {
LOG_DEBUG("ConfigManager: Unhealthy - SD card not initialized");
return false;
}
if (deviceConfig.deviceUID.isEmpty()) {
LOG_DEBUG("ConfigManager: Unhealthy - Device UID not set (factory configuration required)");
return false;
}
if (deviceConfig.hwType.isEmpty()) {
LOG_DEBUG("ConfigManager: Unhealthy - Hardware type not set (factory configuration required)");
return false;
}
if (networkConfig.hostname.isEmpty()) {
LOG_DEBUG("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()) {
LOG_ERROR("ConfigManager: SD Card not initialized. Using default bell durations.");
return false;
}
File file = SD.open("/settings/relayTimings.json", FILE_READ);
if (!file) {
LOG_ERROR("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<uint16_t>();
}
}
LOG_INFO("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_INFO("ConfigManager: Bell durations 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<uint16_t>();
}
}
LOG_INFO("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<uint16_t>() - 1;
}
}
LOG_INFO("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()) {
LOG_ERROR("ConfigManager: SD Card not initialized!");
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_INFO("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<uint8_t>();
}
if (doc.containsKey("c2")) {
clockConfig.c2output = doc["c2"].as<uint8_t>();
}
if (doc.containsKey("pulseDuration")) {
clockConfig.pulseDuration = doc["pulseDuration"].as<uint16_t>();
}
if (doc.containsKey("pauseDuration")) {
clockConfig.pauseDuration = doc["pauseDuration"].as<uint16_t>();
}
LOG_INFO("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<String>();
}
if (doc.containsKey("alertRingInterval")) {
clockConfig.alertRingInterval = doc["alertRingInterval"].as<uint16_t>();
}
if (doc.containsKey("hourBell")) {
clockConfig.hourBell = doc["hourBell"].as<uint8_t>();
}
if (doc.containsKey("halfBell")) {
clockConfig.halfBell = doc["halfBell"].as<uint8_t>();
}
if (doc.containsKey("quarterBell")) {
clockConfig.quarterBell = doc["quarterBell"].as<uint8_t>();
}
LOG_INFO("ConfigManager: Updated Clock alerts");
}
void ConfigManager::updateClockBacklight(JsonVariant doc) {
if (doc.containsKey("enabled")) {
clockConfig.backlight = doc["enabled"].as<bool>();
}
if (doc.containsKey("output")) {
clockConfig.backlightOutput = doc["output"].as<uint8_t>();
}
if (doc.containsKey("onTime")) {
clockConfig.backlightOnTime = doc["onTime"].as<String>();
}
if (doc.containsKey("offTime")) {
clockConfig.backlightOffTime = doc["offTime"].as<String>();
}
LOG_INFO("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<bool>();
}
if (daytime.containsKey("onTime")) {
clockConfig.daytimeSilenceOnTime = daytime["onTime"].as<String>();
}
if (daytime.containsKey("offTime")) {
clockConfig.daytimeSilenceOffTime = daytime["offTime"].as<String>();
}
}
if (doc.containsKey("nighttime")) {
JsonObject nighttime = doc["nighttime"];
if (nighttime.containsKey("enabled")) {
clockConfig.nighttimeSilenceEnabled = nighttime["enabled"].as<bool>();
}
if (nighttime.containsKey("onTime")) {
clockConfig.nighttimeSilenceOnTime = nighttime["onTime"].as<String>();
}
if (nighttime.containsKey("offTime")) {
clockConfig.nighttimeSilenceOffTime = nighttime["offTime"].as<String>();
}
}
LOG_INFO("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("c1output")) clockConfig.c1output = doc["c1output"].as<uint8_t>();
if (doc.containsKey("c2output")) clockConfig.c2output = doc["c2output"].as<uint8_t>();
if (doc.containsKey("pulseDuration")) clockConfig.pulseDuration = doc["pulseDuration"].as<uint16_t>();
if (doc.containsKey("pauseDuration")) clockConfig.pauseDuration = doc["pauseDuration"].as<uint16_t>();
LOG_INFO("ConfigManager: Clock config loaded");
return true;
}
bool ConfigManager::saveClockConfig() {
if (!ensureSDCard()) return false;
StaticJsonDocument<512> doc;
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_INFO("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<uint8_t>() % 12;
clockConfig.physicalMinute = doc["minute"].as<uint8_t>() % 60;
clockConfig.nextOutputIsC1 = doc["nextIsC1"].as<bool>();
clockConfig.lastSyncTime = doc["lastSyncTime"].as<uint32_t>();
LOG_INFO("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_DEBUG("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_INFO("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>()) {
JsonArray serversArray = doc["servers"];
for (JsonObject server : serversArray) {
if (server.containsKey("url")) {
String url = server["url"].as<String>();
if (!url.isEmpty()) {
updateServers.push_back(url);
}
}
}
}
LOG_INFO("ConfigManager: Loaded %d update servers from SD card", updateServers.size());
return true;
}
std::vector<String> ConfigManager::getUpdateServers() const {
std::vector<String> 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;
saveToSD();
LOG_INFO("ConfigManager: TimeConfig updated - GMT offset %ld sec, DST offset %d sec",
gmtOffsetSec, daylightOffsetSec);
}
void ConfigManager::updateNetworkConfig(bool useStaticIP, IPAddress ip, IPAddress gateway,
IPAddress subnet, IPAddress dns1, IPAddress dns2) {
networkConfig.useStaticIP = useStaticIP;
networkConfig.ip = ip;
networkConfig.gateway = gateway;
networkConfig.subnet = subnet;
networkConfig.dns1 = dns1;
networkConfig.dns2 = dns2;
saveNetworkConfig(); // Save immediately to SD
LOG_INFO("ConfigManager: NetworkConfig updated - Static IP: %s, IP: %s",
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_INFO("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<String>();
if (!customHostname.isEmpty()) {
networkConfig.hostname = customHostname;
LOG_INFO("ConfigManager: Custom hostname loaded from SD: %s", customHostname.c_str());
}
}
// Load static IP configuration
if (doc.containsKey("useStaticIP")) {
networkConfig.useStaticIP = doc["useStaticIP"].as<bool>();
}
if (doc.containsKey("ip")) {
String ipStr = doc["ip"].as<String>();
if (!ipStr.isEmpty() && ipStr != "0.0.0.0") {
networkConfig.ip.fromString(ipStr);
}
}
if (doc.containsKey("gateway")) {
String gwStr = doc["gateway"].as<String>();
if (!gwStr.isEmpty() && gwStr != "0.0.0.0") {
networkConfig.gateway.fromString(gwStr);
}
}
if (doc.containsKey("subnet")) {
String subnetStr = doc["subnet"].as<String>();
if (!subnetStr.isEmpty() && subnetStr != "0.0.0.0") {
networkConfig.subnet.fromString(subnetStr);
}
}
if (doc.containsKey("dns1")) {
String dns1Str = doc["dns1"].as<String>();
if (!dns1Str.isEmpty() && dns1Str != "0.0.0.0") {
networkConfig.dns1.fromString(dns1Str);
}
}
if (doc.containsKey("dns2")) {
String dns2Str = doc["dns2"].as<String>();
if (!dns2Str.isEmpty() && dns2Str != "0.0.0.0") {
networkConfig.dns2.fromString(dns2Str);
}
}
LOG_INFO("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_INFO("ConfigManager: Network config saved to SD");
return true;
}
// ═══════════════════════════════════════════════════════════════════════════════
// FACTORY RESET IMPLEMENTATION
// ═══════════════════════════════════════════════════════════════════════════════
bool ConfigManager::factoryReset() {
LOG_WARNING("═══════════════════════════════════════════════════════════════════════════");
LOG_WARNING("🏭 FACTORY RESET INITIATED");
LOG_WARNING("═══════════════════════════════════════════════════════════════════════════");
if (!ensureSDCard()) {
LOG_ERROR("❌ ConfigManager: Cannot perform factory reset - SD card not available");
return false;
}
// Step 1: Delete all configuration files
LOG_INFO("🗑️ Step 1: Deleting all configuration files from SD card...");
bool deleteSuccess = clearAllSettings();
if (!deleteSuccess) {
LOG_ERROR("❌ ConfigManager: Factory reset failed - could not delete all settings");
return false;
}
// Step 2: Reset in-memory configuration to defaults
LOG_INFO("🔄 Step 2: Resetting in-memory configuration to defaults...");
// Reset network config (keep device-generated values)
networkConfig.useStaticIP = false;
networkConfig.ip = IPAddress(0, 0, 0, 0);
networkConfig.gateway = IPAddress(0, 0, 0, 0);
networkConfig.subnet = IPAddress(0, 0, 0, 0);
networkConfig.dns1 = IPAddress(0, 0, 0, 0);
networkConfig.dns2 = IPAddress(0, 0, 0, 0);
// hostname and apSsid are auto-generated from deviceUID, keep them
// Reset time config
timeConfig.gmtOffsetSec = 0;
timeConfig.daylightOffsetSec = 0;
// Reset bell config
createDefaultBellConfig();
// Reset clock config to defaults
clockConfig.c1output = 255;
clockConfig.c2output = 255;
clockConfig.pulseDuration = 5000;
clockConfig.pauseDuration = 2000;
clockConfig.physicalHour = 0;
clockConfig.physicalMinute = 0;
clockConfig.nextOutputIsC1 = true;
clockConfig.lastSyncTime = 0;
clockConfig.alertType = "OFF";
clockConfig.alertRingInterval = 1200;
clockConfig.hourBell = 255;
clockConfig.halfBell = 255;
clockConfig.quarterBell = 255;
clockConfig.backlight = false;
clockConfig.backlightOutput = 255;
clockConfig.backlightOnTime = "18:00";
clockConfig.backlightOffTime = "06:00";
clockConfig.daytimeSilenceEnabled = false;
clockConfig.daytimeSilenceOnTime = "14:00";
clockConfig.daytimeSilenceOffTime = "17:00";
clockConfig.nighttimeSilenceEnabled = false;
clockConfig.nighttimeSilenceOnTime = "22:00";
clockConfig.nighttimeSilenceOffTime = "07:00";
// Note: Device identity (deviceUID, hwType, hwVersion) in NVS is NOT reset
// Note: WiFi credentials are handled by WiFiManager, not reset here
LOG_INFO("✅ Step 2: In-memory configuration reset to defaults");
LOG_WARNING("✅ FACTORY RESET COMPLETE");
LOG_WARNING("🔄 Device will boot with default settings on next restart");
LOG_WARNING("🆔 Device identity (UID) preserved in NVS");
LOG_INFO("WiFi credentials should be cleared separately using WiFiManager");
return true;
}
bool ConfigManager::clearAllSettings() {
if (!ensureSDCard()) {
LOG_ERROR("ConfigManager: SD card not available for clearing settings");
return false;
}
bool allDeleted = true;
int filesDeleted = 0;
int filesFailed = 0;
// List of all configuration files to delete
const char* settingsFiles[] = {
"/settings/deviceConfig.json",
"/settings/networkConfig.json",
"/settings/relayTimings.json",
"/settings/clockConfig.json",
"/settings/clockState.json",
"/settings/updateServers.json"
};
int numFiles = sizeof(settingsFiles) / sizeof(settingsFiles[0]);
LOG_INFO("ConfigManager: Attempting to delete %d configuration files...", numFiles);
// Delete each configuration file
for (int i = 0; i < numFiles; i++) {
const char* filepath = settingsFiles[i];
if (SD.exists(filepath)) {
if (SD.remove(filepath)) {
LOG_INFO("✅ Deleted: %s", filepath);
filesDeleted++;
} else {
LOG_ERROR("❌ Failed to delete: %s", filepath);
filesFailed++;
allDeleted = false;
}
} else {
LOG_DEBUG("⏩ Skip (not found): %s", filepath);
}
}
// Also delete the /melodies directory if you want a complete reset
// Uncomment if you want to delete melodies too:
/*
if (SD.exists("/melodies")) {
LOG_INFO("Deleting /melodies directory...");
// Note: SD library doesn't have rmdir for non-empty dirs
// You'd need to implement recursive delete or just leave melodies
}
*/
LOG_INFO("═══════════════════════════════════════════════════════════════════════════");
LOG_INFO("📄 Settings cleanup summary:");
LOG_INFO(" ✅ Files deleted: %d", filesDeleted);
LOG_INFO(" ❌ Files failed: %d", filesFailed);
LOG_INFO(" 🔄 Total processed: %d / %d", filesDeleted + filesFailed, numFiles);
LOG_INFO("═══════════════════════════════════════════════════════════════════════════");
return allDeleted;
}