feat: Seamless sleep/wake screens for displaying book pages during deep sleep (#2064)
<img width="605" height="454" alt="image" src="https://github.com/user-attachments/assets/bfd84afe-3b58-436e-9a5d-539af3ec3d4e" /> 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
082b5f295b
commit
41e6e15229
+61
-11
@@ -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<Language>(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;
|
||||
|
||||
Reference in New Issue
Block a user