Add quick resume

This commit is contained in:
jpirnay
2026-05-21 23:13:28 +02:00
parent ce521a63aa
commit 5bb3980dc0
40 changed files with 262 additions and 26 deletions
+2
View File
@@ -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: "Хуткае узнаўленне пасля таймаўту"
+2
View File
@@ -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"
+2
View File
@@ -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í"
+2
View File
@@ -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"
+2
View File
@@ -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"
+2
View File
@@ -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"
+2
View File
@@ -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"
+2
View File
@@ -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"
+2
View File
@@ -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"
+2
View File
@@ -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"
+2
View File
@@ -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"
+2
View File
@@ -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: "Таймауттан кейін жылдам жалғастыру"
+2
View File
@@ -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"
+2
View File
@@ -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"
+2
View File
@@ -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"
+2
View File
@@ -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"
+2
View File
@@ -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"
+2
View File
@@ -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: "Быстрое возобновление по таймауту"
+2
View File
@@ -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"
+2
View File
@@ -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"
+2
View File
@@ -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"
+2
View File
@@ -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"
+2
View File
@@ -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: "Швидке продовження після таймауту"
+2
View File
@@ -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"
+9 -6
View File
@@ -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();
}
}
}
+4 -2
View File
@@ -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;
+6
View File
@@ -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};
+13
View File
@@ -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;
+3
View File
@@ -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;
+6
View File
@@ -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<JsonObject>();
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<JsonObject>();
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;
+3 -1
View File
@@ -54,7 +54,7 @@ inline const std::vector<SettingInfo> 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<SettingInfo> 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)
+2 -2
View File
@@ -351,8 +351,8 @@ void ActivityManager::returnFromChild() {
}
}
void ActivityManager::goToSleep() {
replaceActivity(std::make_unique<SleepActivity>(renderer, mappedInput));
void ActivityManager::goToSleep(bool fromTimeout) {
replaceActivity(std::make_unique<SleepActivity>(renderer, mappedInput, fromTimeout));
loop(); // Important: sleep screen must be rendered immediately, the caller will go to sleep right after this returns
}
+2 -1
View File
@@ -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();
@@ -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();
+6 -2
View File
@@ -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;
};
@@ -286,6 +286,7 @@ void SettingsActivity::toggleCurrentSetting() {
: FontSelectionActivity::Target::EPUB;
startActivityForResult(std::make_unique<FontSelectionActivity>(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<MenuResult>(&result.data);
@@ -323,6 +325,7 @@ void SettingsActivity::toggleCurrentSetting() {
}
setting.toggleValue();
CrossPointSettings::normalizeDependentSettings(SETTINGS);
SETTINGS.saveToFile();
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <cstdint>
// 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};
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <cstdint>
// 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};
+86 -12
View File
@@ -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<size_t>(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
+1
View File
@@ -1505,6 +1505,7 @@ void CrossPointWebServer::handlePostSettings() {
}
}
CrossPointSettings::normalizeDependentSettings(SETTINGS);
SETTINGS.saveToFile();
LOG_DBG("WEB", "Applied %d setting(s)", applied);