From 5bb3980dc0be82b0875d97c6b5fd313495c1c547 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 21 May 2026 23:13:28 +0200 Subject: [PATCH] Add quick resume --- lib/I18n/translations/belarusian.yaml | 2 + lib/I18n/translations/catalan.yaml | 2 + lib/I18n/translations/czech.yaml | 2 + lib/I18n/translations/danish.yaml | 2 + lib/I18n/translations/dutch.yaml | 2 + lib/I18n/translations/english.yaml | 2 + lib/I18n/translations/finnish.yaml | 2 + lib/I18n/translations/french.yaml | 2 + lib/I18n/translations/german.yaml | 2 + lib/I18n/translations/hungarian.yaml | 2 + lib/I18n/translations/italian.yaml | 2 + lib/I18n/translations/kazakh.yaml | 2 + lib/I18n/translations/lithuanian.yaml | 2 + lib/I18n/translations/polish.yaml | 2 + lib/I18n/translations/portuguese_br.yaml | 2 + lib/I18n/translations/portuguese_pt.yaml | 2 + lib/I18n/translations/romanian.yaml | 2 + lib/I18n/translations/russian.yaml | 2 + lib/I18n/translations/slovenian.yaml | 2 + lib/I18n/translations/spanish.yaml | 2 + lib/I18n/translations/swedish.yaml | 2 + lib/I18n/translations/turkish.yaml | 2 + lib/I18n/translations/ukrainian.yaml | 2 + lib/I18n/translations/vietnamese.yaml | 2 + lib/hal/HalDisplay.cpp | 15 +-- lib/hal/HalDisplay.h | 6 +- src/CrossPointSettings.cpp | 6 ++ src/CrossPointSettings.h | 13 +++ src/CrossPointState.h | 3 + src/JsonSettingsIO.cpp | 6 ++ src/SettingsList.h | 4 +- src/activities/ActivityManager.cpp | 4 +- src/activities/ActivityManager.h | 3 +- src/activities/boot_sleep/SleepActivity.cpp | 22 +++++ src/activities/boot_sleep/SleepActivity.h | 8 +- src/activities/settings/SettingsActivity.cpp | 3 + src/images/LoadingIcon.h | 24 +++++ src/images/MoonIcon.h | 24 +++++ src/main.cpp | 98 +++++++++++++++++--- src/network/CrossPointWebServer.cpp | 1 + 40 files changed, 262 insertions(+), 26 deletions(-) create mode 100644 src/images/LoadingIcon.h create mode 100644 src/images/MoonIcon.h diff --git a/lib/I18n/translations/belarusian.yaml b/lib/I18n/translations/belarusian.yaml index 442788b4..64fdf7db 100644 --- a/lib/I18n/translations/belarusian.yaml +++ b/lib/I18n/translations/belarusian.yaml @@ -674,3 +674,5 @@ STR_LARGE_IMAGE_PLACEHOLDER: "Запаўняльнік для вялікіх в STR_LARGE_IMAGE: "Вялікая выява" STR_PRESS_CONFIRM_TO_LOAD: "Націсніце Пацв. для загрузкі" STR_TOOL_UTILITIES: "Дапаможныя" +STR_QUICK_RESUME: "Хуткае узнаўленне" +STR_QUICK_RESUME_TIMEOUT: "Хуткае узнаўленне пасля таймаўту" diff --git a/lib/I18n/translations/catalan.yaml b/lib/I18n/translations/catalan.yaml index a563459b..3864c73c 100644 --- a/lib/I18n/translations/catalan.yaml +++ b/lib/I18n/translations/catalan.yaml @@ -294,3 +294,5 @@ STR_LARGE_IMAGE_PLACEHOLDER: "Text de mostra per a imatges grans" STR_LARGE_IMAGE: "Imatge gran" STR_PRESS_CONFIRM_TO_LOAD: "Premeu Confirma per carregar" STR_TOOL_UTILITIES: "Utilitats" +STR_QUICK_RESUME: "Represa ràpida" +STR_QUICK_RESUME_TIMEOUT: "Represa ràpida després del temps" diff --git a/lib/I18n/translations/czech.yaml b/lib/I18n/translations/czech.yaml index a7606fca..777a8ed4 100644 --- a/lib/I18n/translations/czech.yaml +++ b/lib/I18n/translations/czech.yaml @@ -265,3 +265,5 @@ STR_LARGE_IMAGE_PLACEHOLDER: "Zástupný symbol pro velké obrázky" STR_LARGE_IMAGE: "Velký obrázek" STR_PRESS_CONFIRM_TO_LOAD: "Stiskněte Potvrdit pro načtení" STR_TOOL_UTILITIES: "Nástroje" +STR_QUICK_RESUME: "Rychlé navázání" +STR_QUICK_RESUME_TIMEOUT: "Rychlé navázání po vypršení" diff --git a/lib/I18n/translations/danish.yaml b/lib/I18n/translations/danish.yaml index 2e8d24cd..9c04d04f 100644 --- a/lib/I18n/translations/danish.yaml +++ b/lib/I18n/translations/danish.yaml @@ -294,3 +294,5 @@ STR_LARGE_IMAGE_PLACEHOLDER: "Pladsholder for store billeder" STR_LARGE_IMAGE: "Stort billede" STR_PRESS_CONFIRM_TO_LOAD: "Tryk på Bekræft for at indlæse" STR_TOOL_UTILITIES: "Hjælpeværktøjer" +STR_QUICK_RESUME: "Hurtig genoptagelse" +STR_QUICK_RESUME_TIMEOUT: "Hurtig genoptagelse ved timeout" diff --git a/lib/I18n/translations/dutch.yaml b/lib/I18n/translations/dutch.yaml index f3f678c7..327aecdf 100644 --- a/lib/I18n/translations/dutch.yaml +++ b/lib/I18n/translations/dutch.yaml @@ -672,3 +672,5 @@ STR_LARGE_IMAGE_PLACEHOLDER: "Placeholder voor grote afbeeldingen" STR_LARGE_IMAGE: "Grote afbeelding" STR_PRESS_CONFIRM_TO_LOAD: "Druk op Bevestig om te laden" STR_TOOL_UTILITIES: "Hulpmiddelen" +STR_QUICK_RESUME: "Snel hervatten" +STR_QUICK_RESUME_TIMEOUT: "Snel hervatten bij timeout" diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 8efbb7a4..bd4b1aec 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -91,6 +91,8 @@ STR_CAT_READER: "Reader" STR_CAT_CONTROLS: "Controls" STR_CAT_SYSTEM: "System" STR_SLEEP_SCREEN: "Sleep Screen" +STR_QUICK_RESUME: "Quick Resume" +STR_QUICK_RESUME_TIMEOUT: "Quick Resume on Timeout" STR_SLEEP_COVER_MODE: "Sleep Screen Cover Mode" STR_HIDE_BATTERY: "Hide Battery %" STR_EXTRA_SPACING: "Extra Paragraph Spacing" diff --git a/lib/I18n/translations/finnish.yaml b/lib/I18n/translations/finnish.yaml index 12234461..12a814d7 100644 --- a/lib/I18n/translations/finnish.yaml +++ b/lib/I18n/translations/finnish.yaml @@ -264,3 +264,5 @@ STR_LARGE_IMAGE_PLACEHOLDER: "Paikkamerkki suurille kuville" STR_LARGE_IMAGE: "Suuri kuva" STR_PRESS_CONFIRM_TO_LOAD: "Paina Vahvista ladataksesi" STR_TOOL_UTILITIES: "Apuvälineet" +STR_QUICK_RESUME: "Pikajatko" +STR_QUICK_RESUME_TIMEOUT: "Pikajatko aikakatkaisulla" diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index 75891c57..68566a85 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -673,3 +673,5 @@ STR_LARGE_IMAGE_PLACEHOLDER: "Espace réservé pour les grandes images" STR_LARGE_IMAGE: "Grande image" STR_PRESS_CONFIRM_TO_LOAD: "Appuyez sur Confirmer pour charger" STR_TOOL_UTILITIES: "Utilitaire" +STR_QUICK_RESUME: "Reprise rapide" +STR_QUICK_RESUME_TIMEOUT: "Reprise rapide après délai" diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index 71d1a1f8..9c98938c 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -674,3 +674,5 @@ STR_LARGE_IMAGE_PLACEHOLDER: "Platzhalter für große Bilder" STR_LARGE_IMAGE: "Großes Bild" STR_PRESS_CONFIRM_TO_LOAD: "Zum Laden Bestätigen drücken" STR_TOOL_UTILITIES: "Hilfsfunktionen" +STR_QUICK_RESUME: "Schnelles Fortsetzen" +STR_QUICK_RESUME_TIMEOUT: "Schnelles Fortsetzen nach Timeout" diff --git a/lib/I18n/translations/hungarian.yaml b/lib/I18n/translations/hungarian.yaml index 15762620..6b620b2d 100644 --- a/lib/I18n/translations/hungarian.yaml +++ b/lib/I18n/translations/hungarian.yaml @@ -294,3 +294,5 @@ STR_LARGE_IMAGE_PLACEHOLDER: "Helyőrző nagy képekhez" STR_LARGE_IMAGE: "Nagy kép" STR_PRESS_CONFIRM_TO_LOAD: "Nyomja meg a Megerősítés gombot a betöltéshez" STR_TOOL_UTILITIES: "Segédeszközök" +STR_QUICK_RESUME: "Gyors folytatás" +STR_QUICK_RESUME_TIMEOUT: "Gyors folytatás időtúllépéskor" diff --git a/lib/I18n/translations/italian.yaml b/lib/I18n/translations/italian.yaml index c54b9001..033c623a 100644 --- a/lib/I18n/translations/italian.yaml +++ b/lib/I18n/translations/italian.yaml @@ -673,3 +673,5 @@ STR_LARGE_IMAGE_PLACEHOLDER: "Segnaposto per immagini grandi" STR_LARGE_IMAGE: "Immagine grande" STR_PRESS_CONFIRM_TO_LOAD: "Premi Conferma per caricare" STR_TOOL_UTILITIES: "Utilità" +STR_QUICK_RESUME: "Ripresa rapida" +STR_QUICK_RESUME_TIMEOUT: "Ripresa rapida dopo timeout" diff --git a/lib/I18n/translations/kazakh.yaml b/lib/I18n/translations/kazakh.yaml index a6f8c4ab..924004bf 100644 --- a/lib/I18n/translations/kazakh.yaml +++ b/lib/I18n/translations/kazakh.yaml @@ -293,3 +293,5 @@ STR_LARGE_IMAGE_PLACEHOLDER: "Үлкен суреттерге арналған STR_LARGE_IMAGE: "Үлкен сурет" STR_PRESS_CONFIRM_TO_LOAD: "Жүктеу үшін Растау түймесін басыңыз" STR_TOOL_UTILITIES: "Құралдар" +STR_QUICK_RESUME: "Жылдам жалғастыру" +STR_QUICK_RESUME_TIMEOUT: "Таймауттан кейін жылдам жалғастыру" diff --git a/lib/I18n/translations/lithuanian.yaml b/lib/I18n/translations/lithuanian.yaml index 56426b14..a68bc3b4 100644 --- a/lib/I18n/translations/lithuanian.yaml +++ b/lib/I18n/translations/lithuanian.yaml @@ -294,3 +294,5 @@ STR_LARGE_IMAGE_PLACEHOLDER: "Vietaženklis dideliems paveikslėliams" STR_LARGE_IMAGE: "Didelis paveikslėlis" STR_PRESS_CONFIRM_TO_LOAD: "Paspauskite Gerai, kad įkeltumėte" STR_TOOL_UTILITIES: "Pagalbiniai įrankiai" +STR_QUICK_RESUME: "Greitas tęsimas" +STR_QUICK_RESUME_TIMEOUT: "Greitas tęsimas po skirtojo laiko" diff --git a/lib/I18n/translations/polish.yaml b/lib/I18n/translations/polish.yaml index 6f1aef36..e3b05efc 100644 --- a/lib/I18n/translations/polish.yaml +++ b/lib/I18n/translations/polish.yaml @@ -687,3 +687,5 @@ STR_LARGE_IMAGE_PLACEHOLDER: "Ramka dużego obrazka" STR_LARGE_IMAGE: "Duży obrazek" STR_PRESS_CONFIRM_TO_LOAD: "Naciśnij Potwierdź, aby wczytać" STR_TOOL_UTILITIES: "Narzędzia" +STR_QUICK_RESUME: "Szybkie wznawianie" +STR_QUICK_RESUME_TIMEOUT: "Szybkie wznawianie po czasie" diff --git a/lib/I18n/translations/portuguese_br.yaml b/lib/I18n/translations/portuguese_br.yaml index 2faaf40f..2ee9a5bc 100644 --- a/lib/I18n/translations/portuguese_br.yaml +++ b/lib/I18n/translations/portuguese_br.yaml @@ -673,3 +673,5 @@ STR_LARGE_IMAGE_PLACEHOLDER: "Marcador para imagens grandes" STR_LARGE_IMAGE: "Imagem grande" STR_PRESS_CONFIRM_TO_LOAD: "Pressione Confirmar para carregar" STR_TOOL_UTILITIES: "Utilitários" +STR_QUICK_RESUME: "Retomada rápida" +STR_QUICK_RESUME_TIMEOUT: "Retomada rápida após tempo limite" diff --git a/lib/I18n/translations/portuguese_pt.yaml b/lib/I18n/translations/portuguese_pt.yaml index e37cc343..daaf1fb3 100644 --- a/lib/I18n/translations/portuguese_pt.yaml +++ b/lib/I18n/translations/portuguese_pt.yaml @@ -672,3 +672,5 @@ STR_LARGE_IMAGE_PLACEHOLDER: "Espaço reservado para imagens grandes" STR_LARGE_IMAGE: "Imagem grande" STR_PRESS_CONFIRM_TO_LOAD: "Prima Confirmar para carregar" STR_TOOL_UTILITIES: "Utilitários" +STR_QUICK_RESUME: "Retomada rápida" +STR_QUICK_RESUME_TIMEOUT: "Retomada rápida após tempo limite" diff --git a/lib/I18n/translations/romanian.yaml b/lib/I18n/translations/romanian.yaml index 15027c23..98cb3000 100644 --- a/lib/I18n/translations/romanian.yaml +++ b/lib/I18n/translations/romanian.yaml @@ -294,3 +294,5 @@ STR_LARGE_IMAGE_PLACEHOLDER: "Substituent pentru imagini mari" STR_LARGE_IMAGE: "Imagine mare" STR_PRESS_CONFIRM_TO_LOAD: "Apăsați Confirmă pentru încărcare" STR_TOOL_UTILITIES: "Utilitare" +STR_QUICK_RESUME: "Reluare rapidă" +STR_QUICK_RESUME_TIMEOUT: "Reluare rapidă la timeout" diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index bbd6196c..be3371bf 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -673,3 +673,5 @@ STR_LARGE_IMAGE_PLACEHOLDER: "Заглушка для больших изобр STR_LARGE_IMAGE: "Большое изображение" STR_PRESS_CONFIRM_TO_LOAD: "Нажмите Подтв. для загрузки" STR_TOOL_UTILITIES: "Утилиты" +STR_QUICK_RESUME: "Быстрое возобновление" +STR_QUICK_RESUME_TIMEOUT: "Быстрое возобновление по таймауту" diff --git a/lib/I18n/translations/slovenian.yaml b/lib/I18n/translations/slovenian.yaml index eae0b193..3b9fc0b5 100644 --- a/lib/I18n/translations/slovenian.yaml +++ b/lib/I18n/translations/slovenian.yaml @@ -672,3 +672,5 @@ STR_LARGE_IMAGE_PLACEHOLDER: "Oznaka mesta za velike slike" STR_LARGE_IMAGE: "Velika slika" STR_PRESS_CONFIRM_TO_LOAD: "Pritisnite Potrdi za nalaganje" STR_TOOL_UTILITIES: "Pomočnik" +STR_QUICK_RESUME: "Hitro nadaljevanje" +STR_QUICK_RESUME_TIMEOUT: "Hitro nadaljevanje po izteku" diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index 4e870a07..cd52db9d 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -673,3 +673,5 @@ STR_LARGE_IMAGE_PLACEHOLDER: "Marcador para imágenes grandes" STR_LARGE_IMAGE: "Imagen grande" STR_PRESS_CONFIRM_TO_LOAD: "Pulsa Confirmar para cargar" STR_TOOL_UTILITIES: "Utilidades" +STR_QUICK_RESUME: "Reanudación rápida" +STR_QUICK_RESUME_TIMEOUT: "Reanudación rápida tras tiempo" diff --git a/lib/I18n/translations/swedish.yaml b/lib/I18n/translations/swedish.yaml index 5c87e126..cbeb1b1c 100644 --- a/lib/I18n/translations/swedish.yaml +++ b/lib/I18n/translations/swedish.yaml @@ -672,3 +672,5 @@ STR_LARGE_IMAGE_PLACEHOLDER: "Platshållare för stora bilder" STR_LARGE_IMAGE: "Stor bild" STR_PRESS_CONFIRM_TO_LOAD: "Tryck på Bekräfta för att läsa in" STR_TOOL_UTILITIES: "Hjälpverktyg" +STR_QUICK_RESUME: "Snabb återupptagning" +STR_QUICK_RESUME_TIMEOUT: "Snabb återupptagning efter timeout" diff --git a/lib/I18n/translations/turkish.yaml b/lib/I18n/translations/turkish.yaml index d909750e..d62b64d1 100644 --- a/lib/I18n/translations/turkish.yaml +++ b/lib/I18n/translations/turkish.yaml @@ -672,3 +672,5 @@ STR_LARGE_IMAGE_PLACEHOLDER: "Büyük görseller için yer tutucu" STR_LARGE_IMAGE: "Büyük görsel" STR_PRESS_CONFIRM_TO_LOAD: "Yüklemek için Onayla'ya basın" STR_TOOL_UTILITIES: "Yardımcı" +STR_QUICK_RESUME: "Hızlı Devam" +STR_QUICK_RESUME_TIMEOUT: "Zaman aşımında Hızlı Devam" diff --git a/lib/I18n/translations/ukrainian.yaml b/lib/I18n/translations/ukrainian.yaml index 004487c3..36193fc5 100644 --- a/lib/I18n/translations/ukrainian.yaml +++ b/lib/I18n/translations/ukrainian.yaml @@ -672,3 +672,5 @@ STR_LARGE_IMAGE_PLACEHOLDER: "Заглушка для великих зобра STR_LARGE_IMAGE: "Велике зображення" STR_PRESS_CONFIRM_TO_LOAD: "Натисніть Підтвердити, щоб завантажити" STR_TOOL_UTILITIES: "Корисні інструменти" +STR_QUICK_RESUME: "Швидке продовження" +STR_QUICK_RESUME_TIMEOUT: "Швидке продовження після таймауту" diff --git a/lib/I18n/translations/vietnamese.yaml b/lib/I18n/translations/vietnamese.yaml index 9d3c5bca..3bd93a6d 100644 --- a/lib/I18n/translations/vietnamese.yaml +++ b/lib/I18n/translations/vietnamese.yaml @@ -672,3 +672,5 @@ STR_LARGE_IMAGE_PLACEHOLDER: "Giữ chỗ cho hình ảnh lớn" STR_LARGE_IMAGE: "Hình ảnh lớn" STR_PRESS_CONFIRM_TO_LOAD: "Nhấn Xác nhận để tải" STR_TOOL_UTILITIES: "Trợ giúp" +STR_QUICK_RESUME: "Tiếp tục nhanh" +STR_QUICK_RESUME_TIMEOUT: "Tiếp tục nhanh sau khi hết thời gian" diff --git a/lib/hal/HalDisplay.cpp b/lib/hal/HalDisplay.cpp index dbe8fefb..43cd316f 100644 --- a/lib/hal/HalDisplay.cpp +++ b/lib/hal/HalDisplay.cpp @@ -10,7 +10,7 @@ HalDisplay::HalDisplay() : einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_R HalDisplay::~HalDisplay() {} -void HalDisplay::begin() { +void HalDisplay::begin(bool seamless) { // Set X3-specific panel mode before initializing. if (gpio.deviceIsX3()) { einkDisplay.setDisplayX3(); @@ -18,11 +18,14 @@ void HalDisplay::begin() { einkDisplay.begin(); - // Request resync after specific wakeup events to ensure clean display state - const auto wakeupReason = gpio.getWakeupReason(); - if (wakeupReason == HalGPIO::WakeupReason::PowerButton || wakeupReason == HalGPIO::WakeupReason::AfterFlash || - wakeupReason == HalGPIO::WakeupReason::Other) { - einkDisplay.requestResync(); + // Request resync after specific wakeup events to ensure clean display state. + // Skip when seamless=true so the current panel content is preserved (Quick Resume). + if (!seamless) { + const auto wakeupReason = gpio.getWakeupReason(); + if (wakeupReason == HalGPIO::WakeupReason::PowerButton || wakeupReason == HalGPIO::WakeupReason::AfterFlash || + wakeupReason == HalGPIO::WakeupReason::Other) { + einkDisplay.requestResync(); + } } } diff --git a/lib/hal/HalDisplay.h b/lib/hal/HalDisplay.h index 0015cb88..6a6b99a1 100644 --- a/lib/hal/HalDisplay.h +++ b/lib/hal/HalDisplay.h @@ -17,8 +17,10 @@ class HalDisplay { FAST_REFRESH // Fast refresh using custom LUT }; - // Initialize the display hardware and driver - void begin(); + // Initialize the display hardware and driver. + // When seamless=true, skip the on-wake resync so existing panel content is preserved + // (used by Quick Resume to bring back the last reader page without a boot screen). + void begin(bool seamless = false); // Display dimensions static constexpr uint16_t DISPLAY_WIDTH = EInkDisplay::DISPLAY_WIDTH; diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index 21b979e3..0333eb9b 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -40,6 +40,12 @@ void enforceFixedShortActions(CrossPointSettings& settings) { } // namespace +void CrossPointSettings::normalizeDependentSettings(CrossPointSettings& settings) { + if (settings.sleepScreen == SLEEP_SCREEN_MODE::QUICK_RESUME) { + settings.quickResumeSleepScreen = QUICK_RESUME_SLEEP_SCREEN::QUICK_RESUME_AFTER_TIMEOUT; + } +} + void CrossPointSettings::validateFrontButtonMapping(CrossPointSettings& settings) { const uint8_t mapping[] = {settings.frontButtonBack, settings.frontButtonConfirm, settings.frontButtonLeft, settings.frontButtonRight}; diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 6ef507ad..2fce1e4c 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -25,6 +25,7 @@ class CrossPointSettings { BLANK = 4, COVER_CUSTOM = 5, OVERLAY = 6, + QUICK_RESUME = 7, SLEEP_SCREEN_MODE_COUNT }; enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1, SLEEP_SCREEN_COVER_MODE_COUNT }; @@ -35,6 +36,11 @@ class CrossPointSettings { INVERTED_BLACK_AND_WHITE = 2, SLEEP_SCREEN_COVER_FILTER_COUNT }; + enum QUICK_RESUME_SLEEP_SCREEN { + QUICK_RESUME_NEVER = 0, + QUICK_RESUME_AFTER_TIMEOUT = 1, + QUICK_RESUME_SLEEP_SCREEN_COUNT + }; // Status bar enum - legacy enum STATUS_BAR_MODE { @@ -198,6 +204,9 @@ class CrossPointSettings { uint8_t sleepCoverOverlay = 0; // Sleep image pick mode (random vs sequential walk-through) uint8_t sleepImagePickMode = PICK_RANDOM; + // Quick Resume on Timeout: keep current page on display with a moon icon when sleeping by timeout, + // and on wake restore the page directly (skipping the boot screen). + uint8_t quickResumeSleepScreen = QUICK_RESUME_NEVER; // Status bar settings (statusBar, statusBarProgressBar, statusBarProgressBarThickness retained for migration only) uint8_t statusBar = FULL; uint8_t statusBarChapterPageCount = 1; @@ -379,6 +388,10 @@ class CrossPointSettings { static void validateFrontButtonMapping(CrossPointSettings& settings); + // Enforce settings whose values depend on others (e.g. sleepScreen=QUICK_RESUME implies + // quickResumeSleepScreen=ON). Call after any setting mutation that could invalidate the pair. + static void normalizeDependentSettings(CrossPointSettings& settings); + float getReaderLineCompression() const; unsigned long getSleepTimeoutMs() const; int getRefreshFrequency() const; diff --git a/src/CrossPointState.h b/src/CrossPointState.h index 940f68aa..5b2a6829 100644 --- a/src/CrossPointState.h +++ b/src/CrossPointState.h @@ -96,6 +96,9 @@ class CrossPointState { uint8_t readerActivityLoadCount = 0; bool lastSleepFromReader = false; bool recentBooksGridView = false; // true = grid/thumbnail view, false = list view + // When false, setup() skips the boot screen on wake and instead restores the + // saved framebuffer overlaid with a loading icon (Quick Resume). + bool showBootScreen = true; KOReaderSyncSessionState koReaderSyncSession; PendingBookmarkJumpState pendingBookmarkJump; ~CrossPointState() = default; diff --git a/src/JsonSettingsIO.cpp b/src/JsonSettingsIO.cpp index 7dfb7d71..f13161fa 100644 --- a/src/JsonSettingsIO.cpp +++ b/src/JsonSettingsIO.cpp @@ -87,6 +87,7 @@ bool JsonSettingsIO::saveState(const CrossPointState& s, const char* path) { doc["readerActivityLoadCount"] = s.readerActivityLoadCount; doc["lastSleepFromReader"] = s.lastSleepFromReader; doc["recentBooksGridView"] = s.recentBooksGridView; + doc["showBootScreen"] = s.showBootScreen; // Information about a pending KOReader sync session JsonObject sync = doc["koReaderSyncSession"].to(); sync["active"] = s.koReaderSyncSession.active; @@ -146,6 +147,7 @@ bool JsonSettingsIO::loadState(CrossPointState& s, const char* json) { s.readerActivityLoadCount = doc["readerActivityLoadCount"] | (uint8_t)0; s.lastSleepFromReader = doc["lastSleepFromReader"] | false; s.recentBooksGridView = doc["recentBooksGridView"] | false; + s.showBootScreen = doc["showBootScreen"] | true; JsonObject sync = doc["koReaderSyncSession"].as(); s.koReaderSyncSession.active = sync["active"] | false; s.koReaderSyncSession.epubPath = sync["epubPath"] | std::string(""); @@ -344,6 +346,10 @@ bool JsonSettingsIO::loadSettings(CrossPointSettings& s, const char* json, bool* s.moveFinishedBooksToCompleted = doc["moveFinishedBooksToCompleted"] | (uint8_t)0; s.removeFinishedBooksFromRecents = doc["removeFinishedBooksFromRecents"] | (uint8_t)0; + const uint8_t quickResumeBeforeNormalize = s.quickResumeSleepScreen; + CrossPointSettings::normalizeDependentSettings(s); + if (s.quickResumeSleepScreen != quickResumeBeforeNormalize && needsResave) *needsResave = true; + LOG_DBG("CPS", "Settings loaded from file"); return true; diff --git a/src/SettingsList.h b/src/SettingsList.h index 7dc1f9a2..5fc16b06 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -54,7 +54,7 @@ inline const std::vector list = { "sleepTimeout", StrId::STR_CAT_DISPLAY), SettingInfo::Enum(StrId::STR_SLEEP_SCREEN, &CrossPointSettings::sleepScreen, {StrId::STR_DARK, StrId::STR_LIGHT, StrId::STR_CUSTOM, StrId::STR_COVER, StrId::STR_NONE_OPT, - StrId::STR_COVER_CUSTOM, StrId::STR_PAGE_OVERLAY}, + StrId::STR_COVER_CUSTOM, StrId::STR_PAGE_OVERLAY, StrId::STR_QUICK_RESUME}, "sleepScreen", StrId::STR_CAT_DISPLAY) .withSubcategory(StrId::STR_MENU_DISP_SLEEP), SettingInfo::Enum(StrId::STR_SLEEP_COVER_MODE, &CrossPointSettings::sleepScreenCoverMode, @@ -68,6 +68,8 @@ inline const std::vector list = { "sleepCoverOverlay", StrId::STR_CAT_DISPLAY), SettingInfo::Enum(StrId::STR_SLEEP_IMAGE_PICK_MODE, &CrossPointSettings::sleepImagePickMode, {StrId::STR_RANDOM, StrId::STR_SEQUENTIAL}, "sleepImagePickMode", StrId::STR_CAT_DISPLAY), + SettingInfo::Enum(StrId::STR_QUICK_RESUME_TIMEOUT, &CrossPointSettings::quickResumeSleepScreen, + {StrId::STR_STATE_OFF, StrId::STR_STATE_ON}, "quickResumeSleepScreen", StrId::STR_CAT_DISPLAY), SettingInfo::Enum(StrId::STR_HIDE_BATTERY, &CrossPointSettings::hideBatteryPercentage, {StrId::STR_NEVER, StrId::STR_IN_READER, StrId::STR_ALWAYS}, "hideBatteryPercentage", StrId::STR_CAT_DISPLAY) diff --git a/src/activities/ActivityManager.cpp b/src/activities/ActivityManager.cpp index 4b69e459..f009b069 100644 --- a/src/activities/ActivityManager.cpp +++ b/src/activities/ActivityManager.cpp @@ -351,8 +351,8 @@ void ActivityManager::returnFromChild() { } } -void ActivityManager::goToSleep() { - replaceActivity(std::make_unique(renderer, mappedInput)); +void ActivityManager::goToSleep(bool fromTimeout) { + replaceActivity(std::make_unique(renderer, mappedInput, fromTimeout)); loop(); // Important: sleep screen must be rendered immediately, the caller will go to sleep right after this returns } diff --git a/src/activities/ActivityManager.h b/src/activities/ActivityManager.h index 1d763206..fdf160e0 100644 --- a/src/activities/ActivityManager.h +++ b/src/activities/ActivityManager.h @@ -124,7 +124,8 @@ class ActivityManager { void goToBrowser(); void goToReader(std::string path); void goToKOReaderSync(); - void goToSleep(); + // fromTimeout=true marks this as an auto-sleep (used to gate Quick Resume on Timeout). + void goToSleep(bool fromTimeout = false); void goToBoot(); void goToFullScreenMessage(std::string message, EpdFontFamily::Style style = EpdFontFamily::REGULAR); void goToWeather(); diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index d22fbddc..88113fee 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -24,6 +24,7 @@ #include "components/UITheme.h" #include "fontIds.h" #include "images/Logo120.h" +#include "images/MoonIcon.h" namespace { @@ -353,6 +354,18 @@ size_t pickSleepImageIndex(size_t numFiles) { void SleepActivity::onEnter() { Activity::onEnter(); RenderLock lock(*this); + + // Quick Resume: paint a moon icon over the current page and keep the framebuffer + // intact for the next wake. Applies always when the user picked Quick Resume as + // sleep screen, or only on timeout sleeps when "Quick Resume on Timeout" is on. + const bool renderQuickResume = + SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::QUICK_RESUME || + (fromTimeout && + SETTINGS.quickResumeSleepScreen == CrossPointSettings::QUICK_RESUME_SLEEP_SCREEN::QUICK_RESUME_AFTER_TIMEOUT); + if (renderQuickResume) { + return renderLastScreenSleepScreen(); + } + // For OVERLAY mode the popup is suppressed so the frame buffer (reader page) stays intact if (SETTINGS.sleepScreen != CrossPointSettings::SLEEP_SCREEN_MODE::OVERLAY) { GUI.drawPopup(renderer, tr(STR_ENTERING_SLEEP)); @@ -804,6 +817,15 @@ void SleepActivity::renderBlankSleepScreen() const { renderer.displayBuffer(HalDisplay::HALF_REFRESH); } +void SleepActivity::renderLastScreenSleepScreen() const { + // Keep whatever is currently in the framebuffer (the reader page) and overlay a small moon + // icon to signal sleep. main.cpp persists the framebuffer to SD so the next wake can restore + // it before the boot screen would otherwise paint. + const auto pageHeight = renderer.getScreenHeight(); + renderer.drawImage(MoonIcon, 0, pageHeight - MOONICON_HEIGHT, MOONICON_WIDTH, MOONICON_HEIGHT); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); +} + void SleepActivity::renderOverlaySleepScreen() const { // Overlay pictures always use portrait orientation regardless of the reader's orientation preference. const auto savedOrientation = renderer.getOrientation(); diff --git a/src/activities/boot_sleep/SleepActivity.h b/src/activities/boot_sleep/SleepActivity.h index b783b935..73d7b68a 100644 --- a/src/activities/boot_sleep/SleepActivity.h +++ b/src/activities/boot_sleep/SleepActivity.h @@ -16,8 +16,8 @@ struct BookOverlayInfo { class SleepActivity final : public Activity { public: - explicit SleepActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) - : Activity("Sleep", renderer, mappedInput) {} + explicit SleepActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, bool fromTimeout = false) + : Activity("Sleep", renderer, mappedInput), fromTimeout(fromTimeout) {} void onEnter() override; private: @@ -28,5 +28,9 @@ class SleepActivity final : public Activity { bool topAlignForCoverFit = false) const; void renderBlankSleepScreen() const; void renderOverlaySleepScreen() const; + // Quick Resume: leaves the framebuffer (reader page) intact and overlays a small moon icon. + void renderLastScreenSleepScreen() const; BookOverlayInfo getBookOverlayInfo(const std::string& bookPath) const; + + const bool fromTimeout = false; }; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index ee8e5823..b85627d9 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -286,6 +286,7 @@ void SettingsActivity::toggleCurrentSetting() { : FontSelectionActivity::Target::EPUB; startActivityForResult(std::make_unique(renderer, mappedInput, target), [this](const ActivityResult&) { + CrossPointSettings::normalizeDependentSettings(SETTINGS); SETTINGS.saveToFile(); needsHalfRefresh = true; }); @@ -294,6 +295,7 @@ void SettingsActivity::toggleCurrentSetting() { if (setting.type == SettingType::ACTION) { auto resultHandler = [this](const ActivityResult& result) { + CrossPointSettings::normalizeDependentSettings(SETTINGS); SETTINGS.saveToFile(); needsHalfRefresh = true; const auto* menuResult = std::get_if(&result.data); @@ -323,6 +325,7 @@ void SettingsActivity::toggleCurrentSetting() { } setting.toggleValue(); + CrossPointSettings::normalizeDependentSettings(SETTINGS); SETTINGS.saveToFile(); } diff --git a/src/images/LoadingIcon.h b/src/images/LoadingIcon.h new file mode 100644 index 00000000..c54c857d --- /dev/null +++ b/src/images/LoadingIcon.h @@ -0,0 +1,24 @@ +#pragma once +#include + +// Image dimensions: 48x48, diagonal dots +#define LOADINGICON_WIDTH 48 +#define LOADINGICON_HEIGHT 48 + +static const uint8_t LoadingIcon[] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x0f, 0xff, + 0xff, 0xff, 0xff, 0xf8, 0x07, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x07, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x07, 0xff, + 0xff, 0xff, 0xff, 0xf8, 0x07, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x1f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x0f, 0xff, + 0xff, 0xff, 0xff, 0xf8, 0x07, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x07, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x07, 0xff, + 0xff, 0xff, 0xff, 0xf8, 0x07, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x1f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x0f, 0xff, + 0xff, 0xff, 0xff, 0xf8, 0x07, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x07, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x07, 0xff, + 0xff, 0xff, 0xff, 0xf8, 0x07, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x1f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; diff --git a/src/images/MoonIcon.h b/src/images/MoonIcon.h new file mode 100644 index 00000000..fcd7e07b --- /dev/null +++ b/src/images/MoonIcon.h @@ -0,0 +1,24 @@ +#pragma once +#include + +// Image dimensions: 48x48 +#define MOONICON_WIDTH 48 +#define MOONICON_HEIGHT 48 + +static const uint8_t MoonIcon[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF9, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0xF9, 0xFF, 0xF0, 0xFF, 0xFF, + 0xFF, 0xC0, 0x1F, 0xF0, 0x3F, 0xFF, 0xFF, 0x00, 0x1F, 0xF8, 0x0F, 0xFF, 0xFF, 0xF0, 0x7F, 0xFC, 0x07, 0xFF, + 0xFF, 0xF9, 0xFF, 0xFC, 0x07, 0xFF, 0xFF, 0xF9, 0xFF, 0xFC, 0x03, 0xFF, 0xFF, 0xF9, 0xFF, 0xFE, 0x01, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFE, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0xFF, 0xFF, 0xEF, 0xFF, 0xFC, 0x00, 0xFF, 0xFF, 0xEF, 0xFF, 0xFC, 0x00, 0xFF, + 0xFF, 0xC7, 0xFF, 0xF8, 0x00, 0xFF, 0xFF, 0xC3, 0xFF, 0xF8, 0x00, 0x7F, 0xFF, 0xC3, 0xFF, 0xF0, 0x00, 0x7F, + 0xFF, 0xC1, 0xFF, 0xC0, 0x00, 0x7F, 0xFF, 0xC0, 0x7F, 0x00, 0x00, 0x7F, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0xFF, + 0xFF, 0xC0, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x03, 0xFF, + 0xFF, 0xF0, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0xFC, 0x00, 0x00, 0x0F, 0xFF, + 0xFF, 0xFE, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x7F, 0xFF, + 0xFF, 0xFF, 0xF0, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; diff --git a/src/main.cpp b/src/main.cpp index 84ea1135..dfc3c235 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -38,6 +38,7 @@ #include "activities/settings/SdFirmwareUpdateActivity.h" #include "components/UITheme.h" #include "fontIds.h" +#include "images/LoadingIcon.h" #include "util/ButtonNavigator.h" #include "util/ScreenshotUtil.h" @@ -150,8 +151,44 @@ void silentRestartToReader() { ESP.restart(); } -// Enter deep sleep mode -void enterDeepSleep() { +// ---- Quick Resume framebuffer persistence ---- +// +// Quick Resume keeps the last reader page on screen during deep sleep (just overlaid with a moon +// icon) and skips the boot screen on wake by restoring the saved framebuffer + a loading icon. +// The buffer is persisted to SD only when the user actually enabled the feature. +constexpr char SLEEP_FRAME_FILE[] = "/.crosspoint/sleep_frame.bin"; + +static void saveSleepFrameBuffer() { + FsFile file; + if (!Storage.openFileForWrite("SLP", SLEEP_FRAME_FILE, file)) { + return; + } + file.write(renderer.getFrameBuffer(), renderer.getBufferSize()); + file.close(); +} + +// Restores the previously saved framebuffer into the display buffer. Returns false if the file is +// missing or the size does not match — in that case the caller should fall back to a boot screen. +// The file is always removed (success or failure) so a future cold boot does not see a stale frame. +static bool loadSleepFrameBuffer() { + FsFile file; + if (!Storage.openFileForRead("SLP", SLEEP_FRAME_FILE, file)) { + return false; + } + const size_t bufferSize = display.getBufferSize(); + const int bytesRead = file.read(display.getFrameBuffer(), bufferSize); + file.close(); + Storage.remove(SLEEP_FRAME_FILE); + return static_cast(bytesRead) == bufferSize; +} + +// Earliest millis() value at which a held-power-button press is allowed to trigger sleep. +// Set near the end of setup() so a wake-press held a bit too long does not bounce straight +// back into deep sleep before the user sees the page. +static unsigned long allowSleepAt = 0; + +// Enter deep sleep mode. fromTimeout=true marks an auto-sleep (gates "Quick Resume on Timeout"). +void enterDeepSleep(bool fromTimeout = false) { LOG_DBG("MAIN", "enterDeepSleep called at millis=%lu, powerBtn isPressed=%d, rawPin=%d", millis(), gpio.isPressed(HalGPIO::BTN_POWER), digitalRead(InputManager::POWER_BUTTON_PIN) == LOW); HalPowerManager::Lock powerLock; // Ensure we are at normal CPU frequency for sleep preparation @@ -166,6 +203,13 @@ void enterDeepSleep() { if (APP_STATE.lastSleepFromReader) { APP_STATE.readerActivityLoadCount = 0; } + + const bool isQuickResumeSleep = + SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::QUICK_RESUME || + (fromTimeout && + SETTINGS.quickResumeSleepScreen == CrossPointSettings::QUICK_RESUME_SLEEP_SCREEN::QUICK_RESUME_AFTER_TIMEOUT); + APP_STATE.showBootScreen = !isQuickResumeSleep; + APP_STATE.saveToFile(); // Tear down WiFi so the modem power domain isn't held alive across deep sleep. // Wake from deep sleep is effectively a chip reset, so no state needs to survive. @@ -173,7 +217,13 @@ void enterDeepSleep() { WiFi.disconnect(true); WiFi.mode(WIFI_OFF); } - activityManager.goToSleep(); + activityManager.goToSleep(fromTimeout); + + // Persist the moon-icon-overlaid framebuffer after goToSleep() has painted it. + if (isQuickResumeSleep) { + saveSleepFrameBuffer(); + } + halTiltSensor.deepSleep(); display.deepSleep(); @@ -183,8 +233,8 @@ void enterDeepSleep() { powerManager.startDeepSleep(gpio, keepLpAlive); } -void setupDisplayAndFonts() { - display.begin(); +void setupDisplayAndFonts(bool seamless = false) { + display.begin(seamless); renderer.begin(); activityManager.begin(); LOG_DBG("MAIN", "Display initialized"); @@ -354,6 +404,9 @@ void setup() { } SETTINGS.loadFromFile(); + // APP_STATE is needed before display init so Quick Resume can skip the on-wake resync + // and so the seamless-wake path can paint the LoadingIcon over the restored framebuffer. + APP_STATE.loadFromFile(); HalSystem::checkPanic(); HalSystem::clearPanic(); // TODO: move this to an activity when we have one to display the panic info @@ -368,20 +421,36 @@ void setup() { // First serial output only here to avoid timing inconsistencies for power button press duration verification LOG_DBG("MAIN", "Starting CrossPoint version " CROSSPOINT_VERSION); - setupDisplayAndFonts(); + const bool seamlessWake = !isSilentReboot && !APP_STATE.showBootScreen; + setupDisplayAndFonts(seamlessWake); - if (!isSilentReboot) { - activityManager.goToBoot(); - } else { + if (isSilentReboot) { // 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); + } else if (seamlessWake && loadSleepFrameBuffer()) { + // Quick Resume wake: framebuffer restored. Overlay a small loading icon to signal + // the device is busy reaching the reader (replacing the moon icon painted at sleep). + const auto pageHeight = renderer.getScreenHeight(); + renderer.drawImage(LoadingIcon, 0, pageHeight - LOADINGICON_HEIGHT, LOADINGICON_WIDTH, LOADINGICON_HEIGHT); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); + // Next sleep (whether crash, cold boot, or anything that doesn't go through enterDeepSleep) + // must fall back to a proper boot screen. + APP_STATE.showBootScreen = true; + APP_STATE.saveToFile(); + } else { + if (seamlessWake) { + // We promised a seamless wake but the framebuffer file was missing or unreadable — + // reset the flag so future cycles don't hit the same bad state. + APP_STATE.showBootScreen = true; + APP_STATE.saveToFile(); + } + activityManager.goToBoot(); } - APP_STATE.loadFromFile(); HalClock::restore(); RECENT_BOOKS.loadFromFile(); GLOBAL_BOOKMARKS.load(); @@ -420,6 +489,11 @@ void setup() { // Flush any pin state transitions that occurred during boot before entering the main loop mappedInputManager.update(); buttonEventManager.drain(); + + // Block held-power sleep for the first 2 seconds. On Quick Resume the user often releases + // the wake-press a fraction late; without this guard the loop() power-hold check would + // immediately fire enterDeepSleep(). + allowSleepAt = millis() + 2000; } void loop() { @@ -496,7 +570,7 @@ void loop() { const unsigned long sleepTimeoutMs = SETTINGS.getSleepTimeoutMs(); if (millis() - lastActivityTime >= sleepTimeoutMs) { LOG_DBG("SLP", "Auto-sleep triggered after %lu ms of inactivity", sleepTimeoutMs); - enterDeepSleep(); + enterDeepSleep(/*fromTimeout=*/true); // This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start return; } @@ -510,7 +584,7 @@ void loop() { powerHoldStart = millis(); LOG_DBG("MAIN", "loop: power button press detected (fresh edge)"); } - if (gpio.isPressed(HalGPIO::BTN_POWER) && powerHoldStart > 0) { + if (millis() >= allowSleepAt && gpio.isPressed(HalGPIO::BTN_POWER) && powerHoldStart > 0) { const unsigned long heldTime = millis() - powerHoldStart; if (heldTime > SETTINGS.getPowerButtonDuration()) { // If the screenshot combination is potentially being pressed, don't sleep diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index 8e9f407e..ec040f80 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -1505,6 +1505,7 @@ void CrossPointWebServer::handlePostSettings() { } } + CrossPointSettings::normalizeDependentSettings(SETTINGS); SETTINGS.saveToFile(); LOG_DBG("WEB", "Applied %d setting(s)", applied);