From f462bab72f14e13053a948d30e018f2afd9d2ef4 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 27 Jul 2025 20:13:03 +0200 Subject: [PATCH 1/8] added boot loop detection and config backup, work in progress --- wled00/cfg.cpp | 14 +++++++ wled00/fcn_declare.h | 8 ++++ wled00/file.cpp | 31 +++++++++++++++ wled00/util.cpp | 86 +++++++++++++++++++++++++++++++++++++++++- wled00/wled.cpp | 44 ++++++++++++++++++++- wled00/wled_server.cpp | 5 +++ 6 files changed, 186 insertions(+), 2 deletions(-) diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index fb67e578e0..fe88203768 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -774,6 +774,19 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { static const char s_cfg_json[] PROGMEM = "/cfg.json"; +static const char s_cfg_bak[] PROGMEM = "/cfg.bak"; + +void backupConfig() { + DEBUG_PRINTLN(F("Backing up current config to /cfg.bak...")); + Serial.println(F("Backing up current config to /cfg.bak...")); + copyFile(FPSTR(s_cfg_json), FPSTR(s_cfg_bak)); +} + +void restoreConfig() { + DEBUG_PRINTLN(F("Restoring backup from /cfg.bak...")); + Serial.println(F("Restoring backup from /cfg.bak...")); + copyFile(FPSTR(s_cfg_bak), FPSTR(s_cfg_json)); +} bool deserializeConfigFromFS() { [[maybe_unused]] bool success = deserializeConfigSec(); @@ -800,6 +813,7 @@ bool deserializeConfigFromFS() { void serializeConfigToFS() { serializeConfigSec(); + backupConfig(); // backup before writing new config DEBUG_PRINTLN(F("Writing settings to /cfg.json...")); diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index d19f89b27d..629ea645f9 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -24,6 +24,8 @@ void handleIO(); void IRAM_ATTR touchButtonISR(); //cfg.cpp +void backupConfig(); +void restoreConfig(); bool deserializeConfig(JsonObject doc, bool fromFS = false); bool deserializeConfigFromFS(); bool deserializeConfigSec(); @@ -223,6 +225,7 @@ inline bool writeObjectToFileUsingId(const String &file, uint16_t id, const Json inline bool writeObjectToFile(const String &file, const char* key, const JsonDocument* content) { return writeObjectToFile(file.c_str(), key, content); }; inline bool readObjectFromFileUsingId(const String &file, uint16_t id, JsonDocument* dest, const JsonDocument* filter = nullptr) { return readObjectFromFileUsingId(file.c_str(), id, dest); }; inline bool readObjectFromFile(const String &file, const char* key, JsonDocument* dest, const JsonDocument* filter = nullptr) { return readObjectFromFile(file.c_str(), key, dest); }; +bool copyFile(const String &src_path, const String &dst_path); //hue.cpp void handleHue(); @@ -580,6 +583,11 @@ extern "C" { #define d_free free #endif + +void bootloopCheckOTA(); // track bootloop actions, call this in setup() to restore config or reset it +#ifndef ESP8266 +bool detectBootLoop(); +#endif // RAII guard class for the JSON Buffer lock // Modeled after std::lock_guard class JSONBufferGuard { diff --git a/wled00/file.cpp b/wled00/file.cpp index c1960e616c..a42b907300 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -439,3 +439,34 @@ bool handleFileRead(AsyncWebServerRequest* request, String path){ } return false; } + +// copy a file, delete dst_path if incomplete to prevent corrupted files +bool copyFile(const String &src_path, const String &dst_path) { + bool success = true; + if(!WLED_FS.exists(src_path)) { + return false; + } + File src = WLED_FS.open(src_path, "r"); + File dst = WLED_FS.open(dst_path, "w"); + DEBUG_PRINTLN(F("copyFile from ") + src_path + F(" to ") + dst_path); + if (src && dst) { + uint8_t buf[128]; + while (true) { + size_t bytesRead = src.read(buf, sizeof(buf)); + if (bytesRead == 0) { + break; // EOF or error + } + size_t bytesWritten = dst.write(buf, bytesRead); + if (bytesWritten != bytesRead) { + success = false; + } + } + } + if(src) src.close(); + if(dst) dst.close(); + if (!success) { + DEBUG_PRINTLN(F("Copy failed")); + WLED_FS.remove(dst_path); // delete incomplete file + } + return success; +} diff --git a/wled00/util.cpp b/wled00/util.cpp index 6ff7b05dfc..9b4eda19d0 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -1,7 +1,11 @@ #include "wled.h" #include "fcn_declare.h" #include "const.h" - +#ifdef ESP8266 +#include "user_interface.h" // for bootloop detection +#else +#include "esp32/rtc.h" // for bootloop detection +#endif //helper to get int value at a position in string int getNumVal(const String &req, uint16_t pos) @@ -706,6 +710,86 @@ void *realloc_malloc(void *ptr, size_t size) { } #endif +// bootloop detection and handling +// checks if the ESP reboots multiple times due to a crash or watchdog timeout +// if a bootloop is detected: restore settings from backup, then reset settings, then switch boot image (and repeat) + +#define BOOTLOOP_THRESHOLD 20 // number of consecutive crashes to trigger boot loop detection +#ifdef ESP8266 +#include "user_interface.h" // for ESP8266, needed for bootloop detection +#define BOOTLOOP_INTERVAL_TICKS (5 * 160000) // ~5 seconds in RTC ticks +#define BOOT_TIME_IDX 0 // index in RTC memory for boot time +#define CRASH_COUNTER_IDX 1 // index in RTC memory for crash counter +#else +#include "esp32/rtc.h" // ESP32 +#define BOOTLOOP_INTERVAL_TICKS 5000 // ~5 seconds +#define BOOTLOOP_ACTION_RESTORE 0 // if bl_actiontracker is set to this, config is restored from /cfg.bak +#define BOOTLOOP_ACTION_RESET 1 // if bl_actiontracker is set to this, config is reset (rename /cfg.json to /cfg.fault) +#define BOOTLOOP_ACTION_OTA 2 // if bl_actiontracker is set to this, the boot partition is swapped +// variables in RTC_NOINIT memory persist between reboots (but not on hardware reset) +RTC_NOINIT_ATTR static uint32_t bl_last_boottime; +RTC_NOINIT_ATTR static uint32_t bl_crashcounter; +RTC_NOINIT_ATTR static uint32_t bl_actiontracker; +#endif + +bool detectBootLoop() { +#ifndef ESP8266 + uint32_t rtctime = esp_rtc_get_time_us() / 1000; // convert to milliseconds + esp_reset_reason_t reason = esp_reset_reason(); + + if (!(reason == ESP_RST_PANIC || reason == ESP_RST_WDT || reason == ESP_RST_INT_WDT || reason == ESP_RST_TASK_WDT)) { + // no crash detected, init variables + bl_crashcounter = 0; + bl_last_boottime = rtctime; + if(reason == ESP_RST_POWERON) // hardware reset + bl_actiontracker = 0; + } + else { + uint32_t rebootinterval = rtctime - bl_last_boottime; + bl_last_boottime = rtctime; // store current runtime for next reboot + if (rebootinterval < BOOTLOOP_INTERVAL_TICKS) { + bl_crashcounter++; + if (bl_crashcounter >= BOOTLOOP_THRESHOLD) { + DEBUG_PRINTLN(F("BOOTLOOP DETECTED")); + bl_crashcounter = 0; + return true; + } + } + } + return false; +#else // ESP8266 + rst_info* resetreason = system_get_rst_info(); + uint32_t bl_last_boottime; + uint32_t bl_crashcounter; + uint32_t rtctime = system_get_rtc_time(); + + if (!(resetreason->reason == REASON_EXCEPTION_RST || resetreason->reason == REASON_WDT_RST)) { + // no crash detected, init variables + bl_crashcounter = 0; + ESP.rtcUserMemoryWrite(BOOT_TIME_IDX, &rtctime, sizeof(uint32_t)); + ESP.rtcUserMemoryWrite(CRASH_COUNTER_IDX, &bl_crashcounter, sizeof(uint32_t)); + } else { + // system has crashed + ESP.rtcUserMemoryRead(BOOT_TIME_IDX, &bl_last_boottime, sizeof(uint32_t)); + ESP.rtcUserMemoryRead(CRASH_COUNTER_IDX, &bl_crashcounter, sizeof(uint32_t)); + uint32_t rebootinterval = rtctime - bl_last_boottime; + ESP.rtcUserMemoryWrite(BOOT_TIME_IDX, &rtctime, sizeof(uint32_t)); // store current ticks for next reboot + if (rebootinterval < BOOTLOOP_INTERVAL_TICKS) { + bl_crashcounter++; + ESP.rtcUserMemoryWrite(CRASH_COUNTER_IDX, &bl_crashcounter, sizeof(uint32_t)); + if (bl_crashcounter >= BOOTLOOP_THRESHOLD) { + DEBUG_PRINTLN(F("BOOTLOOP DETECTED")); + bl_crashcounter = 0; + ESP.rtcUserMemoryWrite(CRASH_COUNTER_IDX, &bl_crashcounter, sizeof(uint32_t)); + return true; + } + } + } + return false; +#endif +} + + /* * Fixed point integer based Perlin noise functions by @dedehai * Note: optimized for speed and to mimic fastled inoise functions, not for accuracy or best randomness diff --git a/wled00/wled.cpp b/wled00/wled.cpp index c372d22abd..22822e425e 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -33,7 +33,7 @@ void WLED::reset() DEBUG_PRINTLN(F("WLED RESET")); ESP.restart(); } - +#include "esp32/rtc.h" // for bootloop detection TODO: remove this!!! debug only. void WLED::loop() { static uint32_t lastHeap = UINT32_MAX; @@ -210,6 +210,32 @@ void WLED::loop() toki.resetTick(); +static unsigned long debugTime=0; + +if(millis() -debugTime>5000) { +debugTime=millis(); +//Serial.printf_P(PSTR("Free heap: %u, Contiguous: %u\n"), getFreeHeapSize(), getContiguousFreeHeap()); + +uint32_t rtcTime = esp_rtc_get_time_us() / 1000; + +Serial.printf("RTC time in ms: %lu\n", rtcTime); // update system time, this is needed for the RTC to work properly +//Serial.printf("tracker: %lu\n", bl_actiontracker); // update system time, this is needed for the RTC to work properly + // bl_actiontracker++; // debug!!! +/* +if(millis() > 20000) { + // make it crash + int *p = nullptr; + *p = 0x12345678; // write to null pointer +}*/ + +} +/* +// make it crash +if(!bootLoopDetected) { + int *p = nullptr; + *p = 0x12345678; // write to null pointer +}*/ + #if WLED_WATCHDOG_TIMEOUT > 0 // we finished our mainloop, reset the watchdog timer static unsigned long lastWDTFeed = 0; @@ -410,6 +436,22 @@ void WLED::setup() #ifdef WLED_ADD_EEPROM_SUPPORT else deEEP(); #else +/* +// check for bootloop +if (detectBootLoop()) { + restoreConfig(); + + // check if backup exists + if (WLED_FS.exists(FPSTR(s_cfg_backup))) { + // just delete the config and do not overwrite the backup + WLED_FS.remove(FPSTR(s_cfg_json)); + } + WLED_FS.rename(FPSTR(s_cfg_json), FPSTR(s_cfg_backup)); + //reboot: + DEBUG_PRINTLN(F("Rebooting...")); + delay(1000); + ESP.restart(); +}*/ initPresetsFile(); #endif updateFSInfo(); diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 4434a2f3e5..16db3330c1 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -408,6 +408,11 @@ void initServer() serveMessage(request, 500, F("Update failed!"), F("Please check your file and retry!"), 254); } else { serveMessage(request, 200, F("Update successful!"), FPSTR(s_rebooting), 131); + // let the bootloop-checker know there was an OTA update + #ifndef ESP8266 + // set the bootlop action to ota revert + // todo: set bootloop action to config revert in config safe function + #endif doReboot = true; } },[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){ From bd2efa8af1a8608bc3fd22e45dc1980e2deb510e Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 27 Jul 2025 22:43:25 +0200 Subject: [PATCH 2/8] updated bootloop check to handle config and OTA partition --- wled00/cfg.cpp | 23 +++++++++++-------- wled00/fcn_declare.h | 10 ++++----- wled00/file.cpp | 18 ++++++++------- wled00/util.cpp | 51 +++++++++++++++++++++++++++++++++--------- wled00/wled.cpp | 19 +++------------- wled00/wled_server.cpp | 4 +--- 6 files changed, 74 insertions(+), 51 deletions(-) diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index fe88203768..b12d454df8 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -774,18 +774,23 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { static const char s_cfg_json[] PROGMEM = "/cfg.json"; -static const char s_cfg_bak[] PROGMEM = "/cfg.bak"; +static const char s_cfg_bak[] PROGMEM = "/cfg.bak.json"; -void backupConfig() { - DEBUG_PRINTLN(F("Backing up current config to /cfg.bak...")); - Serial.println(F("Backing up current config to /cfg.bak...")); - copyFile(FPSTR(s_cfg_json), FPSTR(s_cfg_bak)); +bool backupConfig() { + DEBUG_PRINTLN(F("Backup config")); + return copyFile(FPSTR(s_cfg_json), FPSTR(s_cfg_bak)); } -void restoreConfig() { - DEBUG_PRINTLN(F("Restoring backup from /cfg.bak...")); - Serial.println(F("Restoring backup from /cfg.bak...")); - copyFile(FPSTR(s_cfg_bak), FPSTR(s_cfg_json)); +bool restoreConfig() { + DEBUG_PRINTLN(F("Restore config")); + return copyFile(FPSTR(s_cfg_bak), FPSTR(s_cfg_json)); +} + +// rename config file and reboot +void resetConfig() { + DEBUG_PRINTLN(F("Reset config")); + WLED_FS.rename(FPSTR(s_cfg_json), String(FPSTR(s_cfg_bak))); + doReboot = true; } bool deserializeConfigFromFS() { diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 629ea645f9..4512c8b5ba 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -24,8 +24,9 @@ void handleIO(); void IRAM_ATTR touchButtonISR(); //cfg.cpp -void backupConfig(); -void restoreConfig(); +bool backupConfig(); +bool restoreConfig(); +void resetConfig(); bool deserializeConfig(JsonObject doc, bool fromFS = false); bool deserializeConfigFromFS(); bool deserializeConfigSec(); @@ -583,10 +584,9 @@ extern "C" { #define d_free free #endif - -void bootloopCheckOTA(); // track bootloop actions, call this in setup() to restore config or reset it +void handleBootLoop(); // detect and handle boot loops #ifndef ESP8266 -bool detectBootLoop(); +void bootloopCheckOTA(); // swap boot image if boot loop is detected instead of restoring config #endif // RAII guard class for the JSON Buffer lock // Modeled after std::lock_guard diff --git a/wled00/file.cpp b/wled00/file.cpp index a42b907300..98e9b59310 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -440,25 +440,27 @@ bool handleFileRead(AsyncWebServerRequest* request, String path){ return false; } -// copy a file, delete dst_path if incomplete to prevent corrupted files +// copy a file, delete destination file if incomplete to prevent corrupted files bool copyFile(const String &src_path, const String &dst_path) { - bool success = true; - if(!WLED_FS.exists(src_path)) { - return false; - } + DEBUG_PRINTLN(F("copyFile from ") + src_path + F(" to ") + dst_path); + if(!WLED_FS.exists(src_path)) return false; + + bool success = false; File src = WLED_FS.open(src_path, "r"); File dst = WLED_FS.open(dst_path, "w"); - DEBUG_PRINTLN(F("copyFile from ") + src_path + F(" to ") + dst_path); + if (src && dst) { uint8_t buf[128]; - while (true) { + while (src.available() > 0) { size_t bytesRead = src.read(buf, sizeof(buf)); if (bytesRead == 0) { - break; // EOF or error + success = false; + break; // error, no data read } size_t bytesWritten = dst.write(buf, bytesRead); if (bytesWritten != bytesRead) { success = false; + break; // error, not all data written } } } diff --git a/wled00/util.cpp b/wled00/util.cpp index 9b4eda19d0..7d93f2e4af 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -715,24 +715,28 @@ void *realloc_malloc(void *ptr, size_t size) { // if a bootloop is detected: restore settings from backup, then reset settings, then switch boot image (and repeat) #define BOOTLOOP_THRESHOLD 20 // number of consecutive crashes to trigger boot loop detection +#define BOOTLOOP_ACTION_RESTORE 0 // default action: restore config from /cfg.bak +#define BOOTLOOP_ACTION_RESET 1 // if restore does not work, reset config (rename /cfg.json to /cfg.fault) +#define BOOTLOOP_ACTION_OTA 2 // swap the boot partition +#define BOOTLOOP_ACTION_NONE 3 // nothing seems to help, no action is taken (until hardware reset) #ifdef ESP8266 #include "user_interface.h" // for ESP8266, needed for bootloop detection -#define BOOTLOOP_INTERVAL_TICKS (5 * 160000) // ~5 seconds in RTC ticks -#define BOOT_TIME_IDX 0 // index in RTC memory for boot time -#define CRASH_COUNTER_IDX 1 // index in RTC memory for crash counter +#define BOOTLOOP_INTERVAL_TICKS (5 * 160000) // time limit between crashes: ~5 seconds in RTC ticks +#define BOOT_TIME_IDX 0 // index in RTC memory for boot time +#define CRASH_COUNTER_IDX 1 // index in RTC memory for crash counter +#define ACTIONT_TRACKER_IDX 2 // index in RTC memory for boot action #else #include "esp32/rtc.h" // ESP32 -#define BOOTLOOP_INTERVAL_TICKS 5000 // ~5 seconds -#define BOOTLOOP_ACTION_RESTORE 0 // if bl_actiontracker is set to this, config is restored from /cfg.bak -#define BOOTLOOP_ACTION_RESET 1 // if bl_actiontracker is set to this, config is reset (rename /cfg.json to /cfg.fault) -#define BOOTLOOP_ACTION_OTA 2 // if bl_actiontracker is set to this, the boot partition is swapped +#define BOOTLOOP_INTERVAL_TICKS 5000 // time limit between crashes: ~5 seconds in milliseconds // variables in RTC_NOINIT memory persist between reboots (but not on hardware reset) RTC_NOINIT_ATTR static uint32_t bl_last_boottime; RTC_NOINIT_ATTR static uint32_t bl_crashcounter; RTC_NOINIT_ATTR static uint32_t bl_actiontracker; +void bootloopCheckOTA() { bl_actiontracker = BOOTLOOP_ACTION_OTA; } // swap boot image if boot loop is detected instead of restoring config #endif -bool detectBootLoop() { +// detect boot loop by checking the reset reason and the time since last boot +static bool detectBootLoop() { #ifndef ESP8266 uint32_t rtctime = esp_rtc_get_time_us() / 1000; // convert to milliseconds esp_reset_reason_t reason = esp_reset_reason(); @@ -741,8 +745,8 @@ bool detectBootLoop() { // no crash detected, init variables bl_crashcounter = 0; bl_last_boottime = rtctime; - if(reason == ESP_RST_POWERON) // hardware reset - bl_actiontracker = 0; + if (reason == ESP_RST_POWERON) // hardware reset + bl_actiontracker = BOOTLOOP_ACTION_RESTORE; } else { uint32_t rebootinterval = rtctime - bl_last_boottime; @@ -761,13 +765,16 @@ bool detectBootLoop() { rst_info* resetreason = system_get_rst_info(); uint32_t bl_last_boottime; uint32_t bl_crashcounter; + uint32_t bl_actiontracker; uint32_t rtctime = system_get_rtc_time(); if (!(resetreason->reason == REASON_EXCEPTION_RST || resetreason->reason == REASON_WDT_RST)) { // no crash detected, init variables bl_crashcounter = 0; + bl_actiontracker = BOOTLOOP_ACTION_RESTORE; ESP.rtcUserMemoryWrite(BOOT_TIME_IDX, &rtctime, sizeof(uint32_t)); ESP.rtcUserMemoryWrite(CRASH_COUNTER_IDX, &bl_crashcounter, sizeof(uint32_t)); + ESP.rtcUserMemoryWrite(ACTIONT_TRACKER_IDX, &bl_actiontracker, sizeof(uint32_t)); } else { // system has crashed ESP.rtcUserMemoryRead(BOOT_TIME_IDX, &bl_last_boottime, sizeof(uint32_t)); @@ -789,6 +796,30 @@ bool detectBootLoop() { #endif } +void handleBootLoop() { + if (!detectBootLoop()) return; // no boot loop detected +#ifdef ESP8266 + uint32_t bl_actiontracker; + ESP.rtcUserMemoryRead(ACTIONT_TRACKER_IDX, &bl_actiontracker, sizeof(uint32_t)); +#endif + if (bl_actiontracker == BOOTLOOP_ACTION_RESET) { + restoreConfig(); + bl_actiontracker = BOOTLOOP_ACTION_RESET; // reset config if it keeps bootlooping + } else if (bl_actiontracker == BOOTLOOP_ACTION_RESET) { + resetConfig(); + bl_actiontracker = BOOTLOOP_ACTION_OTA; // swap boot partition if it keeps bootlooping. On ESP8266 this is the same as BOOTLOOP_ACTION_NONE + } +#ifndef ESP8266 + else if (bl_actiontracker == BOOTLOOP_ACTION_OTA) { + if(Update.canRollBack()) { + DEBUG_PRINTLN(F("Swapping boot partition...")); + Update.rollBack(); // swap boot partition + } + bl_actiontracker = BOOTLOOP_ACTION_NONE; // out of options + } +#endif + ESP.restart(); // restart cleanly and don't wait for another crash +} /* * Fixed point integer based Perlin noise functions by @dedehai diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 22822e425e..fcab49291c 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -433,25 +433,12 @@ void WLED::setup() DEBUGFS_PRINTLN(F("FS failed!")); errorFlag = ERR_FS_BEGIN; } + + handleBootLoop(); // check for bootloop and take action (requires WLED_FS) + #ifdef WLED_ADD_EEPROM_SUPPORT else deEEP(); #else -/* -// check for bootloop -if (detectBootLoop()) { - restoreConfig(); - - // check if backup exists - if (WLED_FS.exists(FPSTR(s_cfg_backup))) { - // just delete the config and do not overwrite the backup - WLED_FS.remove(FPSTR(s_cfg_json)); - } - WLED_FS.rename(FPSTR(s_cfg_json), FPSTR(s_cfg_backup)); - //reboot: - DEBUG_PRINTLN(F("Rebooting...")); - delay(1000); - ESP.restart(); -}*/ initPresetsFile(); #endif updateFSInfo(); diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 16db3330c1..3e606f4e8d 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -408,10 +408,8 @@ void initServer() serveMessage(request, 500, F("Update failed!"), F("Please check your file and retry!"), 254); } else { serveMessage(request, 200, F("Update successful!"), FPSTR(s_rebooting), 131); - // let the bootloop-checker know there was an OTA update #ifndef ESP8266 - // set the bootlop action to ota revert - // todo: set bootloop action to config revert in config safe function + bootloopCheckOTA(); // let the bootloop-checker know there was an OTA update #endif doReboot = true; } From 22b6f05b4a1de8a6b31018480a9218e4f9948c56 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 29 Jul 2025 07:34:18 +0200 Subject: [PATCH 3/8] bootloop: added some helper functions, code cleanup, bugfixes --- wled00/cfg.cpp | 13 ++++---- wled00/fcn_declare.h | 7 +++-- wled00/file.cpp | 70 +++++++++++++++++++++++++++++++++++++++--- wled00/util.cpp | 39 ++++++++++++++--------- wled00/wled.cpp | 26 ---------------- wled00/wled_server.cpp | 3 +- 6 files changed, 102 insertions(+), 56 deletions(-) diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index b12d454df8..a9abbd08d4 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -772,24 +772,23 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { return (doc["sv"] | true); } - static const char s_cfg_json[] PROGMEM = "/cfg.json"; -static const char s_cfg_bak[] PROGMEM = "/cfg.bak.json"; bool backupConfig() { - DEBUG_PRINTLN(F("Backup config")); - return copyFile(FPSTR(s_cfg_json), FPSTR(s_cfg_bak)); + return backupFile(s_cfg_json); } bool restoreConfig() { - DEBUG_PRINTLN(F("Restore config")); - return copyFile(FPSTR(s_cfg_bak), FPSTR(s_cfg_json)); + return restoreFile(s_cfg_json); } // rename config file and reboot void resetConfig() { DEBUG_PRINTLN(F("Reset config")); - WLED_FS.rename(FPSTR(s_cfg_json), String(FPSTR(s_cfg_bak))); + char backupname[32]; + strcpy(backupname, s_cfg_json); + strcat(backupname, ".rst.json"); + WLED_FS.rename(s_cfg_json, backupname); doReboot = true; } diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 4512c8b5ba..7da80b24b1 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -227,6 +227,9 @@ inline bool writeObjectToFile(const String &file, const char* key, const JsonDoc inline bool readObjectFromFileUsingId(const String &file, uint16_t id, JsonDocument* dest, const JsonDocument* filter = nullptr) { return readObjectFromFileUsingId(file.c_str(), id, dest); }; inline bool readObjectFromFile(const String &file, const char* key, JsonDocument* dest, const JsonDocument* filter = nullptr) { return readObjectFromFile(file.c_str(), key, dest); }; bool copyFile(const String &src_path, const String &dst_path); +bool backupFile(const char* filename); +bool restoreFile(const char* filename); +void dumpFilesToSerial(); //hue.cpp void handleHue(); @@ -584,9 +587,9 @@ extern "C" { #define d_free free #endif -void handleBootLoop(); // detect and handle boot loops +void handleBootLoop(); // detect and handle bootloops #ifndef ESP8266 -void bootloopCheckOTA(); // swap boot image if boot loop is detected instead of restoring config +void bootloopCheckOTA(); // swap boot image if bootloop is detected instead of restoring config #endif // RAII guard class for the JSON Buffer lock // Modeled after std::lock_guard diff --git a/wled00/file.cpp b/wled00/file.cpp index 98e9b59310..a942f6e6dc 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -441,16 +441,19 @@ bool handleFileRead(AsyncWebServerRequest* request, String path){ } // copy a file, delete destination file if incomplete to prevent corrupted files -bool copyFile(const String &src_path, const String &dst_path) { - DEBUG_PRINTLN(F("copyFile from ") + src_path + F(" to ") + dst_path); - if(!WLED_FS.exists(src_path)) return false; +bool copyFile(const char* src_path, const char* dst_path) { + DEBUG_PRINTF("copyFile from %s to %s", src_path, dst_path); + if(!WLED_FS.exists(src_path)) { + DEBUG_PRINTLN(F("file not found")); + return false; + } bool success = false; File src = WLED_FS.open(src_path, "r"); File dst = WLED_FS.open(dst_path, "w"); if (src && dst) { - uint8_t buf[128]; + uint8_t buf[128]; // copy file in 128-byte blocks while (src.available() > 0) { size_t bytesRead = src.read(buf, sizeof(buf)); if (bytesRead == 0) { @@ -467,8 +470,65 @@ bool copyFile(const String &src_path, const String &dst_path) { if(src) src.close(); if(dst) dst.close(); if (!success) { - DEBUG_PRINTLN(F("Copy failed")); + DEBUG_PRINTLN(F("copy failed")); WLED_FS.remove(dst_path); // delete incomplete file } return success; } + +static const char s_backup_json[] PROGMEM = ".bkp.json"; + +bool backupFile(const char* filename) { + DEBUG_PRINTF("backup %s", filename); + if (!WLED_FS.exists(filename)) return false; + char backupname[32]; + strcpy(backupname, filename); + strcat(backupname, s_backup_json); + + if (copyFile(filename, backupname)) { + DEBUG_PRINTLN(F("backup ok")); + return true; + } + DEBUG_PRINTLN(F("backup failed")); + return false; +} + +bool restoreFile(const char* filename) { + DEBUG_PRINTF("restore %s", filename); + char backupname[32]; + strcpy(backupname, filename); + strcat(backupname, s_backup_json); + + if (!WLED_FS.exists(backupname)) { + DEBUG_PRINTLN(F("no backup found")); + return false; + } + + if (copyFile(backupname, filename)) { + DEBUG_PRINTLN(F("restore ok")); + return true; + } + DEBUG_PRINTLN(F("restore failed")); + return false; +} + +// print contents of all files in root dir to Serial except wsec files +void dumpFilesToSerial() { + File rootdir = WLED_FS.open("/"); + File rootfile = rootdir.openNextFile(); + while (rootfile) { + size_t len = strlen(rootfile.name()); + // skip files starting with "wsec" and dont end in .json + if (strncmp(rootfile.name(), "wsec", 4) != 0 && len >= 6 && strcmp(rootfile.name() + len - 5, ".json") == 0) { + Serial.println(rootfile.name()); + while (rootfile.available()) { + Serial.write(rootfile.read()); + } + Serial.println(); + Serial.println(); + } + rootfile.close(); + rootfile = rootdir.openNextFile(); + } +} + diff --git a/wled00/util.cpp b/wled00/util.cpp index 7d93f2e4af..e407ca13f3 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -714,11 +714,11 @@ void *realloc_malloc(void *ptr, size_t size) { // checks if the ESP reboots multiple times due to a crash or watchdog timeout // if a bootloop is detected: restore settings from backup, then reset settings, then switch boot image (and repeat) -#define BOOTLOOP_THRESHOLD 20 // number of consecutive crashes to trigger boot loop detection +#define BOOTLOOP_THRESHOLD 5 // number of consecutive crashes to trigger bootloop detection #define BOOTLOOP_ACTION_RESTORE 0 // default action: restore config from /cfg.bak #define BOOTLOOP_ACTION_RESET 1 // if restore does not work, reset config (rename /cfg.json to /cfg.fault) #define BOOTLOOP_ACTION_OTA 2 // swap the boot partition -#define BOOTLOOP_ACTION_NONE 3 // nothing seems to help, no action is taken (until hardware reset) +#define BOOTLOOP_ACTION_DUMP 3 // nothing seems to help, dump files to serial and reboot (until hardware reset) #ifdef ESP8266 #include "user_interface.h" // for ESP8266, needed for bootloop detection #define BOOTLOOP_INTERVAL_TICKS (5 * 160000) // time limit between crashes: ~5 seconds in RTC ticks @@ -732,10 +732,10 @@ void *realloc_malloc(void *ptr, size_t size) { RTC_NOINIT_ATTR static uint32_t bl_last_boottime; RTC_NOINIT_ATTR static uint32_t bl_crashcounter; RTC_NOINIT_ATTR static uint32_t bl_actiontracker; -void bootloopCheckOTA() { bl_actiontracker = BOOTLOOP_ACTION_OTA; } // swap boot image if boot loop is detected instead of restoring config +void bootloopCheckOTA() { bl_actiontracker = BOOTLOOP_ACTION_OTA; } // swap boot image if bootloop is detected instead of restoring config #endif -// detect boot loop by checking the reset reason and the time since last boot +// detect bootloop by checking the reset reason and the time since last boot static bool detectBootLoop() { #ifndef ESP8266 uint32_t rtctime = esp_rtc_get_time_us() / 1000; // convert to milliseconds @@ -745,16 +745,20 @@ static bool detectBootLoop() { // no crash detected, init variables bl_crashcounter = 0; bl_last_boottime = rtctime; - if (reason == ESP_RST_POWERON) // hardware reset - bl_actiontracker = BOOTLOOP_ACTION_RESTORE; - } - else { + if(reason != ESP_RST_SW) + bl_actiontracker = BOOTLOOP_ACTION_RESTORE; // init action tracker if not an intentional reboot (e.g. from OTA or bootloop handler) + } else if (reason == ESP_RST_BROWNOUT) { + // crash due to brownout can't be detected unless using flash memory to store bootloop variables + // this is a simpler way to preemtively revert the config in case current brownout is caused by a bad choice of settings + DEBUG_PRINTLN(F("brownout detected")); + restoreConfig(); + } else { uint32_t rebootinterval = rtctime - bl_last_boottime; bl_last_boottime = rtctime; // store current runtime for next reboot if (rebootinterval < BOOTLOOP_INTERVAL_TICKS) { bl_crashcounter++; if (bl_crashcounter >= BOOTLOOP_THRESHOLD) { - DEBUG_PRINTLN(F("BOOTLOOP DETECTED")); + DEBUG_PRINTLN(F("!BOOTLOOP DETECTED!")); bl_crashcounter = 0; return true; } @@ -771,10 +775,12 @@ static bool detectBootLoop() { if (!(resetreason->reason == REASON_EXCEPTION_RST || resetreason->reason == REASON_WDT_RST)) { // no crash detected, init variables bl_crashcounter = 0; - bl_actiontracker = BOOTLOOP_ACTION_RESTORE; ESP.rtcUserMemoryWrite(BOOT_TIME_IDX, &rtctime, sizeof(uint32_t)); ESP.rtcUserMemoryWrite(CRASH_COUNTER_IDX, &bl_crashcounter, sizeof(uint32_t)); - ESP.rtcUserMemoryWrite(ACTIONT_TRACKER_IDX, &bl_actiontracker, sizeof(uint32_t)); + if(resetreason->reason != REASON_SOFT_RESTART) { + bl_actiontracker = BOOTLOOP_ACTION_RESTORE; // init action tracker if not an intentional reboot (e.g. from OTA or bootloop handler) + ESP.rtcUserMemoryWrite(ACTIONT_TRACKER_IDX, &bl_actiontracker, sizeof(uint32_t)); + } } else { // system has crashed ESP.rtcUserMemoryRead(BOOT_TIME_IDX, &bl_last_boottime, sizeof(uint32_t)); @@ -797,13 +803,14 @@ static bool detectBootLoop() { } void handleBootLoop() { - if (!detectBootLoop()) return; // no boot loop detected + DEBUG_PRINTLN(F("checking for bootloop")); + if (!detectBootLoop()) return; // no bootloop detected #ifdef ESP8266 uint32_t bl_actiontracker; ESP.rtcUserMemoryRead(ACTIONT_TRACKER_IDX, &bl_actiontracker, sizeof(uint32_t)); #endif - if (bl_actiontracker == BOOTLOOP_ACTION_RESET) { - restoreConfig(); + if (bl_actiontracker == BOOTLOOP_ACTION_RESTORE) { + restoreConfig(); // note: if this fails, could reset immediately. instead just let things play out and save a few lines of code bl_actiontracker = BOOTLOOP_ACTION_RESET; // reset config if it keeps bootlooping } else if (bl_actiontracker == BOOTLOOP_ACTION_RESET) { resetConfig(); @@ -815,7 +822,9 @@ void handleBootLoop() { DEBUG_PRINTLN(F("Swapping boot partition...")); Update.rollBack(); // swap boot partition } - bl_actiontracker = BOOTLOOP_ACTION_NONE; // out of options + bl_actiontracker = BOOTLOOP_ACTION_DUMP; // out of options + } else { + dumpFilesToSerial(); } #endif ESP.restart(); // restart cleanly and don't wait for another crash diff --git a/wled00/wled.cpp b/wled00/wled.cpp index fcab49291c..3993c284f1 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -210,32 +210,6 @@ void WLED::loop() toki.resetTick(); -static unsigned long debugTime=0; - -if(millis() -debugTime>5000) { -debugTime=millis(); -//Serial.printf_P(PSTR("Free heap: %u, Contiguous: %u\n"), getFreeHeapSize(), getContiguousFreeHeap()); - -uint32_t rtcTime = esp_rtc_get_time_us() / 1000; - -Serial.printf("RTC time in ms: %lu\n", rtcTime); // update system time, this is needed for the RTC to work properly -//Serial.printf("tracker: %lu\n", bl_actiontracker); // update system time, this is needed for the RTC to work properly - // bl_actiontracker++; // debug!!! -/* -if(millis() > 20000) { - // make it crash - int *p = nullptr; - *p = 0x12345678; // write to null pointer -}*/ - -} -/* -// make it crash -if(!bootLoopDetected) { - int *p = nullptr; - *p = 0x12345678; // write to null pointer -}*/ - #if WLED_WATCHDOG_TIMEOUT > 0 // we finished our mainloop, reset the watchdog timer static unsigned long lastWDTFeed = 0; diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 3e606f4e8d..2777cd65a5 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -429,8 +429,9 @@ void initServer() UsermodManager::onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init) lastEditTime = millis(); // make sure PIN does not lock during update strip.suspend(); - #ifdef ESP8266 + backupConfig(); // backup current config in case the update ends badly strip.resetSegments(); // free as much memory as you can + #ifdef ESP8266 Update.runAsync(true); #endif Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000); From 70a883a86419fe154f81a90e6357998bd3004e6c Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 29 Jul 2025 07:46:24 +0200 Subject: [PATCH 4/8] remove debug code --- wled00/wled.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 3993c284f1..4dc0cda9d0 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -33,7 +33,7 @@ void WLED::reset() DEBUG_PRINTLN(F("WLED RESET")); ESP.restart(); } -#include "esp32/rtc.h" // for bootloop detection TODO: remove this!!! debug only. + void WLED::loop() { static uint32_t lastHeap = UINT32_MAX; From 4b460c54eb0b04689056e3776e803a7c68e769de Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 29 Jul 2025 08:01:04 +0200 Subject: [PATCH 5/8] compile fix for ESP8266 --- wled00/file.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/file.cpp b/wled00/file.cpp index a942f6e6dc..796c2b972b 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -514,7 +514,7 @@ bool restoreFile(const char* filename) { // print contents of all files in root dir to Serial except wsec files void dumpFilesToSerial() { - File rootdir = WLED_FS.open("/"); + File rootdir = WLED_FS.open("/", "r"); File rootfile = rootdir.openNextFile(); while (rootfile) { size_t len = strlen(rootfile.name()); From ab69078aeebc90a8a78bf3bc54538183912d58b5 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 1 Aug 2025 08:31:13 +0200 Subject: [PATCH 6/8] bugfixes, compile fixes: bootloop detector disabled on ESP32 IDF V3 builds --- wled00/fcn_declare.h | 2 +- wled00/file.cpp | 4 +++- wled00/util.cpp | 21 +++++++++++---------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 7da80b24b1..21cac2f80e 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -226,7 +226,7 @@ inline bool writeObjectToFileUsingId(const String &file, uint16_t id, const Json inline bool writeObjectToFile(const String &file, const char* key, const JsonDocument* content) { return writeObjectToFile(file.c_str(), key, content); }; inline bool readObjectFromFileUsingId(const String &file, uint16_t id, JsonDocument* dest, const JsonDocument* filter = nullptr) { return readObjectFromFileUsingId(file.c_str(), id, dest); }; inline bool readObjectFromFile(const String &file, const char* key, JsonDocument* dest, const JsonDocument* filter = nullptr) { return readObjectFromFile(file.c_str(), key, dest); }; -bool copyFile(const String &src_path, const String &dst_path); +bool copyFile(const char* src_path, const char* dst_path); bool backupFile(const char* filename); bool restoreFile(const char* filename); void dumpFilesToSerial(); diff --git a/wled00/file.cpp b/wled00/file.cpp index 796c2b972b..1fa9ac92f1 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -448,7 +448,7 @@ bool copyFile(const char* src_path, const char* dst_path) { return false; } - bool success = false; + bool success = true; // is set to false on error File src = WLED_FS.open(src_path, "r"); File dst = WLED_FS.open(dst_path, "w"); @@ -466,6 +466,8 @@ bool copyFile(const char* src_path, const char* dst_path) { break; // error, not all data written } } + } else { + success = false; // error, could not open files } if(src) src.close(); if(dst) dst.close(); diff --git a/wled00/util.cpp b/wled00/util.cpp index e407ca13f3..daf2e76cf0 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -3,10 +3,12 @@ #include "const.h" #ifdef ESP8266 #include "user_interface.h" // for bootloop detection -#else +#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) #include "esp32/rtc.h" // for bootloop detection +#include #endif + //helper to get int value at a position in string int getNumVal(const String &req, uint16_t pos) { @@ -720,13 +722,11 @@ void *realloc_malloc(void *ptr, size_t size) { #define BOOTLOOP_ACTION_OTA 2 // swap the boot partition #define BOOTLOOP_ACTION_DUMP 3 // nothing seems to help, dump files to serial and reboot (until hardware reset) #ifdef ESP8266 -#include "user_interface.h" // for ESP8266, needed for bootloop detection #define BOOTLOOP_INTERVAL_TICKS (5 * 160000) // time limit between crashes: ~5 seconds in RTC ticks #define BOOT_TIME_IDX 0 // index in RTC memory for boot time #define CRASH_COUNTER_IDX 1 // index in RTC memory for crash counter #define ACTIONT_TRACKER_IDX 2 // index in RTC memory for boot action #else -#include "esp32/rtc.h" // ESP32 #define BOOTLOOP_INTERVAL_TICKS 5000 // time limit between crashes: ~5 seconds in milliseconds // variables in RTC_NOINIT memory persist between reboots (but not on hardware reset) RTC_NOINIT_ATTR static uint32_t bl_last_boottime; @@ -737,7 +737,8 @@ void bootloopCheckOTA() { bl_actiontracker = BOOTLOOP_ACTION_OTA; } // swap boot // detect bootloop by checking the reset reason and the time since last boot static bool detectBootLoop() { -#ifndef ESP8266 +#if !defined(ESP8266) + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) uint32_t rtctime = esp_rtc_get_time_us() / 1000; // convert to milliseconds esp_reset_reason_t reason = esp_reset_reason(); @@ -751,7 +752,7 @@ static bool detectBootLoop() { // crash due to brownout can't be detected unless using flash memory to store bootloop variables // this is a simpler way to preemtively revert the config in case current brownout is caused by a bad choice of settings DEBUG_PRINTLN(F("brownout detected")); - restoreConfig(); + //restoreConfig(); // TODO: blindly restoring config if brownout detected is a bad idea, need a better way (if at all) } else { uint32_t rebootinterval = rtctime - bl_last_boottime; bl_last_boottime = rtctime; // store current runtime for next reboot @@ -764,7 +765,7 @@ static bool detectBootLoop() { } } } - return false; + #endif #else // ESP8266 rst_info* resetreason = system_get_rst_info(); uint32_t bl_last_boottime; @@ -798,8 +799,8 @@ static bool detectBootLoop() { } } } - return false; #endif + return false; // no bootloop detected } void handleBootLoop() { @@ -823,10 +824,10 @@ void handleBootLoop() { Update.rollBack(); // swap boot partition } bl_actiontracker = BOOTLOOP_ACTION_DUMP; // out of options - } else { - dumpFilesToSerial(); } -#endif + #endif + else + dumpFilesToSerial(); ESP.restart(); // restart cleanly and don't wait for another crash } From a59376a79e8545bd7dd75d2d7cc481c146782ba4 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 1 Aug 2025 14:50:18 +0200 Subject: [PATCH 7/8] Adding verification of json files, added config restore at bootup if broken --- wled00/cfg.cpp | 4 ++++ wled00/fcn_declare.h | 2 ++ wled00/file.cpp | 31 +++++++++++++++++++++++++++---- wled00/wled.cpp | 5 +++++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index a9abbd08d4..b7f7adc57a 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -782,6 +782,10 @@ bool restoreConfig() { return restoreFile(s_cfg_json); } +bool verifyConfig() { + return validateJsonFile(s_cfg_json); +} + // rename config file and reboot void resetConfig() { DEBUG_PRINTLN(F("Reset config")); diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 21cac2f80e..0cd28a31a0 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -26,6 +26,7 @@ void IRAM_ATTR touchButtonISR(); //cfg.cpp bool backupConfig(); bool restoreConfig(); +bool verifyConfig(); void resetConfig(); bool deserializeConfig(JsonObject doc, bool fromFS = false); bool deserializeConfigFromFS(); @@ -229,6 +230,7 @@ inline bool readObjectFromFile(const String &file, const char* key, JsonDocument bool copyFile(const char* src_path, const char* dst_path); bool backupFile(const char* filename); bool restoreFile(const char* filename); +bool validateJsonFile(const char* filename); void dumpFilesToSerial(); //hue.cpp diff --git a/wled00/file.cpp b/wled00/file.cpp index 1fa9ac92f1..13b18574fd 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -442,7 +442,7 @@ bool handleFileRead(AsyncWebServerRequest* request, String path){ // copy a file, delete destination file if incomplete to prevent corrupted files bool copyFile(const char* src_path, const char* dst_path) { - DEBUG_PRINTF("copyFile from %s to %s", src_path, dst_path); + DEBUG_PRINTF("copyFile from %s to %s\n", src_path, dst_path); if(!WLED_FS.exists(src_path)) { DEBUG_PRINTLN(F("file not found")); return false; @@ -481,8 +481,11 @@ bool copyFile(const char* src_path, const char* dst_path) { static const char s_backup_json[] PROGMEM = ".bkp.json"; bool backupFile(const char* filename) { - DEBUG_PRINTF("backup %s", filename); - if (!WLED_FS.exists(filename)) return false; + DEBUG_PRINTF("backup %s \n", filename); + if (!validateJsonFile(filename)) { + DEBUG_PRINTLN(F("broken file")); + return false; + } char backupname[32]; strcpy(backupname, filename); strcat(backupname, s_backup_json); @@ -496,7 +499,7 @@ bool backupFile(const char* filename) { } bool restoreFile(const char* filename) { - DEBUG_PRINTF("restore %s", filename); + DEBUG_PRINTF("restore %s \n", filename); char backupname[32]; strcpy(backupname, filename); strcat(backupname, s_backup_json); @@ -506,6 +509,11 @@ bool restoreFile(const char* filename) { return false; } + if (!validateJsonFile(backupname)) { + DEBUG_PRINTLN(F("broken backup")); + return false; + } + if (copyFile(backupname, filename)) { DEBUG_PRINTLN(F("restore ok")); return true; @@ -514,6 +522,21 @@ bool restoreFile(const char* filename) { return false; } +bool validateJsonFile(const char* filename) { + if (!WLED_FS.exists(filename)) return false; + File file = WLED_FS.open(filename, "r"); + if (!file) return false; + StaticJsonDocument<0> doc, filter; // https://arduinojson.org/v6/how-to/validate-json/ + bool result = deserializeJson(doc, file, DeserializationOption::Filter(filter)) == DeserializationError::Ok; + file.close(); + if (!result) { + DEBUG_PRINTF("Invalid JSON file %s\n", filename); + } else { + DEBUG_PRINTF("Valid JSON file %s\n", filename); + } + return result; +} + // print contents of all files in root dir to Serial except wsec files void dumpFilesToSerial() { File rootdir = WLED_FS.open("/", "r"); diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 4dc0cda9d0..717eec0fcc 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -425,6 +425,11 @@ void WLED::setup() WLED_SET_AP_SSID(); // otherwise it is empty on first boot until config is saved multiWiFi.push_back(WiFiConfig(CLIENT_SSID,CLIENT_PASS)); // initialise vector with default WiFi + if(!verifyConfig()) { + if(!restoreConfig()) { + resetConfig(); + } + } DEBUG_PRINTLN(F("Reading config")); bool needsCfgSave = deserializeConfigFromFS(); DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap()); From 04959480a2cc735e17ead096cca54aac125ebd47 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 15 Aug 2025 20:27:36 +0200 Subject: [PATCH 8/8] changed backup file name to bkp.file.json also added function to compare contents of two files for future use (currently not used) --- wled00/file.cpp | 45 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/wled00/file.cpp b/wled00/file.cpp index 13b18574fd..7ec9bd6152 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -478,7 +478,44 @@ bool copyFile(const char* src_path, const char* dst_path) { return success; } -static const char s_backup_json[] PROGMEM = ".bkp.json"; +// compare two files, return true if identical +bool compareFiles(const char* path1, const char* path2) { + DEBUG_PRINTF("compareFile %s and %s\n", path1, path2); + if (!WLED_FS.exists(path1) || !WLED_FS.exists(path2)) { + DEBUG_PRINTLN(F("file not found")); + return false; + } + + bool identical = true; // set to false on mismatch + File f1 = WLED_FS.open(path1, "r"); + File f2 = WLED_FS.open(path2, "r"); + + if (f1 && f2) { + uint8_t buf1[128], buf2[128]; + while (f1.available() > 0 || f2.available() > 0) { + size_t len1 = f1.read(buf1, sizeof(buf1)); + size_t len2 = f2.read(buf2, sizeof(buf2)); + + if (len1 != len2) { + identical = false; + break; // files differ in size or read failed + } + + if (memcmp(buf1, buf2, len1) != 0) { + identical = false; + break; // files differ in content + } + } + } else { + identical = false; // error opening files + } + + if (f1) f1.close(); + if (f2) f2.close(); + return identical; +} + +static const char s_backup_json[] PROGMEM = "/bkp."; bool backupFile(const char* filename) { DEBUG_PRINTF("backup %s \n", filename); @@ -487,8 +524,7 @@ bool backupFile(const char* filename) { return false; } char backupname[32]; - strcpy(backupname, filename); - strcat(backupname, s_backup_json); + snprintf(backupname, sizeof(backupname), "%s%s", s_backup_json, filename + 1); // skip leading '/' in filename if (copyFile(filename, backupname)) { DEBUG_PRINTLN(F("backup ok")); @@ -501,8 +537,7 @@ bool backupFile(const char* filename) { bool restoreFile(const char* filename) { DEBUG_PRINTF("restore %s \n", filename); char backupname[32]; - strcpy(backupname, filename); - strcat(backupname, s_backup_json); + snprintf(backupname, sizeof(backupname), "%s%s", s_backup_json, filename + 1); // skip leading '/' in filename if (!WLED_FS.exists(backupname)) { DEBUG_PRINTLN(F("no backup found"));