Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No fs format nor file recreation and boot reason changes #656

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
70 changes: 66 additions & 4 deletions src/Wippersnapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
*/
Expand Down Expand Up @@ -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");
Expand All @@ -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.
Expand All @@ -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() {
Expand Down
2 changes: 2 additions & 0 deletions src/Wippersnapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
168 changes: 117 additions & 51 deletions src/provisioning/tinyusb/Wippersnapper_FS.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -105,17 +106,33 @@ 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()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am confused about tracing the brownout detection throughout the FS class.

Could you help clarify the following:

  1. I see brownOutCausedReset is set within get_and_print_reset_reason_for_cpu().
  2. When does this function get called within FS? I don't see any calls outwards to this function, only the boolean brownOutCausedReset which isn't a function

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.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please add some descriptive text/instructions on what to do if we couldn't recreate the FS after a brownout?

}
} else if (!WS.brownOutCausedReset && !initFilesystem(true)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a brownout was detected, all the handling for this was performed inside the if condition above and we don't need to work with/handle it anymore.

Consider changing this to an if and only handle the initFileSystem(true case.

setStatusLEDColor(RED);
fsHalt("ERROR Initializing Filesystem");
}
}

// Initialize USB-MSD
initUSBMSC();

// If we created a new filesystem, halt until user RESETs device.
tyeth marked this conversation as resolved.
Show resolved Hide resolved
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);
}
Comment on lines +131 to +134
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great, I like the idea of resetting instead of instructing the user to reset!

Shorten this by doing:

for (;;) delay(1000);

}
}

/************************************************************/
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to do this within eraseCPFS, as the vlume was likely named CIRCUITPY?

// 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/");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we're in it, let's refactor out the macOS-specific handling to a new function, handleMacOS(). This gives us room for handling other OS like Windows, Linux, ChromeOs the same way.

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to check if a brownout happened here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still check the bootlog contents in createBootfile, and print if there is a discrepency. In the event of a failure to write bootlog, assuming the secrets are still valid, we don't want to call it a failure by returning false

return false;

// Check if secrets.json file already exists
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we checking for a brownout here, again?

Copy link
Contributor Author

@tyeth tyeth Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We wanted to inform that the bootlog isn't correct but we can't update it. I guess I could return true instead, or comment this as to why

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!");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think we'll see this except in debug builds with Tx monitored. The next PR (#655) will resolve this.

}

return is_success;
}

Expand Down Expand Up @@ -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(
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we decreasing this delay? It was from TinyUSB Arduino

initUSBMSC(); // re-init USB MSC to show new file to user for editing
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Where did we detach USB MSC?
  2. Is this called twice? We shouldn't be.
  • Do we call this again, outside of this function, after this is called?

fsHalt("ERROR: Please edit the secrets.json file. Then, reset your board.");
}

Expand All @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A special case to handle the brownout detection shouldn't be located here.

We should only handle the brownout detection/errors within the class constructor/init.

{
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());
Expand Down