1235 lines
47 KiB
C++
1235 lines
47 KiB
C++
#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_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<String>();
|
|
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<uint16_t>();
|
|
}
|
|
}
|
|
|
|
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<uint16_t>(); // 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<uint16_t>();
|
|
}
|
|
}
|
|
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<uint16_t>() - 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<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_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<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_DEBUG("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_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<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_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<bool>();
|
|
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_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<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_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>()) {
|
|
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_DEBUG("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;
|
|
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<String>();
|
|
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<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_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<String>();
|
|
if (!ntpServer.isEmpty()) {
|
|
timeConfig.ntpServer = ntpServer;
|
|
}
|
|
}
|
|
|
|
// Load GMT offset
|
|
if (doc.containsKey("gmtOffsetSec")) {
|
|
timeConfig.gmtOffsetSec = doc["gmtOffsetSec"].as<long>();
|
|
}
|
|
|
|
// Load daylight saving offset
|
|
if (doc.containsKey("daylightOffsetSec")) {
|
|
timeConfig.daylightOffsetSec = doc["daylightOffsetSec"].as<int>();
|
|
}
|
|
|
|
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<uint8_t>();
|
|
}
|
|
if (doc.containsKey("sdLogLevel")) {
|
|
generalConfig.sdLogLevel = doc["sdLogLevel"].as<uint8_t>();
|
|
}
|
|
if (doc.containsKey("mqttLogLevel")) {
|
|
generalConfig.mqttLogLevel = doc["mqttLogLevel"].as<uint8_t>();
|
|
}
|
|
if (doc.containsKey("mqttEnabled")) {
|
|
generalConfig.mqttEnabled = doc["mqttEnabled"].as<bool>();
|
|
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;
|
|
}
|