Merge pull request #254 from jpirnay/feat-quick-resume

feat: Add quick resume (idea from #2064/#2089)
This commit is contained in:
jpirnay
2026-05-22 14:17:48 +02:00
committed by GitHub
41 changed files with 328 additions and 39 deletions
+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);
@@ -301,6 +303,7 @@ void SettingsActivity::toggleCurrentSetting() {
auto activity = createActivityForAction(static_cast<SettingAction>(menuResult->action), renderer, mappedInput);
if (activity) {
startActivityForResult(std::move(activity), [this](const ActivityResult&) {
CrossPointSettings::normalizeDependentSettings(SETTINGS);
SETTINGS.saveToFile();
needsHalfRefresh = true;
});
@@ -323,6 +326,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};
+138 -20
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"
@@ -134,7 +135,25 @@ constexpr uint32_t SILENT_REBOOT_MAGIC = 0xC1EAB007;
constexpr uint32_t SILENT_REBOOT_TARGET_HOME = 0;
constexpr uint32_t SILENT_REBOOT_TARGET_READER = 1;
// How the device is coming back to life, resolved once at boot. Both resume
// flows suppress the splash and leave the panel holding its pre-boot frame; a
// plain boot shows the splash. See setup() for the resolution.
enum class BootResume : uint8_t {
Splash, // cold boot, flash, panic, or plain reboot
Silent, // heap-defrag ESP.restart() (RTC flag; lost on power loss)
QuickResume, // wake from a quick-resume deep sleep (SD flag; survives power loss)
};
// Latched true once enterDeepSleep() commits to sleeping, before it tears down
// the current activity. WiFi activities call silentRestart() in onExit() to
// clear heap fragmentation on the way out, but deep sleep is a full chip reset
// on wake and already clears the heap, so rebooting here would just power the
// device back up against the user's sleep gesture. Never cleared:
// startDeepSleep() does not return, so a set latch only ends at the wakeup reset.
static bool deepSleepInProgress = false;
void silentRestart() {
if (deepSleepInProgress) return; // sleeping supersedes the heap-defrag reboot
silentRebootTarget = SILENT_REBOOT_TARGET_HOME;
silentRebootMagic = SILENT_REBOOT_MAGIC;
LOG_DBG("MAIN", "Silent restart (target=home)");
@@ -143,6 +162,7 @@ void silentRestart() {
}
void silentRestartToReader() {
if (deepSleepInProgress) return; // sleeping supersedes the heap-defrag reboot
silentRebootTarget = SILENT_REBOOT_TARGET_READER;
silentRebootMagic = SILENT_REBOOT_MAGIC;
LOG_DBG("MAIN", "Silent restart (target=reader)");
@@ -150,8 +170,56 @@ 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;
}
const size_t bufferSize = renderer.getBufferSize();
const size_t written = file.write(renderer.getFrameBuffer(), bufferSize);
file.close();
if (written != bufferSize) {
LOG_DBG("MAIN", "Quick Resume frame save short write: %u/%u", static_cast<unsigned>(written),
static_cast<unsigned>(bufferSize));
Storage.remove(SLEEP_FRAME_FILE);
}
}
// 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 size_t fileSize = file.size();
const int bytesRead = file.read(display.getFrameBuffer(), bufferSize);
file.close();
Storage.remove(SLEEP_FRAME_FILE);
if (fileSize != bufferSize || bytesRead < 0 || static_cast<size_t>(bytesRead) != bufferSize) {
LOG_DBG("MAIN", "Quick Resume frame size mismatch (file=%u, read=%d, expected=%u)", static_cast<unsigned>(fileSize),
bytesRead, static_cast<unsigned>(bufferSize));
return false;
}
return true;
}
// 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 +234,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 +248,17 @@ void enterDeepSleep() {
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
}
activityManager.goToSleep();
// Commit to sleeping before goToSleep() runs the outgoing activity's onExit():
// a WiFi activity would otherwise silentRestart() here and reboot instead.
deepSleepInProgress = true;
activityManager.goToSleep(fromTimeout);
// Persist the moon-icon-overlaid framebuffer after goToSleep() has painted it.
if (isQuickResumeSleep) {
saveSleepFrameBuffer();
}
halTiltSensor.deepSleep();
display.deepSleep();
@@ -183,8 +268,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 +439,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 +456,45 @@ 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();
// Resolve the single boot-presentation decision. Skipping the splash also
// skips the panel-clearing pass and the X3 initial-full-sync arming (see
// HalDisplay::begin), so the first paint is FAST_REFRESH (~500ms) over the
// retained frame and input dispatches against a visible UI.
const BootResume resume = isSilentReboot ? BootResume::Silent
: !APP_STATE.showBootScreen ? BootResume::QuickResume
: BootResume::Splash;
if (!isSilentReboot) {
activityManager.goToBoot();
} else {
// After a silent reboot the panel still shows the previous session's pixels but
// the SDK's RED-RAM diff buffer was cleared by begin(). A FAST refresh would only
// flip pixels the SDK *thinks* changed, leaving the old screen visible. Force the
// first paint to HALF_REFRESH so the panel cleanly repaints; subsequent paints
// resume FAST as normal.
renderer.setNextDisplayRefreshMode(HalDisplay::HALF_REFRESH);
setupDisplayAndFonts(resume != BootResume::Splash);
switch (resume) {
case BootResume::Silent:
// 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);
break;
case BootResume::QuickResume:
// One-shot flag: re-arm the splash for the next non-quick-resume boot. Save
// before any painting so a hang in the blocking paint path can't strand
// us in a quick-resume-with-no-frame loop on the next boot.
APP_STATE.showBootScreen = true;
APP_STATE.saveToFile();
if (loadSleepFrameBuffer()) {
// Frame restored: swap the sleep moon for the loading icon.
const auto pageHeight = renderer.getScreenHeight();
renderer.drawImage(LoadingIcon, 0, pageHeight - LOADINGICON_HEIGHT, LOADINGICON_WIDTH, LOADINGICON_HEIGHT);
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
} else {
activityManager.goToBoot(); // frame file missing, fall back to the splash
}
break;
case BootResume::Splash:
activityManager.goToBoot();
break;
}
APP_STATE.loadFromFile();
HalClock::restore();
RECENT_BOOKS.loadFromFile();
GLOBAL_BOOKMARKS.load();
@@ -391,10 +504,10 @@ void setup() {
// Skip normal home/reader routing: jump straight into the SD firmware picker.
activityManager.replaceActivity(
std::make_unique<SdFirmwareUpdateActivity>(renderer, mappedInputManager, /*recoveryMode=*/true));
} else if (isSilentReboot && silentRebootTargetSnapshot == SILENT_REBOOT_TARGET_READER &&
} else if (resume == BootResume::Silent && silentRebootTargetSnapshot == SILENT_REBOOT_TARGET_READER &&
!APP_STATE.openEpubPath.empty()) {
activityManager.goToReader(APP_STATE.openEpubPath);
} else if (isSilentReboot) {
} else if (resume == BootResume::Silent) {
// target == home (or reader with no open book): land on home — don't fall
// through to the sleep-wake "resume reader" logic, which fires on stale
// openEpubPath + lastSleepFromReader from a prior session.
@@ -420,6 +533,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 +614,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 +628,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);