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