From 7acc31bc34fdbd6c975b7ff4c70a04a86913613e Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Fri, 15 May 2026 17:27:54 -0700 Subject: [PATCH] fix: silent-reboot on wifi activity exit to clear heap fragmentation (#1908) WiFi/LWIP/netif teardown scatters long-lived allocations across the heap, leaving ~50KB of contiguous space unrecoverable without a reboot. Reboot the SoC on exit from any wifi-using activity to guarantee a clean heap. An RTC_NOINIT flag survives the reboot and tells setup() to skip the boot splash and route the user back where they came from: - File transfer / Calibre / OPDS / Font download -> home - KOReader sync -> currently-open EPUB Activities check WiFi.getMode() before rebooting, so backing out of the network mode menu without joining doesn't trigger a cycle. KOSync also esp_wifi_stop()s after the sync result so the radio is off while the user reads it; full teardown happens at the reboot. ## Additional Context The silent reboot skips the booting splash screen - it visibly looks like a screen refresh. This does cause a disconnection/reconnection blip for developers actively pulling logs over serial, but `pio device monitor` and the like successfully reconnect and feed in the early boot serial. as an example: ``` [256676] [DBG] [ACT] Exiting activity: KOReaderSync [256706] [DBG] [MAIN] Silent restart (target=reader) ESP-ROM:esp32c3-api1-20210207 Build:Feb 7 2021 rst:0xc (RTC_SW_CPU_RST),boot:0xf (SPI_FAST_FLASH_BOOT) Saved PC:0x403872bc SPIWP:0xee mode:DIO, clock div:1 load:0x3fcd72a0,len:0x990 load:0x403cbf10,len:0xac8 load:0x403ce710,len:0x4d28 entry 0x403cbf10 [22] [INF] [MAIN] Hardware detect: X4 [29] [SD] SD card detected [43] [DBG] [CPS] Settings loaded from file [58] [DBG] [KRS] Loaded KOReader credentials for user: jeremydk [69] [DBG] [OPS] Loaded 1 OPDS servers from file [69] [DBG] [UI] Using Lyra theme [70] [DBG] [MAIN] Starting CrossPoint version 1.2.0-dev-detached-bde75787 ... [203] [DBG] [ACT] Entering activity: Reader [211] [DBG] [EBP] Loading ePub: /Halting State - Charles Stross.epub [221] [DBG] [BMC] Loaded cache data: 51 spine, 41 TOC entries [246] [DBG] [CSS] Loaded 41 rules from cache [247] [DBG] [EBP] Loaded ePub: /Halting State - Charles Stross.epub ``` --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _PARTIALLY_ --- src/SilentRestart.h | 8 ++++ .../browser/OpdsBookBrowserActivity.cpp | 11 +++-- .../network/CalibreConnectActivity.cpp | 14 +++--- .../network/CrossPointWebServerActivity.cpp | 48 ++++--------------- .../network/CrossPointWebServerActivity.h | 1 - .../reader/KOReaderSyncActivity.cpp | 25 +++++----- src/activities/reader/KOReaderSyncActivity.h | 6 +++ .../settings/FontDownloadActivity.cpp | 11 +++-- src/main.cpp | 45 ++++++++++++++++- 9 files changed, 102 insertions(+), 67 deletions(-) create mode 100644 src/SilentRestart.h diff --git a/src/SilentRestart.h b/src/SilentRestart.h new file mode 100644 index 00000000..f94345c5 --- /dev/null +++ b/src/SilentRestart.h @@ -0,0 +1,8 @@ +#pragma once + +// ESP.restart() with an RTC_NOINIT flag that survives the reboot, so setup() +// skips the boot splash and routes straight to a destination. Used to clear +// heap fragmentation accumulated during a wifi session. + +void silentRestart(); // home screen +void silentRestartToReader(); // currently-open EPUB (APP_STATE.openEpubPath) diff --git a/src/activities/browser/OpdsBookBrowserActivity.cpp b/src/activities/browser/OpdsBookBrowserActivity.cpp index 96e4e28c..b1d5a8d4 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.cpp +++ b/src/activities/browser/OpdsBookBrowserActivity.cpp @@ -8,6 +8,7 @@ #include #include "MappedInputManager.h" +#include "SilentRestart.h" #include "activities/network/WifiSelectionActivity.h" #include "activities/util/KeyboardEntryActivity.h" #include "components/UITheme.h" @@ -40,9 +41,14 @@ void OpdsBookBrowserActivity::onEnter() { void OpdsBookBrowserActivity::onExit() { Activity::onExit(); - WiFi.mode(WIFI_OFF); entries.clear(); navigationHistory.clear(); + + if (WiFi.getMode() != WIFI_MODE_NULL) { + WiFi.disconnect(false); + delay(30); + silentRestart(); + } } void OpdsBookBrowserActivity::loop() { @@ -364,8 +370,7 @@ void OpdsBookBrowserActivity::onWifiSelectionComplete(const bool connected) { requestUpdate(true); fetchFeed(currentPath); } else { - WiFi.disconnect(); - WiFi.mode(WIFI_OFF); + // Leave WiFi up; onExit's silent reboot handles teardown without fragmenting. state = BrowserState::ERROR; errorMessage = tr(STR_WIFI_CONN_FAILED); requestUpdate(); diff --git a/src/activities/network/CalibreConnectActivity.cpp b/src/activities/network/CalibreConnectActivity.cpp index a42ab16e..ca8b5199 100644 --- a/src/activities/network/CalibreConnectActivity.cpp +++ b/src/activities/network/CalibreConnectActivity.cpp @@ -7,6 +7,7 @@ #include #include "MappedInputManager.h" +#include "SilentRestart.h" #include "WifiSelectionActivity.h" #include "components/UITheme.h" #include "fontIds.h" @@ -51,14 +52,11 @@ void CalibreConnectActivity::onEnter() { void CalibreConnectActivity::onExit() { Activity::onExit(); - stopWebServer(); - MDNS.end(); - - delay(50); - WiFi.disconnect(false); - delay(30); - WiFi.mode(WIFI_OFF); - delay(30); + if (WiFi.getMode() != WIFI_MODE_NULL) { + WiFi.disconnect(false); + delay(30); + silentRestart(); + } } void CalibreConnectActivity::onWifiSelectionComplete(const bool connected) { diff --git a/src/activities/network/CrossPointWebServerActivity.cpp b/src/activities/network/CrossPointWebServerActivity.cpp index 71f1bf41..44ccfb5b 100644 --- a/src/activities/network/CrossPointWebServerActivity.cpp +++ b/src/activities/network/CrossPointWebServerActivity.cpp @@ -11,6 +11,7 @@ #include "MappedInputManager.h" #include "NetworkModeSelectionActivity.h" +#include "SilentRestart.h" #include "WifiSelectionActivity.h" #include "activities/network/CalibreConnectActivity.h" #include "components/UITheme.h" @@ -75,37 +76,17 @@ void CrossPointWebServerActivity::onExit() { state = WebServerActivityState::SHUTTING_DOWN; - // Stop the web server first (before disconnecting WiFi) - stopWebServer(); - - // Stop mDNS - MDNS.end(); - - // Stop DNS server if running (AP mode) - if (dnsServer) { - LOG_DBG("WEBACT", "Stopping DNS server..."); - dnsServer->stop(); - delete dnsServer; - dnsServer = nullptr; + // Skip reboot if WiFi was never activated (e.g. user backed out of mode selection). + if (WiFi.getMode() != WIFI_MODE_NULL) { + if (isApMode) { + WiFi.softAPdisconnect(true); + } else { + WiFi.disconnect(false); + } + delay(30); + silentRestart(); } - // Brief wait for LWIP stack to flush pending packets - delay(50); - - // Disconnect WiFi gracefully - if (isApMode) { - LOG_DBG("WEBACT", "Stopping WiFi AP..."); - WiFi.softAPdisconnect(true); - } else { - LOG_DBG("WEBACT", "Disconnecting WiFi (graceful)..."); - WiFi.disconnect(false); // false = don't erase credentials, send disconnect frame - } - delay(30); // Allow disconnect frame to be sent - - LOG_DBG("WEBACT", "Setting WiFi mode OFF..."); - WiFi.mode(WIFI_OFF); - delay(30); // Allow WiFi hardware to power down - LOG_DBG("WEBACT", "Free heap at onExit end: %d bytes", ESP.getFreeHeap()); } @@ -270,15 +251,6 @@ void CrossPointWebServerActivity::startWebServer() { } } -void CrossPointWebServerActivity::stopWebServer() { - if (webServer && webServer->isRunning()) { - LOG_DBG("WEBACT", "Stopping web server..."); - webServer->stop(); - LOG_DBG("WEBACT", "Web server stopped"); - } - webServer.reset(); -} - void CrossPointWebServerActivity::loop() { // Handle different states if (state == WebServerActivityState::SERVER_RUNNING) { diff --git a/src/activities/network/CrossPointWebServerActivity.h b/src/activities/network/CrossPointWebServerActivity.h index bc685c6b..557bf443 100644 --- a/src/activities/network/CrossPointWebServerActivity.h +++ b/src/activities/network/CrossPointWebServerActivity.h @@ -59,7 +59,6 @@ class CrossPointWebServerActivity final : public Activity { void onWifiSelectionComplete(bool connected); void startAccessPoint(); void startWebServer(); - void stopWebServer(); public: explicit CrossPointWebServerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) diff --git a/src/activities/reader/KOReaderSyncActivity.cpp b/src/activities/reader/KOReaderSyncActivity.cpp index d4451036..ec4ec845 100644 --- a/src/activities/reader/KOReaderSyncActivity.cpp +++ b/src/activities/reader/KOReaderSyncActivity.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -16,6 +17,7 @@ #include "KOReaderDocumentId.h" #include "MappedInputManager.h" #include "ReaderUtils.h" +#include "SilentRestart.h" #include "activities/ActivityManager.h" #include "activities/network/WifiSelectionActivity.h" #include "components/UITheme.h" @@ -47,15 +49,6 @@ void syncTimeWithNTP() { LOG_DBG("KOSync", "NTP sync timeout, using fallback"); } } -void wifiOff() { - if (esp_sntp_enabled()) { - esp_sntp_stop(); - } - WiFi.disconnect(false); - delay(100); - WiFi.mode(WIFI_OFF); - delay(100); -} } // namespace void KOReaderSyncActivity::ensureEpubLoaded() { @@ -269,8 +262,10 @@ void KOReaderSyncActivity::performUpload() { const auto result = KOReaderSyncClient::updateProgress(progress); + // Drop the radio while user reads the result; full teardown happens at silent reboot. + esp_wifi_stop(); + if (result != KOReaderSyncClient::OK) { - wifiOff(); { RenderLock lock(*this); state = SYNC_FAILED; @@ -280,7 +275,6 @@ void KOReaderSyncActivity::performUpload() { return; } - wifiOff(); { RenderLock lock(*this); state = UPLOAD_COMPLETE; @@ -299,6 +293,9 @@ void KOReaderSyncActivity::onEnter() { return; } + // Past this point every path uses WiFi. + wifiActivated = true; + // Check if already connected (e.g. from settings page auth) if (WiFi.status() == WL_CONNECTED) { LOG_DBG("KOSync", "Already connected to WiFi"); @@ -315,7 +312,11 @@ void KOReaderSyncActivity::onEnter() { void KOReaderSyncActivity::onExit() { Activity::onExit(); - wifiOff(); + if (wifiActivated) { + WiFi.disconnect(false); + delay(30); + silentRestartToReader(); + } } void KOReaderSyncActivity::render(RenderLock&&) { diff --git a/src/activities/reader/KOReaderSyncActivity.h b/src/activities/reader/KOReaderSyncActivity.h index 1f07e4f7..7cc824ba 100644 --- a/src/activities/reader/KOReaderSyncActivity.h +++ b/src/activities/reader/KOReaderSyncActivity.h @@ -78,6 +78,12 @@ class KOReaderSyncActivity final : public Activity { // Selection in result screen (0=Apply, 1=Upload) int selectedOption = 0; + // Tracks whether this session activated WiFi. Set in onEnter past the credentials + // check; checked in onExit to decide whether to silent-reboot. Can't rely on + // WiFi.getMode() because performUpload() calls esp_wifi_stop() on the way out, + // which makes WiFi.getMode() return WIFI_MODE_NULL. + bool wifiActivated = false; + void onWifiSelectionComplete(bool success); void performSync(); void performUpload(); diff --git a/src/activities/settings/FontDownloadActivity.cpp b/src/activities/settings/FontDownloadActivity.cpp index 27abc575..ab9bf50b 100644 --- a/src/activities/settings/FontDownloadActivity.cpp +++ b/src/activities/settings/FontDownloadActivity.cpp @@ -10,6 +10,7 @@ #include "MappedInputManager.h" #include "SdCardFontSystem.h" +#include "SilentRestart.h" #include "activities/network/WifiSelectionActivity.h" #include "activities/util/ConfirmationActivity.h" #include "components/UITheme.h" @@ -30,10 +31,12 @@ void FontDownloadActivity::onEnter() { void FontDownloadActivity::onExit() { Activity::onExit(); - WiFi.disconnect(false); - delay(100); - WiFi.mode(WIFI_OFF); - delay(100); + + if (WiFi.getMode() != WIFI_MODE_NULL) { + WiFi.disconnect(false); + delay(30); + silentRestart(); + } } void FontDownloadActivity::onWifiSelectionComplete(const bool success) { diff --git a/src/main.cpp b/src/main.cpp index 27fb4a07..7cdf8068 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -131,6 +131,29 @@ EpdFontFamily ui12FontFamily(&ui12RegularFont, &ui12BoldFont); unsigned long t1 = 0; unsigned long t2 = 0; +// Definitions for SilentRestart.h. RTC_NOINIT survives ESP.restart() but not power loss. +RTC_NOINIT_ATTR uint32_t silentRebootMagic; +RTC_NOINIT_ATTR uint32_t silentRebootTarget; +constexpr uint32_t SILENT_REBOOT_MAGIC = 0xC1EAB007; +constexpr uint32_t SILENT_REBOOT_TARGET_HOME = 0; +constexpr uint32_t SILENT_REBOOT_TARGET_READER = 1; + +void silentRestart() { + silentRebootTarget = SILENT_REBOOT_TARGET_HOME; + silentRebootMagic = SILENT_REBOOT_MAGIC; + LOG_DBG("MAIN", "Silent restart (target=home)"); + delay(50); + ESP.restart(); +} + +void silentRestartToReader() { + silentRebootTarget = SILENT_REBOOT_TARGET_READER; + silentRebootMagic = SILENT_REBOOT_MAGIC; + LOG_DBG("MAIN", "Silent restart (target=reader)"); + delay(50); + ESP.restart(); +} + // Verify power button press duration on wake-up from deep sleep // Pre-condition: isWakeupByPowerButton() == true void verifyPowerButtonDuration() { @@ -238,6 +261,15 @@ void setup() { t1 = millis(); HalSystem::begin(); + + // Read-and-clear so a panic later in setup() doesn't loop into silent reboot. + // Bound the target range too — RTC_NOINIT memory is uninitialized on cold boot. + const bool isSilentReboot = (silentRebootMagic == SILENT_REBOOT_MAGIC); + const uint32_t snapshotTarget = + (isSilentReboot && silentRebootTarget <= SILENT_REBOOT_TARGET_READER) ? silentRebootTarget : 0; + silentRebootMagic = 0; + silentRebootTarget = 0; + gpio.begin(); powerManager.begin(); halTiltSensor.begin(); @@ -315,7 +347,11 @@ void setup() { setupDisplayAndFonts(); - activityManager.goToBoot(); + // First paint after silent reboot is HALF_REFRESH (SDK forces it after begin()'s + // panel reset); subsequent paints FAST. + if (!isSilentReboot) { + activityManager.goToBoot(); + } APP_STATE.loadFromFile(); RECENT_BOOKS.loadFromFile(); @@ -327,6 +363,13 @@ void setup() { } else if (HalSystem::isRebootFromPanic()) { // If we rebooted from a panic, go to crash report screen to show the panic info activityManager.goToCrashReport(); + } else if (isSilentReboot && snapshotTarget == SILENT_REBOOT_TARGET_READER && !APP_STATE.openEpubPath.empty()) { + activityManager.goToReader(APP_STATE.openEpubPath); + } else if (isSilentReboot) { + // target == home (or reader with no open book): land on home — don't fall + // through to the sleep-wake "resume reader" logic, which fires on stale + // openEpubPath + lastSleepFromReader from a prior session. + activityManager.goHome(); } else if (APP_STATE.openEpubPath.empty() || !APP_STATE.lastSleepFromReader || mappedInputManager.isPressed(MappedInputManager::Button::Back) || APP_STATE.readerActivityLoadCount > 0) { // Boot to home screen if no book is open, last sleep was not from reader, back button is held, or reader activity