From 41e6e15229da9f2d1110622518d5edad8e09626a Mon Sep 17 00:00:00 2001 From: Eloren1 Date: Wed, 20 May 2026 01:45:28 +0300 Subject: [PATCH] feat: Seamless sleep/wake screens for displaying book pages during deep sleep (#2064) image Actualized "Last" sleep screen setting from previous PRs, rebranded as a ~~`Seamless Sleep`~~ `Page as Sleep Screen` option with more improvements. https://github.com/user-attachments/assets/59029ba6-007e-4841-abfa-f680d7e98b79 --- New option: `Page as Sleep Screen` - `Never (default)`, `After Timeout`, `Always` When enabled, it seamlessly sleeps on timeout or power off, making a fast refresh for the moon icon. When waking up, we still show the last page, instead of the boot screen, making it fully seamless. I tried different icons such as "refresh arrow" and others, but they looked not as nice as 3 simple dots. With this mode, the device turns off 4 seconds faster. And has 6 seconds less delay when turning back on. Much more responsive. Previously, even a 10-minute timeout sometimes wasn't enough, and I'd worry about seeing the book cover. It's now easier to use a shorter sleep timeout: if I get distracted during a reading session but don't want to stop, the new screen is much more inviting to come back to. --- Did you use AI tools to help write this code? _**PARTIALLY**_. --- Test v1.3.0 firmware.bin file [download](https://github.com/user-attachments/files/28015193/firmware.zip) Based on PRs #410 and #495 Closes #400, #1649 --------- Co-authored-by: Claude Sonnet 4.6 --- 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.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/hal/HalDisplay.cpp | 15 +++-- lib/hal/HalDisplay.h | 2 +- src/CrossPointSettings.h | 9 +++ src/CrossPointState.h | 1 + src/JsonSettingsIO.cpp | 2 + src/SettingsList.h | 3 + src/activities/ActivityManager.cpp | 4 +- src/activities/ActivityManager.h | 2 +- src/activities/boot_sleep/SleepActivity.cpp | 16 +++++ src/activities/boot_sleep/SleepActivity.h | 7 +- src/images/LoadingIcon.h | 24 +++++++ src/images/MoonIcon.h | 24 +++++++ src/main.cpp | 72 +++++++++++++++++---- 35 files changed, 202 insertions(+), 23 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 b5b29be7..cecb40de 100644 --- a/lib/I18n/translations/belarusian.yaml +++ b/lib/I18n/translations/belarusian.yaml @@ -206,6 +206,8 @@ STR_THEME_CLASSIC: "Класічная" STR_THEME_LYRA: "Lyra" STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Кампенсацыя выцвітання" +STR_SEAMLESS_SLEEP: "Старонка як экран сну" +STR_AFTER_TIMEOUT: "Пасля таймаўту" STR_REMAP_FRONT_BUTTONS: "Пераназначыць пярэднія кнопкі" STR_OPDS_BROWSER: "OPDS браўзер" STR_COVER_CUSTOM: "Вокладка + Свой" diff --git a/lib/I18n/translations/catalan.yaml b/lib/I18n/translations/catalan.yaml index 7e3296bc..08b8f148 100644 --- a/lib/I18n/translations/catalan.yaml +++ b/lib/I18n/translations/catalan.yaml @@ -234,6 +234,8 @@ STR_THEME_CLASSIC: "Clàssic" STR_THEME_LYRA: "Lyra" STR_THEME_LYRA_EXTENDED: "Lyra Ampliat" STR_SUNLIGHT_FADING_FIX: "Correcció de l'esvaïment pel sol" +STR_SEAMLESS_SLEEP: "Pàgina com a repòs" +STR_AFTER_TIMEOUT: "Després del temps" STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals" STR_OPDS_BROWSER: "Navegador OPDS" STR_COVER_CUSTOM: "Portada + Personalitzat" diff --git a/lib/I18n/translations/czech.yaml b/lib/I18n/translations/czech.yaml index c11b9e43..7363a98c 100644 --- a/lib/I18n/translations/czech.yaml +++ b/lib/I18n/translations/czech.yaml @@ -212,6 +212,8 @@ STR_THEME_LYRA: "Lyra" STR_THEME_ROUNDEDRAFF: "RoundedRaff" STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Oprava blednutí na slunci" +STR_SEAMLESS_SLEEP: "Stránka jako spánek" +STR_AFTER_TIMEOUT: "Po vypršení" STR_REMAP_FRONT_BUTTONS: "Přemapovat přední tlačítka" STR_OPDS_BROWSER: "Prohlížeč OPDS" STR_COVER_CUSTOM: "Obálka + Vlastní" diff --git a/lib/I18n/translations/danish.yaml b/lib/I18n/translations/danish.yaml index 9a888af1..0126aec8 100644 --- a/lib/I18n/translations/danish.yaml +++ b/lib/I18n/translations/danish.yaml @@ -234,6 +234,8 @@ STR_THEME_CLASSIC: "Klassisk" STR_THEME_LYRA: "Lyra" STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Sollysfading-rettelse" +STR_SEAMLESS_SLEEP: "Side som dvaleskærm" +STR_AFTER_TIMEOUT: "Efter timeout" STR_REMAP_FRONT_BUTTONS: "Omtildel frontknapper" STR_OPDS_BROWSER: "OPDS Browser" STR_COVER_CUSTOM: "Omslag + Brugerdefineret" diff --git a/lib/I18n/translations/dutch.yaml b/lib/I18n/translations/dutch.yaml index e8503553..917ac475 100644 --- a/lib/I18n/translations/dutch.yaml +++ b/lib/I18n/translations/dutch.yaml @@ -234,6 +234,8 @@ STR_THEME_CLASSIC: "Klassiek" STR_THEME_LYRA: "Lyra" STR_THEME_LYRA_EXTENDED: "Lyra Uitgebreid" STR_SUNLIGHT_FADING_FIX: "Zonlicht vervaging fix" +STR_SEAMLESS_SLEEP: "Pagina als slaapscherm" +STR_AFTER_TIMEOUT: "Na timeout" STR_REMAP_FRONT_BUTTONS: "Knoppen voorzijde wijzigen" STR_OPDS_BROWSER: "OPDS-browser" STR_COVER_CUSTOM: "Omslag + Aangepast" diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 8458ec63..c10420fc 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -62,6 +62,8 @@ STR_CAT_READER: "Reader" STR_CAT_CONTROLS: "Controls" STR_CAT_SYSTEM: "System" STR_SLEEP_SCREEN: "Sleep Screen" +STR_SEAMLESS_SLEEP: "Page as Sleep Screen" +STR_AFTER_TIMEOUT: "After 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 c6e7e240..cd7b3b6d 100644 --- a/lib/I18n/translations/finnish.yaml +++ b/lib/I18n/translations/finnish.yaml @@ -211,6 +211,8 @@ STR_THEME_CLASSIC: "Klassinen" STR_THEME_LYRA: "Lyra" STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Auringonvalon haalistumiskorjaus" +STR_SEAMLESS_SLEEP: "Sivu lepotilassa" +STR_AFTER_TIMEOUT: "Aikakatkon jälkeen" STR_REMAP_FRONT_BUTTONS: "Uudelleenmääritä etupainikkeet" STR_OPDS_BROWSER: "OPDS-selain" STR_COVER_CUSTOM: "Kansi + mukautettu" diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index b7cd1323..0cfc4b55 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -235,6 +235,8 @@ STR_THEME_LYRA: "Lyra" STR_THEME_ROUNDEDRAFF: "RoundedRaff" STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Correction lisibilité au soleil" +STR_SEAMLESS_SLEEP: "Page comme veille" +STR_AFTER_TIMEOUT: "Après délai" STR_REMAP_FRONT_BUTTONS: "Configurer boutons façade" STR_OPDS_BROWSER: "Navigateur OPDS" STR_COVER_CUSTOM: "Couverture + Perso" diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index 62f4980e..9215635e 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -233,6 +233,8 @@ STR_THEME_LYRA: "Lyra" STR_THEME_ROUNDEDRAFF: "RoundedRaff" STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Anti-Verblassen" +STR_SEAMLESS_SLEEP: "Seite als Ruhebild" +STR_AFTER_TIMEOUT: "Nach Timeout" STR_REMAP_FRONT_BUTTONS: "Vordere Tasten belegen" STR_OPDS_BROWSER: "OPDS-Browser" STR_COVER_CUSTOM: "Umschlag + Eigenes" diff --git a/lib/I18n/translations/hungarian.yaml b/lib/I18n/translations/hungarian.yaml index 79fb9e7d..166e4b00 100644 --- a/lib/I18n/translations/hungarian.yaml +++ b/lib/I18n/translations/hungarian.yaml @@ -231,6 +231,8 @@ STR_THEME_CLASSIC: "Klasszikus" STR_THEME_LYRA: "Lyra" STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Napfény halványulás javítás" +STR_SEAMLESS_SLEEP: "Oldal alvóképernyő" +STR_AFTER_TIMEOUT: "Időtúllépés után" STR_REMAP_FRONT_BUTTONS: "Elülső gombok átállítása" STR_OPDS_BROWSER: "OPDS böngésző" STR_COVER_CUSTOM: "Borító + Egyéni" diff --git a/lib/I18n/translations/italian.yaml b/lib/I18n/translations/italian.yaml index e0b7ba0a..4ccdacc7 100644 --- a/lib/I18n/translations/italian.yaml +++ b/lib/I18n/translations/italian.yaml @@ -242,6 +242,8 @@ STR_THEME_LYRA: "Lyra" STR_THEME_ROUNDEDRAFF: "RoundedRaff" STR_THEME_LYRA_EXTENDED: "Lyra esteso" STR_SUNLIGHT_FADING_FIX: "Correzione luce solare" +STR_SEAMLESS_SLEEP: "Pagina come standby" +STR_AFTER_TIMEOUT: "Dopo timeout" STR_REMAP_FRONT_BUTTONS: "Rimappa pulsanti frontali" STR_OPDS_BROWSER: "Browser OPDS" STR_SEARCH: "Cerca" diff --git a/lib/I18n/translations/kazakh.yaml b/lib/I18n/translations/kazakh.yaml index 7f705971..9c7417c2 100644 --- a/lib/I18n/translations/kazakh.yaml +++ b/lib/I18n/translations/kazakh.yaml @@ -207,6 +207,8 @@ STR_THEME_CLASSIC: "Классикалық" STR_THEME_LYRA: "Lyra" STR_THEME_LYRA_EXTENDED: "Lyra кеңейтілген" STR_SUNLIGHT_FADING_FIX: "Күн сәулесінен солу түзету" +STR_SEAMLESS_SLEEP: "Бет – ұйқы экраны" +STR_AFTER_TIMEOUT: "Таймауттан кейін" STR_REMAP_FRONT_BUTTONS: "Алдыңғы түймелерді қайта баптау" STR_OPDS_BROWSER: "OPDS шолғышы" STR_COVER_CUSTOM: "Мұқаба + Өзгертілген" diff --git a/lib/I18n/translations/lithuanian.yaml b/lib/I18n/translations/lithuanian.yaml index 3dcc1965..154669e0 100644 --- a/lib/I18n/translations/lithuanian.yaml +++ b/lib/I18n/translations/lithuanian.yaml @@ -231,6 +231,8 @@ STR_THEME_CLASSIC: "Klasikinė" STR_THEME_LYRA: "Lyra" STR_THEME_LYRA_EXTENDED: "Lyra Ext." STR_SUNLIGHT_FADING_FIX: "Blyškumo pataisa" +STR_SEAMLESS_SLEEP: "Puslapis miego ekrane" +STR_AFTER_TIMEOUT: "Po skirtojo laiko" STR_REMAP_FRONT_BUTTONS: "Keisti mygtukus" STR_OPDS_BROWSER: "OPDS naršyklė" STR_COVER_CUSTOM: "Viršelis + Kita" diff --git a/lib/I18n/translations/polish.yaml b/lib/I18n/translations/polish.yaml index e63cbf90..3b85b70e 100644 --- a/lib/I18n/translations/polish.yaml +++ b/lib/I18n/translations/polish.yaml @@ -242,6 +242,8 @@ STR_THEME_LYRA: "Lyra" STR_THEME_ROUNDEDRAFF: "RoundedRaff" STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Przeciwdziałanie blaknięciu od słońca" +STR_SEAMLESS_SLEEP: "Strona jako ekran snu" +STR_AFTER_TIMEOUT: "Po upływie czasu" STR_REMAP_FRONT_BUTTONS: "Skonfiguruj przednie przyciski" STR_OPDS_BROWSER: "OPDS Browser" STR_SEARCH: "Szukaj" diff --git a/lib/I18n/translations/portuguese.yaml b/lib/I18n/translations/portuguese.yaml index 508a42a4..1d13d16d 100644 --- a/lib/I18n/translations/portuguese.yaml +++ b/lib/I18n/translations/portuguese.yaml @@ -212,6 +212,8 @@ STR_THEME_LYRA: "Lyra" STR_THEME_ROUNDEDRAFF: "RoundedRaff" STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Ajuste desbotamento ao sol" +STR_SEAMLESS_SLEEP: "Página como repouso" +STR_AFTER_TIMEOUT: "Após tempo limite" STR_REMAP_FRONT_BUTTONS: "Remapear botões frontais" STR_OPDS_BROWSER: "Navegador OPDS" STR_COVER_CUSTOM: "Capa + personalizado" diff --git a/lib/I18n/translations/romanian.yaml b/lib/I18n/translations/romanian.yaml index 1fe47e51..e8ffe123 100644 --- a/lib/I18n/translations/romanian.yaml +++ b/lib/I18n/translations/romanian.yaml @@ -234,6 +234,8 @@ STR_THEME_CLASSIC: "Clasic" STR_THEME_LYRA: "Lyra" STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Corecţie estompare lumină" +STR_SEAMLESS_SLEEP: "Pagină ca repaus" +STR_AFTER_TIMEOUT: "După timeout" STR_REMAP_FRONT_BUTTONS: "Remapare butoane frontale" STR_OPDS_BROWSER: "Browser OPDS" STR_COVER_CUSTOM: "Copertă + Personalizat" diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index 02c94c86..b74d8fa2 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -242,6 +242,8 @@ STR_THEME_LYRA: "Lyra" STR_THEME_ROUNDEDRAFF: "RoundedRaff" STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Компенсация выцветания" +STR_SEAMLESS_SLEEP: "Страница как экран сна" +STR_AFTER_TIMEOUT: "По таймауту" STR_REMAP_FRONT_BUTTONS: "Переназначить передние кнопки" STR_OPDS_BROWSER: "OPDS браузер" STR_SEARCH: "Поиск" diff --git a/lib/I18n/translations/slovenian.yaml b/lib/I18n/translations/slovenian.yaml index 69b7d6be..eddfd5c9 100644 --- a/lib/I18n/translations/slovenian.yaml +++ b/lib/I18n/translations/slovenian.yaml @@ -231,6 +231,8 @@ STR_THEME_CLASSIC: "Klasična" STR_THEME_LYRA: "Lyra" STR_THEME_LYRA_EXTENDED: "Lyra razširjena" STR_SUNLIGHT_FADING_FIX: "Popravek bledenja na soncu" +STR_SEAMLESS_SLEEP: "Stran kot zaslon sna" +STR_AFTER_TIMEOUT: "Po izteku" STR_REMAP_FRONT_BUTTONS: "Prenastavi sprednje gumbe" STR_OPDS_BROWSER: "OPDS brskalnik" STR_COVER_CUSTOM: "Naslovnica + po meri" diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index 919d9439..65990dc2 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -242,6 +242,8 @@ STR_THEME_LYRA: "Lyra" STR_THEME_ROUNDEDRAFF: "RoundedRaff" STR_THEME_LYRA_EXTENDED: "Lyra Extendido" STR_SUNLIGHT_FADING_FIX: "Corrección de desvanecimiento" +STR_SEAMLESS_SLEEP: "Página como reposo" +STR_AFTER_TIMEOUT: "Tras tiempo" STR_REMAP_FRONT_BUTTONS: "Reconfigurar botones frontales" STR_OPDS_BROWSER: "Navegador OPDS" STR_SEARCH: "Buscar" diff --git a/lib/I18n/translations/swedish.yaml b/lib/I18n/translations/swedish.yaml index 66e62db3..1c311be5 100644 --- a/lib/I18n/translations/swedish.yaml +++ b/lib/I18n/translations/swedish.yaml @@ -242,6 +242,8 @@ STR_THEME_LYRA: "Lyra" STR_THEME_ROUNDEDRAFF: "RoundedRaff" STR_THEME_LYRA_EXTENDED: "Lyra utökad" STR_SUNLIGHT_FADING_FIX: "Fix för solskensmattning" +STR_SEAMLESS_SLEEP: "Sida som viloskärm" +STR_AFTER_TIMEOUT: "Efter timeout" STR_REMAP_FRONT_BUTTONS: "Ändra frontknappar" STR_OPDS_BROWSER: "OPDS-webbläsare" STR_SEARCH: "Sök" diff --git a/lib/I18n/translations/turkish.yaml b/lib/I18n/translations/turkish.yaml index a8f15a7c..5fa44cc1 100644 --- a/lib/I18n/translations/turkish.yaml +++ b/lib/I18n/translations/turkish.yaml @@ -211,6 +211,8 @@ STR_THEME_CLASSIC: "Klasik" STR_THEME_LYRA: "Lyra" STR_THEME_LYRA_EXTENDED: "Lyra Genişletilmiş" STR_SUNLIGHT_FADING_FIX: "Güneş Işığı Solma Düzeltmesi" +STR_SEAMLESS_SLEEP: "Sayfa uyku ekranı" +STR_AFTER_TIMEOUT: "Zaman aşımında" STR_REMAP_FRONT_BUTTONS: "Ön Tuşları Yeniden Ata" STR_OPDS_BROWSER: "OPDS Tarayıcı" STR_COVER_CUSTOM: "Kapak + Özel" diff --git a/lib/I18n/translations/ukrainian.yaml b/lib/I18n/translations/ukrainian.yaml index 1cfa2c28..a23d33f8 100644 --- a/lib/I18n/translations/ukrainian.yaml +++ b/lib/I18n/translations/ukrainian.yaml @@ -242,6 +242,8 @@ STR_THEME_LYRA: "Lyra" STR_THEME_ROUNDEDRAFF: "RoundedRaff" STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Виправлення вицвітання на сонці" +STR_SEAMLESS_SLEEP: "Сторінка як екран сну" +STR_AFTER_TIMEOUT: "Після таймауту" STR_REMAP_FRONT_BUTTONS: "Налаштувати передні кнопки" STR_OPDS_BROWSER: "Браузер OPDS" STR_SEARCH: "Пошук" diff --git a/lib/hal/HalDisplay.cpp b/lib/hal/HalDisplay.cpp index 665df535..e531f1da 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 screen content is preserved. + 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 a0a7f920..4d51bdda 100644 --- a/lib/hal/HalDisplay.h +++ b/lib/hal/HalDisplay.h @@ -18,7 +18,7 @@ class HalDisplay { }; // Initialize the display hardware and driver - void begin(); + void begin(bool seamless = false); // Display dimensions static constexpr uint16_t DISPLAY_WIDTH = EInkDisplay::DISPLAY_WIDTH; diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index c8774b1f..38d7c1c4 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -155,6 +155,13 @@ class CrossPointSettings { enum TILT_PAGE_TURN { TILT_OFF = 0, TILT_NORMAL = 1, TILT_NVERTED = 2, TILT_PAGE_TURN_COUNT }; + enum SEAMLESS_SLEEP_SCREEN { + SEAMLESS_NEVER = 0, + SEAMLESS_AFTER_TIMEOUT = 1, + SEAMLESS_ALWAYS = 2, + SEAMLESS_SLEEP_SCREEN_COUNT + }; + // Sleep screen settings uint8_t sleepScreen = DARK; // Sleep screen cover mode settings @@ -242,6 +249,8 @@ class CrossPointSettings { uint8_t tiltPageTurn = TILT_OFF; // Language setting (Language enum index, default 0 = EN) uint8_t language = 0; + // Seamless sleep: keep current content visible with moon icon instead of showing sleep screen + uint8_t seamlessSleepScreen = SEAMLESS_NEVER; ~CrossPointSettings() = default; diff --git a/src/CrossPointState.h b/src/CrossPointState.h index 4b18f3b1..80d5d560 100644 --- a/src/CrossPointState.h +++ b/src/CrossPointState.h @@ -15,6 +15,7 @@ class CrossPointState { uint8_t recentSleepFill = 0; // valid entries (0..SLEEP_RECENT_COUNT) uint8_t readerActivityLoadCount = 0; bool lastSleepFromReader = false; + bool showBootScreen = true; // Returns true if idx was shown within the last checkCount picks. // Walks backwards from the most recently written slot. diff --git a/src/JsonSettingsIO.cpp b/src/JsonSettingsIO.cpp index 6becefc3..065c6e51 100644 --- a/src/JsonSettingsIO.cpp +++ b/src/JsonSettingsIO.cpp @@ -75,6 +75,7 @@ bool JsonSettingsIO::saveState(const CrossPointState& s, const char* path) { doc["recentSleepFill"] = s.recentSleepFill; doc["readerActivityLoadCount"] = s.readerActivityLoadCount; doc["lastSleepFromReader"] = s.lastSleepFromReader; + doc["showBootScreen"] = s.showBootScreen; String json; serializeJson(doc, json); @@ -109,6 +110,7 @@ bool JsonSettingsIO::loadState(CrossPointState& s, const char* json) { } s.readerActivityLoadCount = doc["readerActivityLoadCount"] | static_cast(0); s.lastSleepFromReader = doc["lastSleepFromReader"] | false; + s.showBootScreen = doc["showBootScreen"] | true; return true; } diff --git a/src/SettingsList.h b/src/SettingsList.h index c17b47da..1f3e9f78 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -113,6 +113,9 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist SettingInfo::Enum(StrId::STR_SLEEP_COVER_FILTER, &CrossPointSettings::sleepScreenCoverFilter, {StrId::STR_NONE_OPT, StrId::STR_FILTER_CONTRAST, StrId::STR_INVERTED}, "sleepScreenCoverFilter", StrId::STR_CAT_DISPLAY), + SettingInfo::Enum(StrId::STR_SEAMLESS_SLEEP, &CrossPointSettings::seamlessSleepScreen, + {StrId::STR_NEVER, StrId::STR_AFTER_TIMEOUT, StrId::STR_ALWAYS}, "seamlessSleepScreen", + 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 b9b3ad7d..524ed3e0 100644 --- a/src/activities/ActivityManager.cpp +++ b/src/activities/ActivityManager.cpp @@ -196,8 +196,8 @@ void ActivityManager::goToReader(std::string path) { replaceActivity(std::make_unique(renderer, mappedInput, std::move(path))); } -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 edd1f48f..9066888d 100644 --- a/src/activities/ActivityManager.h +++ b/src/activities/ActivityManager.h @@ -86,7 +86,7 @@ class ActivityManager { void goToRecentBooks(); void goToBrowser(); void goToReader(std::string path); - void goToSleep(); + void goToSleep(bool fromTimeout = false); void goToBoot(); void goToFullScreenMessage(std::string message, EpdFontFamily::Style style = EpdFontFamily::REGULAR); void goToCrashReport(); diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index 19f443b0..5ea25653 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -14,10 +14,20 @@ #include "components/UITheme.h" #include "fontIds.h" #include "images/Logo120.h" +#include "images/MoonIcon.h" void SleepActivity::onEnter() { Activity::onEnter(); + const bool renderSeamless = + SETTINGS.seamlessSleepScreen == CrossPointSettings::SEAMLESS_SLEEP_SCREEN::SEAMLESS_ALWAYS || + (fromTimeout && + SETTINGS.seamlessSleepScreen == CrossPointSettings::SEAMLESS_SLEEP_SCREEN::SEAMLESS_AFTER_TIMEOUT); + + if (renderSeamless) { + return renderLastScreenSleepScreen(); + } + // Show popup with reader orientation only when going to sleep from reader if (APP_STATE.lastSleepFromReader) { ReaderUtils::applyOrientation(renderer, SETTINGS.orientation); @@ -307,6 +317,12 @@ void SleepActivity::renderCoverSleepScreen() const { return (this->*renderNoCoverSleepScreen)(); } +void SleepActivity::renderLastScreenSleepScreen() const { + const auto pageHeight = renderer.getScreenHeight(); + renderer.drawImage(MoonIcon, 0, pageHeight - MOONICON_HEIGHT, MOONICON_WIDTH, MOONICON_HEIGHT); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); +} + void SleepActivity::renderBlankSleepScreen() const { renderer.clearScreen(); renderer.displayBuffer(HalDisplay::HALF_REFRESH); diff --git a/src/activities/boot_sleep/SleepActivity.h b/src/activities/boot_sleep/SleepActivity.h index 7d6235b6..e74a7d62 100644 --- a/src/activities/boot_sleep/SleepActivity.h +++ b/src/activities/boot_sleep/SleepActivity.h @@ -5,8 +5,8 @@ class Bitmap; 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: @@ -14,5 +14,8 @@ class SleepActivity final : public Activity { void renderCustomSleepScreen() const; void renderCoverSleepScreen() const; void renderBitmapSleepScreen(const Bitmap& bitmap) const; + void renderLastScreenSleepScreen() const; void renderBlankSleepScreen() const; + + bool fromTimeout = false; }; 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 3b7e7c9e..259bd368 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -30,6 +30,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" @@ -39,6 +40,7 @@ ActivityManager activityManager(renderer, mappedInputManager); FontDecompressor fontDecompressor; SdCardFontSystem sdFontSystem; FontCacheManager fontCacheManager(renderer.getFontMap(), renderer.getSdCardFonts()); +static unsigned long allowSleepAt = 0; // Fonts EpdFont notoserif14RegularFont(¬oserif_14_regular); @@ -207,13 +209,46 @@ void waitForPowerRelease() { } } +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(); +} + +static bool loadSleepFrameBuffer() { + FsFile file; + if (!Storage.openFileForRead("SLP", SLEEP_FRAME_FILE, file)) return false; + const size_t bufferSize = display.getBufferSize(); + const size_t bytesRead = file.read(display.getFrameBuffer(), bufferSize); + file.close(); + if (bytesRead != bufferSize) { + Storage.remove(SLEEP_FRAME_FILE); + return false; + } + Storage.remove(SLEEP_FRAME_FILE); + return true; +} + // Enter deep sleep mode -void enterDeepSleep() { +void enterDeepSleep(bool fromTimeout = false) { HalPowerManager::Lock powerLock; // Ensure we are at normal CPU frequency for sleep preparation APP_STATE.lastSleepFromReader = activityManager.isReaderActivity(); + + const bool isSeamless = SETTINGS.seamlessSleepScreen == CrossPointSettings::SEAMLESS_SLEEP_SCREEN::SEAMLESS_ALWAYS || + (fromTimeout && SETTINGS.seamlessSleepScreen == + CrossPointSettings::SEAMLESS_SLEEP_SCREEN::SEAMLESS_AFTER_TIMEOUT); + APP_STATE.showBootScreen = !isSeamless; + APP_STATE.saveToFile(); - activityManager.goToSleep(); + activityManager.goToSleep(fromTimeout); + + if (isSeamless) { + saveSleepFrameBuffer(); + } // 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. @@ -229,8 +264,8 @@ void enterDeepSleep() { powerManager.startDeepSleep(gpio); } -void setupDisplayAndFonts() { - display.begin(); +void setupDisplayAndFonts(bool seamless = false) { + display.begin(seamless); renderer.begin(); activityManager.begin(); LOG_DBG("MAIN", "Display initialized"); @@ -313,6 +348,8 @@ void setup() { HalSystem::checkPanic(); SETTINGS.loadFromFile(); + APP_STATE.loadFromFile(); + RECENT_BOOKS.loadFromFile(); I18N.setLanguage(static_cast(SETTINGS.language)); KOREADER_STORE.loadFromFile(); OPDS_STORE.loadFromFile(); @@ -360,17 +397,28 @@ 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(); + setupDisplayAndFonts(/*seamless=*/!APP_STATE.showBootScreen); // First paint after silent reboot is HALF_REFRESH (SDK forces it after begin()'s // panel reset); subsequent paints FAST. if (!isSilentReboot) { - activityManager.goToBoot(); + if (APP_STATE.showBootScreen) { + activityManager.goToBoot(); + } else if (loadSleepFrameBuffer()) { + // Seamless wake: buffer restored, replace moon icon with loading icon + const auto pageHeight = renderer.getScreenHeight(); + renderer.drawImage(LoadingIcon, 0, pageHeight - LOADINGICON_HEIGHT, LOADINGICON_WIDTH, LOADINGICON_HEIGHT); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); + APP_STATE.showBootScreen = true; + APP_STATE.saveToFile(); + } else { + // Frame buffer file missing — fall back to normal boot screen + APP_STATE.showBootScreen = true; + APP_STATE.saveToFile(); + activityManager.goToBoot(); + } } - APP_STATE.loadFromFile(); - RECENT_BOOKS.loadFromFile(); - if (recoveryFirmwareMode) { // Skip normal home/reader routing: jump straight into the SD firmware picker. activityManager.replaceActivity( @@ -401,6 +449,7 @@ void setup() { // Ensure we're not still holding the power button before leaving setup waitForPowerRelease(); + allowSleepAt = millis() + 2000; } void loop() { @@ -471,12 +520,13 @@ 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(true); // This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start return; } - if (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.getPowerButtonHeldTime() > SETTINGS.getPowerButtonDuration()) { + if (millis() >= allowSleepAt && gpio.isPressed(HalGPIO::BTN_POWER) && + gpio.getPowerButtonHeldTime() > SETTINGS.getPowerButtonDuration()) { // If the screenshot combination is potentially being pressed, don't sleep if (gpio.isPressed(HalGPIO::BTN_DOWN)) { return;