From a2161f067ef278476c7dd1ac3a6fbf8f50745287 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Tue, 19 May 2026 19:39:40 +0200 Subject: [PATCH] Implement silent reboot --- src/SilentRestart.h | 10 ++++ .../browser/OpdsBookBrowserActivity.cpp | 13 ++-- .../network/CalibreConnectActivity.cpp | 14 ++--- .../network/CrossPointWebServerActivity.cpp | 55 +++-------------- .../network/CrossPointWebServerActivity.h | 1 - .../reader/KOReaderSyncActivity.cpp | 32 +++++++--- src/activities/reader/KOReaderSyncActivity.h | 10 ++++ .../settings/FontDownloadActivity.cpp | 11 ++-- .../settings/KOReaderAuthActivity.cpp | 8 ++- src/activities/settings/OtaUpdateActivity.cpp | 8 ++- src/activities/weather/WeatherActivity.cpp | 10 +++- src/main.cpp | 59 ++++++++++++++++++- 12 files changed, 150 insertions(+), 81 deletions(-) create mode 100644 src/SilentRestart.h diff --git a/src/SilentRestart.h b/src/SilentRestart.h new file mode 100644 index 00000000..469f27ee --- /dev/null +++ b/src/SilentRestart.h @@ -0,0 +1,10 @@ +#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 — WiFi/LWIP/netif +// teardown scatters long-lived allocations across the heap, leaving ~50KB of +// contiguous space unrecoverable without a reboot. + +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 c968727b..7a79ba44 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.cpp +++ b/src/activities/browser/OpdsBookBrowserActivity.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -17,6 +16,7 @@ #include "MappedInputManager.h" #include "OpdsFormatLabel.h" +#include "SilentRestart.h" #include "activities/network/WifiSelectionActivity.h" #include "activities/util/KeyboardEntryActivity.h" #include "components/UITheme.h" @@ -133,12 +133,16 @@ void OpdsBookBrowserActivity::onEnter() { void OpdsBookBrowserActivity::onExit() { Activity::onExit(); - HalClock::wifiOff(); - entryOffsets.clear(); navigationHistory.clear(); formatSelectionLabels.clear(); Storage.remove("/.tmp_opds_cache.dat"); + + if (WiFi.getMode() != WIFI_MODE_NULL) { + WiFi.disconnect(false); + delay(30); + silentRestart(); + } } void OpdsBookBrowserActivity::loop() { @@ -701,8 +705,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 further 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 f5cf2bb9..2bc5f202 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 "activities/network/SignalStrengthWidget.h" #include "components/UITheme.h" @@ -54,14 +55,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 5edaf007..5d9db36f 100644 --- a/src/activities/network/CrossPointWebServerActivity.cpp +++ b/src/activities/network/CrossPointWebServerActivity.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include @@ -12,6 +11,7 @@ #include "MappedInputManager.h" #include "NetworkModeSelectionActivity.h" +#include "SilentRestart.h" #include "WifiSelectionActivity.h" #include "activities/network/CalibreConnectActivity.h" #include "activities/network/SignalStrengthWidget.h" @@ -64,45 +64,17 @@ void CrossPointWebServerActivity::onEnter() { void CrossPointWebServerActivity::onExit() { Activity::onExit(); - LOG_DBG("WEBACT", "Free heap at onExit start: %d bytes", ESP.getFreeHeap()); - 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; - } - - // 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); + // 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); - WiFi.mode(WIFI_OFF); - delay(30); - } else { - HalClock::wifiOff(); - } - - LOG_DBG("WEBACT", "Free heap at onExit end: %d bytes", ESP.getFreeHeap()); - requestUpdate(); - - if (webServerStarted) { - GUI.drawPopup(renderer, tr(STR_RESTART_DEVICE)); - LOG_DBG("WEBACT", "Restarting device after webserver exit to regain heap..."); - ESP.restart(); + silentRestart(); } } @@ -271,15 +243,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 51b66b68..130e1ba8 100644 --- a/src/activities/network/CrossPointWebServerActivity.h +++ b/src/activities/network/CrossPointWebServerActivity.h @@ -55,7 +55,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 ea51caf5..8f3d439b 100644 --- a/src/activities/reader/KOReaderSyncActivity.cpp +++ b/src/activities/reader/KOReaderSyncActivity.cpp @@ -8,10 +8,12 @@ #include #include #include +#include #include "KOReaderCredentialStore.h" #include "KOReaderDocumentId.h" #include "MappedInputManager.h" +#include "SilentRestart.h" #include "activities/network/WifiSelectionActivity.h" #include "components/UITheme.h" #include "fontIds.h" @@ -140,7 +142,8 @@ bool KOReaderSyncActivity::handleAutoPushPreflight() { LOG_DBG("KOSync", "AUTO_PUSH skipped: remote %.4f >= local %.4f", warmupProgress.percentage, localProgress.percentage); KOReaderSyncClient::endPersistentSession(); - HalClock::wifiOff(true); + // Drop the radio while user reads the result; full teardown happens at silent reboot. + esp_wifi_stop(); APP_STATE.koReaderSyncSession.outcome = KOReaderSyncOutcomeState::UPLOAD_COMPLETE; APP_STATE.saveToFile(); resumeReader(KOReaderSyncOutcomeState::UPLOAD_COMPLETE); @@ -180,7 +183,7 @@ void KOReaderSyncActivity::performFetchAndCompare() { if (syncIntent == KOReaderSyncIntentState::AUTO_PULL) { // Auto-pull at book open: nothing to apply, just open the book with local progress. KOReaderSyncClient::endPersistentSession(); - HalClock::wifiOff(true); + esp_wifi_stop(); resumeReader(KOReaderSyncOutcomeState::CANCELLED); return; } @@ -230,7 +233,7 @@ void KOReaderSyncActivity::performFetchAndCompare() { if (!ensureRemotePositionMapped()) { if (syncIntent == KOReaderSyncIntentState::AUTO_PULL) { // Auto-pull was best-effort. Fail silently and just open the book. - HalClock::wifiOff(true); + esp_wifi_stop(); resumeReader(KOReaderSyncOutcomeState::CANCELLED); return; } @@ -258,7 +261,7 @@ void KOReaderSyncActivity::performFetchAndCompare() { if (syncIntent == KOReaderSyncIntentState::AUTO_PULL) { // Auto-pull skips the success-screen dwell — the reader will render the new // position immediately, which is the only visible feedback the user needs. - HalClock::wifiOff(true); + esp_wifi_stop(); resumeReader(KOReaderSyncOutcomeState::APPLIED_REMOTE); return; } @@ -418,7 +421,8 @@ void KOReaderSyncActivity::performUpload() { logSyncMemSnapshot("after_updateProgress"); if (result != KOReaderSyncClient::OK) { - HalClock::wifiOff(true); + // Drop the radio while user reads the result; full teardown happens at silent reboot. + esp_wifi_stop(); { RenderLock lock(*this); state = SYNC_FAILED; @@ -435,7 +439,8 @@ void KOReaderSyncActivity::performUpload() { return; } - HalClock::wifiOff(true); + // Drop the radio while user reads the success screen; full teardown happens at silent reboot. + esp_wifi_stop(); APP_STATE.koReaderSyncSession.outcome = KOReaderSyncOutcomeState::UPLOAD_COMPLETE; APP_STATE.saveToFile(); if (syncIntent == KOReaderSyncIntentState::AUTO_PUSH) { @@ -465,6 +470,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"); @@ -483,9 +491,18 @@ void KOReaderSyncActivity::onExit() { logSyncMemSnapshot("onExit_before_cleanup"); KOReaderSyncClient::endPersistentSession(); - HalClock::wifiOff(true); releaseEpubForMapping(); logSyncMemSnapshot("onExit_after_cleanup"); + + if (wifiActivated) { + WiFi.disconnect(false); + delay(30); + if (exitToHomeAfterSync) { + silentRestart(); + } else { + silentRestartToReader(); + } + } } void KOReaderSyncActivity::closeCancelled() { @@ -526,6 +543,7 @@ void KOReaderSyncActivity::resumeReader(const KOReaderSyncOutcomeState outcome, // the user just left would be jarring. The session state is consumed and cleared by the // home destination's normal flow (no reader to apply it to in this case). const bool exitToHome = sync.exitToHomeAfterSync; + exitToHomeAfterSync = exitToHome; if (exitToHome) { sync.clear(); } diff --git a/src/activities/reader/KOReaderSyncActivity.h b/src/activities/reader/KOReaderSyncActivity.h index 0985ac22..94609c23 100644 --- a/src/activities/reader/KOReaderSyncActivity.h +++ b/src/activities/reader/KOReaderSyncActivity.h @@ -102,6 +102,16 @@ class KOReaderSyncActivity final : public Activity { unsigned long uploadCompleteTime = 0; bool closeRequested = false; + // 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 intermediate paths call esp_wifi_stop() to drop the + // radio while user reads the result, which makes WiFi.getMode() return WIFI_MODE_NULL. + bool wifiActivated = false; + + // Captured from sync.exitToHomeAfterSync in resumeReader() so onExit can route the + // silent reboot to home instead of the reader when reader-close auto-sync triggered. + bool exitToHomeAfterSync = false; + void onWifiSelectionComplete(bool success); void performSync(); bool calculateDocumentHash(); diff --git a/src/activities/settings/FontDownloadActivity.cpp b/src/activities/settings/FontDownloadActivity.cpp index a1edcec9..e7cd6818 100644 --- a/src/activities/settings/FontDownloadActivity.cpp +++ b/src/activities/settings/FontDownloadActivity.cpp @@ -12,6 +12,7 @@ #include "MappedInputManager.h" #include "SdCardFontGlobals.h" +#include "SilentRestart.h" #include "activities/network/WifiSelectionActivity.h" #include "activities/util/ConfirmationActivity.h" #include "components/UITheme.h" @@ -32,10 +33,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/activities/settings/KOReaderAuthActivity.cpp b/src/activities/settings/KOReaderAuthActivity.cpp index 6058b944..8fb42860 100644 --- a/src/activities/settings/KOReaderAuthActivity.cpp +++ b/src/activities/settings/KOReaderAuthActivity.cpp @@ -1,13 +1,13 @@ #include "KOReaderAuthActivity.h" #include -#include #include #include #include "KOReaderCredentialStore.h" #include "KOReaderSyncClient.h" #include "MappedInputManager.h" +#include "SilentRestart.h" #include "activities/network/WifiSelectionActivity.h" #include "components/UITheme.h" #include "fontIds.h" @@ -104,7 +104,11 @@ void KOReaderAuthActivity::onEnter() { void KOReaderAuthActivity::onExit() { Activity::onExit(); - HalClock::wifiOff(); + if (WiFi.getMode() != WIFI_MODE_NULL) { + WiFi.disconnect(false); + delay(30); + silentRestart(); + } } void KOReaderAuthActivity::render(RenderLock&&) { diff --git a/src/activities/settings/OtaUpdateActivity.cpp b/src/activities/settings/OtaUpdateActivity.cpp index b777bf87..24a633a7 100644 --- a/src/activities/settings/OtaUpdateActivity.cpp +++ b/src/activities/settings/OtaUpdateActivity.cpp @@ -1,11 +1,11 @@ #include "OtaUpdateActivity.h" #include -#include #include #include #include "MappedInputManager.h" +#include "SilentRestart.h" #include "activities/network/WifiSelectionActivity.h" #include "components/UITheme.h" #include "fontIds.h" @@ -68,7 +68,11 @@ void OtaUpdateActivity::onEnter() { void OtaUpdateActivity::onExit() { Activity::onExit(); - HalClock::wifiOff(); + if (WiFi.getMode() != WIFI_MODE_NULL) { + WiFi.disconnect(false); + delay(30); + silentRestart(); + } } void OtaUpdateActivity::render(RenderLock&&) { diff --git a/src/activities/weather/WeatherActivity.cpp b/src/activities/weather/WeatherActivity.cpp index eb0830e9..30b46357 100644 --- a/src/activities/weather/WeatherActivity.cpp +++ b/src/activities/weather/WeatherActivity.cpp @@ -14,6 +14,7 @@ #include #include "MappedInputManager.h" +#include "SilentRestart.h" #include "WeatherSettingsActivity.h" #include "activities/network/WifiSelectionActivity.h" #include "components/UITheme.h" @@ -222,7 +223,11 @@ void WeatherActivity::onExit() { // Weather screen is always landscape; restore app UI to portrait on exit. renderer.setOrientation(GfxRenderer::Orientation::Portrait); - WiFi.mode(WIFI_OFF); + if (WiFi.getMode() != WIFI_MODE_NULL) { + WiFi.disconnect(false); + delay(30); + silentRestart(); + } } void WeatherActivity::loadAndDisplay() { @@ -298,8 +303,7 @@ void WeatherActivity::onWifiSelectionComplete(bool connected) { LOG_DBG("WEA", "state -> CHECK_WIFI (waitStart=%lu)", (unsigned long)wifiWaitStartedAtMs); requestUpdate(true); } else { - WiFi.disconnect(); - WiFi.mode(WIFI_OFF); + // Leave WiFi up; onExit's silent reboot handles teardown without further fragmenting. state = State::ERROR; errorMessage = tr(STR_WIFI_CONN_FAILED); requestUpdate(); diff --git a/src/main.cpp b/src/main.cpp index 8ab62df7..bdf1833a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -29,6 +29,7 @@ #include "OpdsServerStore.h" #include "RecentBooksStore.h" #include "SdCardFontSystem.h" +#include "SilentRestart.h" #include "WeatherSettingsStore.h" #include "activities/Activity.h" #include "activities/ActivityManager.h" @@ -124,6 +125,29 @@ EpdFont ui12RegularFont(&inter_ui_12_regular); EpdFont ui12BoldFont(&inter_ui_12_bold); EpdFontFamily ui12FontFamily(&ui12RegularFont, &ui12BoldFont); +// SilentRestart.h definitions. 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(); +} + // Enter deep sleep mode void enterDeepSleep() { LOG_DBG("MAIN", "enterDeepSleep called at millis=%lu, powerBtn isPressed=%d, rawPin=%d", millis(), @@ -233,6 +257,15 @@ void setup() { esp_ota_mark_app_valid_cancel_rollback(); } } + + // 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 silentRebootTargetSnapshot = + (isSilentReboot && silentRebootTarget <= SILENT_REBOOT_TARGET_READER) ? silentRebootTarget : 0; + silentRebootMagic = 0; + silentRebootTarget = 0; + HalSystem::begin(); gpio.begin(); powerManager.begin(); @@ -330,7 +363,16 @@ void setup() { setupDisplayAndFonts(); - activityManager.goToBoot(); + if (!isSilentReboot) { + activityManager.goToBoot(); + } else { + // After a silent reboot the panel still shows the previous session's pixels but + // the SDK's RED-RAM diff buffer was cleared by begin(). A FAST refresh would only + // flip pixels the SDK *thinks* changed, leaving the old screen visible. Force the + // first paint to HALF_REFRESH so the panel cleanly repaints; subsequent paints + // resume FAST as normal. + renderer.setNextDisplayRefreshMode(HalDisplay::HALF_REFRESH); + } APP_STATE.loadFromFile(); HalClock::restore(); @@ -341,6 +383,14 @@ void setup() { // Skip normal home/reader routing: jump straight into the SD firmware picker. activityManager.replaceActivity( std::make_unique(renderer, mappedInputManager, /*recoveryMode=*/true)); + } else if (isSilentReboot && silentRebootTargetSnapshot == 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) { activityManager.goHome(); @@ -353,9 +403,12 @@ void setup() { activityManager.goToReader(path); } - // Ensure we're not still holding the power button before leaving setup + // Ensure we're not still holding the power button before leaving setup. // waitForStablePowerRelease protects against switch bounce that might register as a false double-press. - gpio.waitForStablePowerRelease(); + // Skip on silent reboot: the firmware triggered the restart, so the button isn't held. + if (!isSilentReboot) { + gpio.waitForStablePowerRelease(); + } // Flush any pin state transitions that occurred during boot before entering the main loop mappedInputManager.update(); buttonEventManager.drain();