diff --git a/platformio.ini b/platformio.ini index dd4acafc..30814818 100644 --- a/platformio.ini +++ b/platformio.ini @@ -91,7 +91,7 @@ lib_deps = ; Common build environment for ESP32 platform [common:esp32] platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.07/platform-espressif32.zip -; This is needed for occasional new features and bug fixes +; develop branch is needed for occasional new features and bug fixes, main may be preferred ; platform = https://github.com/pioarduino/platform-espressif32#develop lib_ignore = WiFiNINA, WiFi101, OneWire monitor_filters = esp32_exception_decoder, time diff --git a/src/Wippersnapper.cpp b/src/Wippersnapper.cpp index a4c19082..fc0b2a36 100644 --- a/src/Wippersnapper.cpp +++ b/src/Wippersnapper.cpp @@ -2644,9 +2644,10 @@ void Wippersnapper::publish(const char *topic, uint8_t *payload, uint16_t bLen, } } +#ifdef ARDUINO_ARCH_ESP32 /**************************************************************/ /*! - @brief Prints last reset reason of ESP32 + @brief Prints string reset reason of ESP32 @param reason The return code of rtc_get_reset_reason(coreNum) */ @@ -2696,6 +2697,7 @@ void print_reset_reason(int reason) { break; /**<14, for APP CPU, reseted by PRO CPU*/ case 15: WS_DEBUG_PRINTLN("RTCWDT_BROWN_OUT_RESET"); + WS.brownOutCausedReset = true; break; /**<15, Reset when the vdd voltage is not stable*/ case 16: WS_DEBUG_PRINTLN("RTCWDT_RTC_RESET"); @@ -2705,6 +2707,64 @@ void print_reset_reason(int reason) { } } +#if CONFIG_IDF_TARGET_ESP32 // ESP32/PICO-D4 +#include "esp32/rom/rtc.h" +#elif CONFIG_IDF_TARGET_ESP32S2 +#include "esp32s2/rom/rtc.h" +#elif CONFIG_IDF_TARGET_ESP32C2 +#include "esp32c2/rom/rtc.h" +#elif CONFIG_IDF_TARGET_ESP32C3 +#include "esp32c3/rom/rtc.h" +#elif CONFIG_IDF_TARGET_ESP32S3 +#include "esp32s3/rom/rtc.h" +#elif CONFIG_IDF_TARGET_ESP32C6 +#include "esp32c6/rom/rtc.h" +#else +#error Target CONFIG_IDF_TARGET is not supported +#endif + +/**************************************************************************/ +/*! + @brief Prints the reason why the ESP32 CPU was reset. + @param cpuCore + The core number to print the reset reason for. +*/ +/**************************************************************************/ +void get_and_print_reset_reason_for_cpu(int cpuCore) { + print_reset_reason(rtc_get_reset_reason(cpuCore)); +} + +// end of ARDUINO_ARCH_ESP32 +#elif defined(ARDUINO_ARCH_RP2040) + +void print_reset_reason() { + RP2040::resetReason_t reason = rp2040.getResetReason(); + WS_DEBUG_PRINT("RP2040 RESET REASON: "); + switch (reason) { + case RP2040::resetReason_t::UNKNOWN_RESET: + WS_DEBUG_PRINTLN("Unknown Reset"); + case RP2040::resetReason_t::PWRON_RESET: + WS_DEBUG_PRINTLN("Power-On Reset"); + case RP2040::resetReason_t::RUN_PIN_RESET: + WS_DEBUG_PRINTLN("Run Pin Reset"); + case RP2040::resetReason_t::SOFT_RESET: + WS_DEBUG_PRINTLN("Soft Reset"); + case RP2040::resetReason_t::WDT_RESET: + WS_DEBUG_PRINTLN("Watchdog Timer Reset"); + case RP2040::resetReason_t::DEBUG_RESET: + WS_DEBUG_PRINTLN("Debug Reset"); + case RP2040::resetReason_t::GLITCH_RESET: + WS_DEBUG_PRINTLN("Glitch Reset"); + case RP2040::resetReason_t::BROWNOUT_RESET: + WS.brownOutCausedReset = true; + WS_DEBUG_PRINTLN("Brownout Reset"); + default: + WS_DEBUG_PRINTLN("Unknown Reset Reason"); + } +} + +#endif + /**************************************************************************/ /*! @brief Prints information about the WS device to the serial monitor. @@ -2731,15 +2791,17 @@ void printDeviceInfo() { // (ESP32-Only) Print reason why device was reset #ifdef ARDUINO_ARCH_ESP32 WS_DEBUG_PRINT("ESP32 CPU0 RESET REASON: "); - print_reset_reason(0); + get_and_print_reset_reason_for_cpu(0); WS_DEBUG_PRINT("ESP32 CPU1 RESET REASON: "); - print_reset_reason(1); + get_and_print_reset_reason_for_cpu(1); +#elif defined(ARDUINO_ARCH_RP2040) || defined(PICO_RP2350) + print_reset_reason(); #endif } /**************************************************************************/ /*! - @brief Connects to Adafruit IO+ Wippersnapper broker. + @brief Connects to Adafruit IO Wippersnapper broker. */ /**************************************************************************/ void Wippersnapper::connect() { diff --git a/src/Wippersnapper.h b/src/Wippersnapper.h index 0c418eab..730a9749 100644 --- a/src/Wippersnapper.h +++ b/src/Wippersnapper.h @@ -254,6 +254,8 @@ class Wippersnapper { void provision(); + bool brownOutCausedReset = + false; ///< True if low power reset - flash write issues bool lockStatusNeoPixel; ///< True if status LED is using the status neopixel bool lockStatusDotStar; ///< True if status LED is using the status dotstar bool lockStatusLED; ///< True if status LED is using the built-in LED diff --git a/src/provisioning/tinyusb/Wippersnapper_FS.cpp b/src/provisioning/tinyusb/Wippersnapper_FS.cpp index d99c6098..2f6326af 100644 --- a/src/provisioning/tinyusb/Wippersnapper_FS.cpp +++ b/src/provisioning/tinyusb/Wippersnapper_FS.cpp @@ -97,6 +97,7 @@ Wippersnapper_FS::Wippersnapper_FS() { WS_DEBUG_PRINTLN(project_dependencies); WS_DEBUG_PRINTLN("*********************"); WS_PRINTER.flush(); + delay(50); // give host a chance to finish reading serial buffer #endif // Detach USB device during init. TinyUSBDevice.detach(); @@ -105,9 +106,18 @@ Wippersnapper_FS::Wippersnapper_FS() { // If a filesystem does not already exist - attempt to initialize a new // filesystem - if (!initFilesystem() && !initFilesystem(true)) { - setStatusLEDColor(RED); - fsHalt("ERROR Initializing Filesystem"); + if (!initFilesystem()) { + if (WS.brownOutCausedReset) { + // try once more for good measure + delay(10); // let power stablise after failure + if (!initFilesystem()) { + // no lights, save power as we're probably on a low battery + fsHalt("Brownout detected. Couldn't initialise filesystem."); + } + } else if (!WS.brownOutCausedReset && !initFilesystem(true)) { + setStatusLEDColor(RED); + fsHalt("ERROR Initializing Filesystem"); + } } // Initialize USB-MSD @@ -115,7 +125,14 @@ Wippersnapper_FS::Wippersnapper_FS() { // If we created a new filesystem, halt until user RESETs device. if (_freshFS) - fsHalt("New filesystem created! Press the reset button on your board."); + { + WS_DEBUG_PRINTLN("New filesystem created! Resetting the board shortly..."); + WS.enableWDT(500); + while (1) + { + delay(1000); + } + } } /************************************************************/ @@ -159,32 +176,39 @@ bool Wippersnapper_FS::initFilesystem(bool force_format) { if (!wipperFatFs.begin(&flash)) return false; + //TODO: Don't do this unless we need the space and createSecrets fails // If CircuitPython was previously installed - erase CPY FS eraseCPFS(); - - // If WipperSnapper was previously installed - remove the - // wippersnapper_boot_out.txt file - eraseBootFile(); + // Also, should probably relabel drive to WIPPER if CIRCUITPY was there + // using setVolumeLabel(), but note the FS must be unmounted first // No file indexing on macOS - wipperFatFs.mkdir("/.fseventsd/"); - File32 writeFile = wipperFatFs.open("/.fseventsd/no_log", FILE_WRITE); - if (!writeFile) - return false; - writeFile.close(); - - writeFile = wipperFatFs.open("/.metadata_never_index", FILE_WRITE); - if (!writeFile) - return false; - writeFile.close(); + if (!wipperFatFs.exists("/.fseventsd/no_log")) + { + wipperFatFs.mkdir("/.fseventsd/"); + File32 writeFile = wipperFatFs.open("/.fseventsd/no_log", FILE_WRITE); + if (!writeFile) + return false; + writeFile.close(); + } - writeFile = wipperFatFs.open("/.Trashes", FILE_WRITE); - if (!writeFile) - return false; - writeFile.close(); + if (!wipperFatFs.exists("/.metadata_never_index")) + { + File32 writeFile = wipperFatFs.open("/.metadata_never_index", FILE_WRITE); + if (!writeFile) + return false; + writeFile.close(); + } + if (!wipperFatFs.exists("/.Trashes")) + { + File32 writeFile = wipperFatFs.open("/.Trashes", FILE_WRITE); + if (!writeFile) + return false; + writeFile.close(); + } // Create wippersnapper_boot_out.txt file - if (!createBootFile()) + if (!createBootFile() && !WS.brownOutCausedReset) return false; // Check if secrets.json file already exists @@ -241,6 +265,9 @@ bool Wippersnapper_FS::configFileExists() { */ /**************************************************************************/ void Wippersnapper_FS::eraseCPFS() { + if (WS.brownOutCausedReset){ + return; // Can't serial print here, in next PR check we're not out of space + } if (wipperFatFs.exists("/boot_out.txt")) { wipperFatFs.remove("/boot_out.txt"); wipperFatFs.remove("/code.py"); @@ -269,44 +296,66 @@ void Wippersnapper_FS::eraseBootFile() { bool Wippersnapper_FS::createBootFile() { bool is_success = false; char sMAC[18] = {0}; + String newContent; - File32 bootFile = wipperFatFs.open("/wipper_boot_out.txt", FILE_WRITE); - if (bootFile) { - bootFile.println("Adafruit.io WipperSnapper"); + // Generate new content + newContent += "Adafruit.io WipperSnapper\n"; + newContent += "Firmware Version: " + String(WS_VERSION) + "\n"; + newContent += "Board ID: " + String(BOARD_ID) + "\n"; + sprintf(sMAC, "%02X:%02X:%02X:%02X:%02X:%02X", WS._macAddr[0], + WS._macAddr[1], WS._macAddr[2], WS._macAddr[3], WS._macAddr[4], + WS._macAddr[5]); + newContent += "MAC Address: " + String(sMAC) + "\n"; - bootFile.print("Firmware Version: "); - bootFile.println(WS_VERSION); +#if PRINT_DEPENDENCIES + newContent += ("Build dependencies:\n"); + newContent += (project_dependencies); + newContent += ("\n"); +#endif - bootFile.print("Board ID: "); - bootFile.println(BOARD_ID); + #ifdef ARDUINO_ARCH_ESP32 + newContent += "ESP-IDF Version: " + String(ESP.getSdkVersion()) + "\n"; + newContent += "ESP32 Core Version: " + String(ESP.getCoreVersion()) + "\n"; + #endif - sprintf(sMAC, "%02X:%02X:%02X:%02X:%02X:%02X", WS._macAddr[0], - WS._macAddr[1], WS._macAddr[2], WS._macAddr[3], WS._macAddr[4], - WS._macAddr[5]); - bootFile.print("MAC Address: "); - bootFile.println(sMAC); + // Check if the file exists and read its content + File32 bootFile = wipperFatFs.open("/wipper_boot_out.txt", FILE_READ); + if (bootFile) { + String existingContent; + while (bootFile.available()) { + existingContent += char(bootFile.read()); + } + bootFile.close(); -#if PRINT_DEPENDENCIES - bootFile.println("Build dependencies:"); - bootFile.println(project_dependencies); -#endif + // Compare existing content with new content + if (existingContent == newContent) { + WS_DEBUG_PRINTLN("INFO: wipper_boot_out.txt already exists with the " + "same content."); + return true; // No need to overwrite + } else { + WS_DEBUG_PRINTLN("INFO: wipper_boot_out.txt exists but with different " + "content. Overwriting..."); + } + } else { + WS_DEBUG_PRINTLN("INFO: could not open wipper_boot_out.txt for reading. " + "Creating new file..."); + } - // Print ESP-specific info to boot file - #ifdef ARDUINO_ARCH_ESP32 - // Get version of ESP-IDF - bootFile.print("ESP-IDF Version: "); - bootFile.println(ESP.getSdkVersion()); - // Get version of this core - bootFile.print("ESP32 Core Version: "); - bootFile.println(ESP.getCoreVersion()); - #endif + if (WS.brownOutCausedReset){ + return false; + } + // Overwrite the file with new content + bootFile = wipperFatFs.open("/wipper_boot_out.txt", FILE_WRITE); + if (bootFile) { + bootFile.print(newContent); bootFile.flush(); bootFile.close(); is_success = true; } else { - bootFile.close(); + WS_DEBUG_PRINTLN("ERROR: Unable to open wipper_boot_out.txt for writing!"); } + return is_success; } @@ -337,7 +386,6 @@ void Wippersnapper_FS::createSecretsFile() { // Flush and close file secretsFile.flush(); secretsFile.close(); - delay(2500); // Signal to user that action must be taken (edit secrets.json) writeToBootOut( @@ -349,6 +397,8 @@ void Wippersnapper_FS::createSecretsFile() { "Please edit it to reflect your Adafruit IO and network credentials. " "When you're done, press RESET on the board."); #endif + delay(500); // previously 2500 + initUSBMSC(); // re-init USB MSC to show new file to user for editing fsHalt("ERROR: Please edit the secrets.json file. Then, reset your board."); } @@ -367,7 +417,23 @@ void Wippersnapper_FS::parseSecrets() { // Attempt to deserialize the file's JSON document JsonDocument doc; DeserializationError error = deserializeJson(doc, secretsFile); - if (error) { + if (error == DeserializationError::EmptyInput) + { + if (WS.brownOutCausedReset) + { + fsHalt("ERROR: Empty secrets.json file, can't recreate due to brownout - recharge or must be fixed manually."); + } + else + { + // TODO: Can't serial print here, in next PR check we're not out of space + WS_DEBUG_PRINTLN("ERROR: Empty secrets.json file, recreating..."); + secretsFile.close(); + wipperFatFs.remove("/secrets.json"); + createSecretsFile(); // calls fsHalt + } + } + else if (error) + { fsHalt(String("ERROR: Unable to parse secrets.json file - " "deserializeJson() failed with code") + error.c_str());