diff --git a/src/configuration/Configuration.cpp b/src/configuration/Configuration.cpp index cadca181b..32112d3e7 100644 --- a/src/configuration/Configuration.cpp +++ b/src/configuration/Configuration.cpp @@ -38,12 +38,32 @@ #define DIR_TOGGLES_OLD "/toggles" #define DIR_TOGGLES "/sensortoggles" +// Global variable defined at file scope (not in namespace) so init.h can access it +extern bool g_safeModeDeferredFactoryResetRequested; + namespace SlimeVR::Configuration { void Configuration::setup() { if (m_Loaded) { return; } + // Safe mode defers LittleFS formatting to normal runtime for reliability. + // If requested, perform the filesystem format before config load. + if (g_safeModeDeferredFactoryResetRequested) { + m_Logger.warn("Deferred safe-mode factory reset detected, formatting LittleFS"); + g_safeModeDeferredFactoryResetRequested = false; + + LittleFS.begin(); + if (!LittleFS.format()) { + m_Logger.error("Deferred LittleFS format failed"); + } else { + m_Logger.info("Deferred LittleFS format OK"); + m_Logger.warn("Rebooting after deferred safe-mode factory reset"); + ESP.restart(); + return; + } + } + bool status = LittleFS.begin(); if (!status) { this->m_Logger.warn("Could not mount LittleFS, formatting"); diff --git a/src/consts.h b/src/consts.h index 76119c5ae..82b136c86 100644 --- a/src/consts.h +++ b/src/consts.h @@ -98,6 +98,63 @@ enum class SensorTypeID : uint8_t { #define BOARD_ESP32S3_SUPERMINI 23 #define BOARD_DEV_RESERVED 250 // Reserved, should not be used in any release firmware +// Returns the BOARD define name (e.g. "BOARD_WEMOSD1MINI"). +// Kept Arduino-independent because this header is used outside Arduino units. +inline const char* boardName() { + switch (BOARD) { + case BOARD_SLIMEVR_LEGACY: + return "BOARD_SLIMEVR_LEGACY"; + case BOARD_SLIMEVR_DEV: + return "BOARD_SLIMEVR_DEV"; + case BOARD_NODEMCU: + return "BOARD_NODEMCU"; + case BOARD_CUSTOM: + return "BOARD_CUSTOM"; + case BOARD_WROOM32: + return "BOARD_WROOM32"; + case BOARD_WEMOSD1MINI: + return "BOARD_WEMOSD1MINI"; + case BOARD_TTGO_TBASE: + return "BOARD_TTGO_TBASE"; + case BOARD_ESP01: + return "BOARD_ESP01"; + case BOARD_SLIMEVR: + return "BOARD_SLIMEVR"; + case BOARD_LOLIN_C3_MINI: + return "BOARD_LOLIN_C3_MINI"; + case BOARD_BEETLE32C3: + return "BOARD_BEETLE32C3"; + case BOARD_ESP32C3DEVKITM1: + return "BOARD_ESP32C3DEVKITM1"; + case BOARD_OWOTRACK: + return "BOARD_OWOTRACK"; + case BOARD_WRANGLER: + return "BOARD_WRANGLER"; + case BOARD_MOCOPI: + return "BOARD_MOCOPI"; + case BOARD_WEMOSWROOM02: + return "BOARD_WEMOSWROOM02"; + case BOARD_XIAO_ESP32C3: + return "BOARD_XIAO_ESP32C3"; + case BOARD_HARITORA: + return "BOARD_HARITORA"; + case BOARD_ESP32C6DEVKITC1: + return "BOARD_ESP32C6DEVKITC1"; + case BOARD_GLOVE_IMU_SLIMEVR_DEV: + return "BOARD_GLOVE_IMU_SLIMEVR_DEV"; + case BOARD_GESTURES: + return "BOARD_GESTURES"; + case BOARD_SLIMEVR_V1_2: + return "BOARD_SLIMEVR_V1_2"; + case BOARD_ESP32S3_SUPERMINI: + return "BOARD_ESP32S3_SUPERMINI"; + case BOARD_DEV_RESERVED: + return "BOARD_DEV_RESERVED"; + default: + return "BOARD_UNKNOWN"; + } +} + #define BAT_EXTERNAL 1 #define BAT_INTERNAL 2 #define BAT_MCP3021 3 diff --git a/src/init.h b/src/init.h new file mode 100644 index 000000000..7c5cd737c --- /dev/null +++ b/src/init.h @@ -0,0 +1,304 @@ +/* + SlimeVR Code is placed under the MIT license + Copyright (c) 2021 Eiren Rain & SlimeVR contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +#include + +#ifdef ESP32 +#include "esp_chip_info.h" +#include "esp_intr_alloc.h" +#include "esp_system.h" +#include "nvs_flash.h" +#if defined(CONFIG_IDF_TARGET_ESP32C3) +#include "soc/rtc_cntl_reg.h" +#endif +#endif + +#define STRINGIFY(x) #x +#define TOSTRING(x) STRINGIFY(x) + +typedef struct rtc_mem { + uint32_t version; // RTC memory version + uint32_t rebootCount; // Number of reboots +} rtc_mem_t; + +bool g_safeModeDeferredFactoryResetRequested = false; + +void failSafeProductInfo(Stream* Serial) { + Serial->println(F("==== SLVR Product Info ====")); + Serial->println(String(F("PRODUCT_NAME: ")) + String(PRODUCT_NAME)); + Serial->println(String(F("VENDOR_NAME: ")) + String(VENDOR_NAME)); + Serial->println(String(F("VENDOR_URL: ")) + String(VENDOR_URL)); + Serial->println(String(F("Firmware update URL: ")) + String(UPDATE_ADDRESS)); + Serial->println(String(F("BOARD: ")) + String(BOARD)); + Serial->println(String(F("BOARD NAME: ")) + String(boardName())); + Serial->println(String(F("HARDWARE_MCU: ")) + String(HARDWARE_MCU)); + Serial->println(String(F("PROTOCOL_VERSION: ")) + String(PROTOCOL_VERSION)); + Serial->println(String(F("FIRMWARE_VERSION: ")) + String(FIRMWARE_VERSION)); + Serial->println(F("SENSOR_DESC_LIST: ")); + Serial->println(String(TOSTRING(SENSOR_DESC_LIST))); +} + +void failSafeBootInfo(Stream* Serial, uint32_t resetreason, rtc_mem_t* rtcMem) { + Serial->println(F("\r\n==== SLVR Boot ====")); + Serial->println(String(F("Reboot reason code: ")) + String(resetreason)); + Serial->println(String(F("Core Version: ")) + String(ESP.getCoreVersion())); + Serial->println(String(F("SDK version: ")) + String(ESP.getSdkVersion())); + Serial->println(String(F("Sketch MD5: ")) + String(ESP.getSketchMD5())); + Serial->println(String(F("RTC Memory Version: ")) + String(rtcMem->version)); + Serial->println( + String(F("RTC Memory Reboot Count: ")) + String(rtcMem->rebootCount) + ); + Serial->println(); + failSafeProductInfo(Serial); +} + +bool cmdSafeModeFlashMode(Stream* Serial) { +#if defined(ESP8266) + Serial->println(F("Entering flash mode...")); + // Serial needs to be stay active for + // rebootIntoUartDownloadMode to work. + // Found out over testing. + delay(1000); + ESP.rebootIntoUartDownloadMode(); + return true; + +#elif defined(CONFIG_IDF_TARGET_ESP32C3) + Serial->println(F("Entering flash mode...")); + // from https://esp32.com/viewtopic.php?t=33180 + delay(1000); + REG_WRITE(RTC_CNTL_OPTION1_REG, RTC_CNTL_FORCE_DOWNLOAD_BOOT); + esp_restart(); + return true; + +#else + Serial->println( + F("The Firmware does not support reboot into UART download mode for " + "this board.") + ); + Serial->println( + F("You need to manually enter flash mode by holding BOOT button while " + "resetting.") + ); +#endif + return false; // Not supported, caller should handle with a message +} + +void cmdSafeModeReboot() { +#if defined(ESP8266) + // During preinit, system_restart() (used by ESP.restart()) is asynchronous — + // it posts to the SDK task queue which hasn't started yet, causing a hang. + // ESP.reset() calls __real_system_restart_local() which is a direct hardware reset. + ESP.reset(); +#else + ESP.restart(); +#endif +} + +bool cmdSafeModeFactoryReset(Stream* Serial) { + // WiFi and LittleFS operations are not reliable during preinit() on either + // platform. Defer filesystem formatting to normal runtime. + g_safeModeDeferredFactoryResetRequested = true; + +#if defined(ESP8266) + ESP.eraseConfig(); // Erase SDK config sectors (WiFi credentials etc.) +#elif defined(ESP32) + nvs_flash_erase(); // Erase NVS (equivalent to ESP8266's config) +#endif + + if (Serial) { + Serial->println(F("LittleFS format deferred to runtime.")); + Serial->println(F("Continuing normal boot for format + final reboot...")); + Serial->flush(); + } + return true; +} + +void cmdSafeModeHelp(Stream* Serial) { + Serial->println(F("Available commands:")); + Serial->println( + F(" FRST - Factory reset (clears WiFi credentials " + "and config, then reboots)") + ); + Serial->println(F(" REBOOT - Restart the device")); + Serial->println(F(" SET FLASHMODE - Enter UART download / flash mode")); + Serial->println(F(" HELP - Show this help message")); +} + +// Maximum input line length — longest valid command is "SET FLASHMODE" (13 chars). +// Anything beyond this is serial noise or garbage; discard to protect the heap. +// This is especially important because mainSave() runs during preinit/initVariant +// before full system initialization. +#define MAINSAVE_CMD_MAXLEN 32 + +// mainSave is for all the Shell needed loop in case of safe mode +bool mainSave(Stream* Serial) { + String inputBuffer = ""; + + Serial->println(); + Serial->println(F("=== SLVR Emergency Shell ===")); + Serial->println(F("Safe mode command prompt. Type 'HELP' for available commands.")); + cmdSafeModeHelp(Serial); + + while (true) { + while (Serial->available()) { + char c = (char)Serial->read(); + if (c == '\r') { + continue; // ignore carriage return + } + if (c == '\n') { + inputBuffer.trim(); + String cmd = inputBuffer; + cmd.toUpperCase(); + inputBuffer = ""; + + if (cmd == "FRST") { + Serial->println(F("Performing factory reset...")); + if (cmdSafeModeFactoryReset(Serial)) { + return true; + } + } else if (cmd == "REBOOT") { + Serial->println(F("Rebooting...")); + cmdSafeModeReboot(); + } else if (cmd == "SET FLASHMODE") { + cmdSafeModeFlashMode(Serial); + } else if (cmd == "HELP") { + cmdSafeModeHelp(Serial); + } else if (cmd.length() > 0) { + Serial->print(F("Unknown command: '")); + Serial->print(cmd); + Serial->println(F("'. Type 'HELP' for available commands.")); + } + } else { + if (inputBuffer.length() >= MAINSAVE_CMD_MAXLEN) { + // Buffer overflow — likely serial noise or garbage data. Discard + // and warn. + Serial->println(F("")); + Serial->println(F("Input too long, discarding line.")); + inputBuffer = ""; + } else { + inputBuffer += c; + } + } + } + +#if defined(ESP8266) + ESP.wdtFeed(); // Feed the watchdog to prevent reset while in this loop +#endif + delay(1); + } +} + +#ifdef ESP8266 +#include + +extern "C" void preinit(void) { + HardwareSerial Serial(0); + struct rst_info* resetreason; + rtc_mem_t rtcMem; + resetreason = ESP.getResetInfoPtr(); + + Serial.begin(115200); + + // Offset 33 to avoid eboot command area + bool rtcOk = ESP.rtcUserMemoryRead(33, (uint32_t*)&rtcMem, sizeof(struct rtc_mem)); + + if (!rtcOk || rtcMem.version != 0x01) { + // First boot, initialize RTC memory + rtcMem.version = 0x01; + rtcMem.rebootCount = 0; + } + + failSafeBootInfo(&Serial, resetreason->reason, &rtcMem); + + if (resetreason->reason != REASON_SOFT_WDT_RST + && resetreason->reason != REASON_EXCEPTION_RST + && resetreason->reason != REASON_WDT_RST) { + // Not a crash, reset reboot counter + rtcMem.rebootCount = 0; + } else { + // Crash detected, increment reboot counter + rtcMem.rebootCount++; + + // If more than 3 consecutive crashes, enter safe mode + if (rtcMem.rebootCount >= 3) { + // Boot into UART download mode + Serial.println(); + Serial.println(); + Serial.println(F("Entering safe mode due to repeated crashes.")); + Serial.println(F("Starting Emergency shell...")); + if (mainSave(&Serial)) { + Serial.println(F("Leaving emergency shell and continuing boot...")); + } else { + return; + } + } + } + ESP.rtcUserMemoryWrite(33, (uint32_t*)&rtcMem, sizeof(struct rtc_mem)); + + Serial.println(F("=== SLVR Boot end ===")); + Serial.flush(); + // Deinit UART for main code to reinitialize + Serial.end(); +} +#endif + +#ifdef ESP32 +// infos from https://circuitlabs.net/rtc-memory-usage-in-esp-idf/ +RTC_DATA_ATTR rtc_mem_t rtcMem; + +extern "C" void initVariant(void) { + esp_reset_reason_t resetreason; + + resetreason = esp_reset_reason(); + + if (rtcMem.version != 0x01) { + // First boot, initialize RTC memory + rtcMem.version = 0x01; + rtcMem.rebootCount = 0; + } + + // don't need to read RTC memory as it is cleard on a WDT reset on ESP32 + Serial.begin(115200); + failSafeBootInfo(&Serial, resetreason, &rtcMem); + + if (resetreason != ESP_RST_PANIC && resetreason != ESP_RST_INT_WDT + && resetreason != ESP_RST_TASK_WDT && resetreason != ESP_RST_WDT + && resetreason != ESP_RST_CPU_LOCKUP) { + // Not a crash, reset reboot counter + rtcMem.rebootCount = 0; + } else { + // Crash detected + // ESP32 ram gets cleared on WDT reset, force safe mode + rtcMem.rebootCount = 4; + + // If more than 3 consecutive crashes, enter safe mode + if (rtcMem.rebootCount >= 3) { + // Boot into UART download mode + mainSave(&Serial); + } + } + + Serial.println(F("=== SLVR Boot end ===")); + Serial.flush(); +} +#endif diff --git a/src/main.cpp b/src/main.cpp index d8589cdf6..c32e901a5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -29,6 +29,7 @@ #include "credentials.h" #include "debugging/TimeTaken.h" #include "globals.h" +#include "init.h" #include "logging/Logger.h" #include "ota.h" #include "serial/serialcommands.h" diff --git a/src/serial/serialcommands.cpp b/src/serial/serialcommands.cpp index 08f9f7f62..dea14f339 100644 --- a/src/serial/serialcommands.cpp +++ b/src/serial/serialcommands.cpp @@ -33,6 +33,9 @@ #ifdef ESP32 #include "nvs_flash.h" +#if defined(CONFIG_IDF_TARGET_ESP32C3) +#include "soc/rtc_cntl_reg.h" +#endif #endif #ifdef EXT_SERIAL_COMMANDS @@ -158,6 +161,20 @@ void cmdSet(CmdParser* parser) { wifiNetwork.setWiFiCredentials(ssid, ppass); logger.info("CMD SET BWIFI OK: New wifi credentials set, reconnecting"); } + } else if (parser->equalCmdParam(1, "FLASHMODE")) { +#if defined(ESP8266) + logger.info("Entering flash mode..."); + delay(1000); + ESP.rebootIntoUartDownloadMode(); +#elif defined(CONFIG_IDF_TARGET_ESP32C3) + logger.info("Entering flash mode..."); + delay(1000); + // from https://esp32.com/viewtopic.php?t=33180 + REG_WRITE(RTC_CNTL_OPTION1_REG, RTC_CNTL_FORCE_DOWNLOAD_BOOT); + esp_restart(); +#else + logger.info("Flash mode not supported on this platform!"); +#endif } else { logger.error("CMD SET ERROR: Unrecognized variable to set"); }