Add a complete SD card font subsystem that enables users to install and use custom fonts beyond the three built-in families. This combines the back-end firmware support (#1327) with the font configuration, build pipeline, CI distribution, and user-facing management UI (#1392). Core font system: - Custom .cpfont binary format (v4) with multi-style support (regular, bold, italic, bold-italic) packed into a single file per size - On-demand glyph loading from SD card with two-pass prewarm rendering to bulk-read glyphs per page, achieving near-flash performance for Latin text (~697ms vs ~681ms) and viable CJK rendering (~32% slower) - Persistent advance cache for layout measurement without SD I/O - Overflow ring buffer for glyph cache misses during rendering - Memory-conscious design: only advance tables kept in RAM; glyph bitmaps, kern tables, and ligatures loaded on demand from SD Font management: - On-device WiFi download from GitHub Releases with manifest-based discovery, install/update detection, and progress UI - Web interface font upload, listing, and deletion via /fonts page - Manual SD card copy to /fonts/ or /.fonts/ directories - Font selection integrated into Settings > Reader > Font Family Build pipeline: - Declarative YAML config (sd-fonts.yaml) as single source of truth for the 17-family font library (serif, sans, mono, accessibility) - Python converter (fontconvert_sdcard.py) for TTF/OTF to .cpfont with FreeType rasterization, class-based kerning, and ligature extraction - Parallel build orchestrator with variable font instance extraction - CI workflow publishing versioned + stable releases to a dedicated crosspoint-fonts repository with auto-incrementing revision tags - Centralized version constants (cpfont_version.py) shared across build tooling and CI, with firmware headers as manual sync points Additional fixes: - CJK characters no longer get hyphens inserted at line breaks - Advance table eliminates 30+ second stalls during CJK section indexing for paragraphs with >512 unique codepoints Closes #930 Co-authored-by: Zach Nelson <zach@zdnelson.com> Co-authored-by: Justin <itsthisjustin@users.noreply.github.com> Co-authored-by: jpirnay <jens@pirnay.com> Co-authored-by: mcrosson <kemonine@kemonine.info>
476 lines
19 KiB
C++
476 lines
19 KiB
C++
#include <Arduino.h>
|
|
#include <Epub.h>
|
|
#include <FontCacheManager.h>
|
|
#include <FontDecompressor.h>
|
|
#include <GfxRenderer.h>
|
|
#include <HalDisplay.h>
|
|
#include <HalGPIO.h>
|
|
#include <HalPowerManager.h>
|
|
#include <HalStorage.h>
|
|
#include <HalSystem.h>
|
|
#include <HalTiltSensor.h>
|
|
#include <I18n.h>
|
|
#include <Logging.h>
|
|
#include <SPI.h>
|
|
#include <builtinFonts/all.h>
|
|
|
|
#include <cstring>
|
|
|
|
#include "CrossPointSettings.h"
|
|
#include "CrossPointState.h"
|
|
#include "KOReaderCredentialStore.h"
|
|
#include "MappedInputManager.h"
|
|
#include "OpdsServerStore.h"
|
|
#include "RecentBooksStore.h"
|
|
#include "SdCardFontSystem.h"
|
|
#include "activities/Activity.h"
|
|
#include "activities/ActivityManager.h"
|
|
#include "activities/settings/SdFirmwareUpdateActivity.h"
|
|
#include "components/UITheme.h"
|
|
#include "fontIds.h"
|
|
#include "util/ButtonNavigator.h"
|
|
#include "util/ScreenshotUtil.h"
|
|
|
|
MappedInputManager mappedInputManager(gpio);
|
|
GfxRenderer renderer(display);
|
|
ActivityManager activityManager(renderer, mappedInputManager);
|
|
FontDecompressor fontDecompressor;
|
|
SdCardFontSystem sdFontSystem;
|
|
FontCacheManager fontCacheManager(renderer.getFontMap(), renderer.getSdCardFonts());
|
|
|
|
// Fonts
|
|
EpdFont notoserif14RegularFont(¬oserif_14_regular);
|
|
EpdFont notoserif14BoldFont(¬oserif_14_bold);
|
|
EpdFont notoserif14ItalicFont(¬oserif_14_italic);
|
|
EpdFont notoserif14BoldItalicFont(¬oserif_14_bolditalic);
|
|
EpdFontFamily notoserif14FontFamily(¬oserif14RegularFont, ¬oserif14BoldFont, ¬oserif14ItalicFont,
|
|
¬oserif14BoldItalicFont);
|
|
#ifndef OMIT_FONTS
|
|
EpdFont notoserif12RegularFont(¬oserif_12_regular);
|
|
EpdFont notoserif12BoldFont(¬oserif_12_bold);
|
|
EpdFont notoserif12ItalicFont(¬oserif_12_italic);
|
|
EpdFont notoserif12BoldItalicFont(¬oserif_12_bolditalic);
|
|
EpdFontFamily notoserif12FontFamily(¬oserif12RegularFont, ¬oserif12BoldFont, ¬oserif12ItalicFont,
|
|
¬oserif12BoldItalicFont);
|
|
EpdFont notoserif16RegularFont(¬oserif_16_regular);
|
|
EpdFont notoserif16BoldFont(¬oserif_16_bold);
|
|
EpdFont notoserif16ItalicFont(¬oserif_16_italic);
|
|
EpdFont notoserif16BoldItalicFont(¬oserif_16_bolditalic);
|
|
EpdFontFamily notoserif16FontFamily(¬oserif16RegularFont, ¬oserif16BoldFont, ¬oserif16ItalicFont,
|
|
¬oserif16BoldItalicFont);
|
|
EpdFont notoserif18RegularFont(¬oserif_18_regular);
|
|
EpdFont notoserif18BoldFont(¬oserif_18_bold);
|
|
EpdFont notoserif18ItalicFont(¬oserif_18_italic);
|
|
EpdFont notoserif18BoldItalicFont(¬oserif_18_bolditalic);
|
|
EpdFontFamily notoserif18FontFamily(¬oserif18RegularFont, ¬oserif18BoldFont, ¬oserif18ItalicFont,
|
|
¬oserif18BoldItalicFont);
|
|
|
|
EpdFont notosans12RegularFont(¬osans_12_regular);
|
|
EpdFont notosans12BoldFont(¬osans_12_bold);
|
|
EpdFont notosans12ItalicFont(¬osans_12_italic);
|
|
EpdFont notosans12BoldItalicFont(¬osans_12_bolditalic);
|
|
EpdFontFamily notosans12FontFamily(¬osans12RegularFont, ¬osans12BoldFont, ¬osans12ItalicFont,
|
|
¬osans12BoldItalicFont);
|
|
EpdFont notosans14RegularFont(¬osans_14_regular);
|
|
EpdFont notosans14BoldFont(¬osans_14_bold);
|
|
EpdFont notosans14ItalicFont(¬osans_14_italic);
|
|
EpdFont notosans14BoldItalicFont(¬osans_14_bolditalic);
|
|
EpdFontFamily notosans14FontFamily(¬osans14RegularFont, ¬osans14BoldFont, ¬osans14ItalicFont,
|
|
¬osans14BoldItalicFont);
|
|
EpdFont notosans16RegularFont(¬osans_16_regular);
|
|
EpdFont notosans16BoldFont(¬osans_16_bold);
|
|
EpdFont notosans16ItalicFont(¬osans_16_italic);
|
|
EpdFont notosans16BoldItalicFont(¬osans_16_bolditalic);
|
|
EpdFontFamily notosans16FontFamily(¬osans16RegularFont, ¬osans16BoldFont, ¬osans16ItalicFont,
|
|
¬osans16BoldItalicFont);
|
|
EpdFont notosans18RegularFont(¬osans_18_regular);
|
|
EpdFont notosans18BoldFont(¬osans_18_bold);
|
|
EpdFont notosans18ItalicFont(¬osans_18_italic);
|
|
EpdFont notosans18BoldItalicFont(¬osans_18_bolditalic);
|
|
EpdFontFamily notosans18FontFamily(¬osans18RegularFont, ¬osans18BoldFont, ¬osans18ItalicFont,
|
|
¬osans18BoldItalicFont);
|
|
|
|
EpdFont opendyslexic8RegularFont(&opendyslexic_8_regular);
|
|
EpdFont opendyslexic8BoldFont(&opendyslexic_8_bold);
|
|
EpdFont opendyslexic8ItalicFont(&opendyslexic_8_italic);
|
|
EpdFont opendyslexic8BoldItalicFont(&opendyslexic_8_bolditalic);
|
|
EpdFontFamily opendyslexic8FontFamily(&opendyslexic8RegularFont, &opendyslexic8BoldFont, &opendyslexic8ItalicFont,
|
|
&opendyslexic8BoldItalicFont);
|
|
EpdFont opendyslexic10RegularFont(&opendyslexic_10_regular);
|
|
EpdFont opendyslexic10BoldFont(&opendyslexic_10_bold);
|
|
EpdFont opendyslexic10ItalicFont(&opendyslexic_10_italic);
|
|
EpdFont opendyslexic10BoldItalicFont(&opendyslexic_10_bolditalic);
|
|
EpdFontFamily opendyslexic10FontFamily(&opendyslexic10RegularFont, &opendyslexic10BoldFont, &opendyslexic10ItalicFont,
|
|
&opendyslexic10BoldItalicFont);
|
|
EpdFont opendyslexic12RegularFont(&opendyslexic_12_regular);
|
|
EpdFont opendyslexic12BoldFont(&opendyslexic_12_bold);
|
|
EpdFont opendyslexic12ItalicFont(&opendyslexic_12_italic);
|
|
EpdFont opendyslexic12BoldItalicFont(&opendyslexic_12_bolditalic);
|
|
EpdFontFamily opendyslexic12FontFamily(&opendyslexic12RegularFont, &opendyslexic12BoldFont, &opendyslexic12ItalicFont,
|
|
&opendyslexic12BoldItalicFont);
|
|
EpdFont opendyslexic14RegularFont(&opendyslexic_14_regular);
|
|
EpdFont opendyslexic14BoldFont(&opendyslexic_14_bold);
|
|
EpdFont opendyslexic14ItalicFont(&opendyslexic_14_italic);
|
|
EpdFont opendyslexic14BoldItalicFont(&opendyslexic_14_bolditalic);
|
|
EpdFontFamily opendyslexic14FontFamily(&opendyslexic14RegularFont, &opendyslexic14BoldFont, &opendyslexic14ItalicFont,
|
|
&opendyslexic14BoldItalicFont);
|
|
#endif // OMIT_FONTS
|
|
|
|
EpdFont smallFont(¬osans_8_regular);
|
|
EpdFontFamily smallFontFamily(&smallFont);
|
|
|
|
EpdFont ui10RegularFont(&ubuntu_10_regular);
|
|
EpdFont ui10BoldFont(&ubuntu_10_bold);
|
|
EpdFontFamily ui10FontFamily(&ui10RegularFont, &ui10BoldFont);
|
|
|
|
EpdFont ui12RegularFont(&ubuntu_12_regular);
|
|
EpdFont ui12BoldFont(&ubuntu_12_bold);
|
|
EpdFontFamily ui12FontFamily(&ui12RegularFont, &ui12BoldFont);
|
|
|
|
// measurement of power button press duration calibration value
|
|
unsigned long t1 = 0;
|
|
unsigned long t2 = 0;
|
|
|
|
// Verify power button press duration on wake-up from deep sleep
|
|
// Pre-condition: isWakeupByPowerButton() == true
|
|
void verifyPowerButtonDuration() {
|
|
if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP) {
|
|
// Fast path for short press
|
|
// Needed because inputManager.isPressed() may take up to ~500ms to return the correct state
|
|
return;
|
|
}
|
|
|
|
// Give the user up to 1000ms to start holding the power button, and must hold for SETTINGS.getPowerButtonDuration()
|
|
const auto start = millis();
|
|
bool abort = false;
|
|
// Subtract the current time, because inputManager only starts counting the HeldTime from the first update()
|
|
// This way, we remove the time we already took to reach here from the duration,
|
|
// assuming the button was held until now from millis()==0 (i.e. device start time).
|
|
const uint16_t calibration = start;
|
|
const uint16_t calibratedPressDuration =
|
|
(calibration < SETTINGS.getPowerButtonDuration()) ? SETTINGS.getPowerButtonDuration() - calibration : 1;
|
|
|
|
gpio.update();
|
|
// Needed because inputManager.isPressed() may take up to ~500ms to return the correct state
|
|
while (!gpio.isPressed(HalGPIO::BTN_POWER) && millis() - start < 1000) {
|
|
delay(10); // only wait 10ms each iteration to not delay too much in case of short configured duration.
|
|
gpio.update();
|
|
}
|
|
|
|
t2 = millis();
|
|
if (gpio.isPressed(HalGPIO::BTN_POWER)) {
|
|
do {
|
|
delay(10);
|
|
gpio.update();
|
|
} while (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.getHeldTime() < calibratedPressDuration);
|
|
abort = gpio.getHeldTime() < calibratedPressDuration;
|
|
} else {
|
|
abort = true;
|
|
}
|
|
|
|
if (abort) {
|
|
// Button released too early. Returning to sleep.
|
|
// IMPORTANT: Re-arm the wakeup trigger before sleeping again
|
|
powerManager.startDeepSleep(gpio);
|
|
}
|
|
}
|
|
void waitForPowerRelease() {
|
|
gpio.update();
|
|
while (gpio.isPressed(HalGPIO::BTN_POWER)) {
|
|
delay(50);
|
|
gpio.update();
|
|
}
|
|
}
|
|
|
|
// Enter deep sleep mode
|
|
void enterDeepSleep() {
|
|
HalPowerManager::Lock powerLock; // Ensure we are at normal CPU frequency for sleep preparation
|
|
APP_STATE.lastSleepFromReader = activityManager.isReaderActivity();
|
|
APP_STATE.saveToFile();
|
|
|
|
activityManager.goToSleep();
|
|
|
|
halTiltSensor.deepSleep();
|
|
display.deepSleep();
|
|
LOG_DBG("MAIN", "Entering deep sleep");
|
|
|
|
powerManager.startDeepSleep(gpio);
|
|
}
|
|
|
|
void ensureSdFontLoaded() { sdFontSystem.ensureLoaded(renderer); }
|
|
|
|
void setupDisplayAndFonts() {
|
|
display.begin();
|
|
renderer.begin();
|
|
activityManager.begin();
|
|
LOG_DBG("MAIN", "Display initialized");
|
|
|
|
// Initialize font decompressor for compressed reader fonts
|
|
if (!fontDecompressor.init()) {
|
|
LOG_ERR("MAIN", "Font decompressor init failed");
|
|
}
|
|
fontCacheManager.setFontDecompressor(&fontDecompressor);
|
|
renderer.setFontCacheManager(&fontCacheManager);
|
|
renderer.insertFont(NOTOSERIF_14_FONT_ID, notoserif14FontFamily);
|
|
#ifndef OMIT_FONTS
|
|
renderer.insertFont(NOTOSERIF_12_FONT_ID, notoserif12FontFamily);
|
|
renderer.insertFont(NOTOSERIF_16_FONT_ID, notoserif16FontFamily);
|
|
renderer.insertFont(NOTOSERIF_18_FONT_ID, notoserif18FontFamily);
|
|
|
|
renderer.insertFont(NOTOSANS_12_FONT_ID, notosans12FontFamily);
|
|
renderer.insertFont(NOTOSANS_14_FONT_ID, notosans14FontFamily);
|
|
renderer.insertFont(NOTOSANS_16_FONT_ID, notosans16FontFamily);
|
|
renderer.insertFont(NOTOSANS_18_FONT_ID, notosans18FontFamily);
|
|
renderer.insertFont(OPENDYSLEXIC_8_FONT_ID, opendyslexic8FontFamily);
|
|
renderer.insertFont(OPENDYSLEXIC_10_FONT_ID, opendyslexic10FontFamily);
|
|
renderer.insertFont(OPENDYSLEXIC_12_FONT_ID, opendyslexic12FontFamily);
|
|
renderer.insertFont(OPENDYSLEXIC_14_FONT_ID, opendyslexic14FontFamily);
|
|
#endif // OMIT_FONTS
|
|
renderer.insertFont(UI_10_FONT_ID, ui10FontFamily);
|
|
renderer.insertFont(UI_12_FONT_ID, ui12FontFamily);
|
|
renderer.insertFont(SMALL_FONT_ID, smallFontFamily);
|
|
|
|
// Discover and load SD card fonts
|
|
sdFontSystem.begin(renderer);
|
|
|
|
LOG_DBG("MAIN", "Fonts setup");
|
|
}
|
|
|
|
void setup() {
|
|
t1 = millis();
|
|
|
|
HalSystem::begin();
|
|
gpio.begin();
|
|
powerManager.begin();
|
|
halTiltSensor.begin();
|
|
|
|
#ifdef ENABLE_SERIAL_LOG
|
|
if (gpio.isUsbConnected()) {
|
|
Serial.begin(115200);
|
|
const unsigned long start = millis();
|
|
while (!Serial && (millis() - start) < 500) {
|
|
delay(10);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
LOG_INF("MAIN", "Hardware detect: %s", gpio.deviceIsX3() ? "X3" : "X4");
|
|
|
|
// SD Card Initialization
|
|
// We need 6 open files concurrently when parsing a new chapter
|
|
if (!Storage.begin()) {
|
|
LOG_ERR("MAIN", "SD card initialization failed");
|
|
setupDisplayAndFonts();
|
|
activityManager.goToFullScreenMessage("SD card error", EpdFontFamily::BOLD);
|
|
return;
|
|
}
|
|
|
|
HalSystem::checkPanic();
|
|
|
|
SETTINGS.loadFromFile();
|
|
I18N.setLanguage(static_cast<Language>(SETTINGS.language));
|
|
KOREADER_STORE.loadFromFile();
|
|
OPDS_STORE.loadFromFile();
|
|
UITheme::getInstance().reload();
|
|
ButtonNavigator::setMappedInputManager(mappedInputManager);
|
|
|
|
const auto wakeupReason = gpio.getWakeupReason();
|
|
switch (wakeupReason) {
|
|
case HalGPIO::WakeupReason::PowerButton:
|
|
LOG_DBG("MAIN", "Verifying power button press duration");
|
|
gpio.verifyPowerButtonWakeup(SETTINGS.getPowerButtonDuration(),
|
|
SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP);
|
|
break;
|
|
case HalGPIO::WakeupReason::AfterUSBPower:
|
|
// If USB power caused a cold boot, go back to sleep
|
|
LOG_DBG("MAIN", "Wakeup reason: After USB Power");
|
|
powerManager.startDeepSleep(gpio);
|
|
break;
|
|
case HalGPIO::WakeupReason::AfterFlash:
|
|
// After flashing, just proceed to boot
|
|
case HalGPIO::WakeupReason::Other:
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Recovery firmware mode: hold left side button (BTN_UP) together with the power button at
|
|
// boot to skip directly to the SD-card firmware update screen. Useful on devices where USB
|
|
// flashing has been locked down (e.g. recent X3 firmware).
|
|
bool recoveryFirmwareMode = false;
|
|
if (wakeupReason == HalGPIO::WakeupReason::PowerButton) {
|
|
// Refresh the cached button state a few times — isPressed() needs ~half a second to settle
|
|
// after boot per the HalGPIO contract. Use a millis-based deadline so we always wait the full
|
|
// settle window even if the loop body takes longer than expected on slow boots.
|
|
const unsigned long settleStart = millis();
|
|
while (millis() - settleStart < 500) {
|
|
gpio.update();
|
|
delay(10);
|
|
}
|
|
if (gpio.isPressed(HalGPIO::BTN_UP)) {
|
|
recoveryFirmwareMode = true;
|
|
LOG_INF("MAIN", "Recovery firmware mode (UP + POWER held at boot)");
|
|
}
|
|
}
|
|
|
|
// First serial output only here to avoid timing inconsistencies for power button press duration verification
|
|
LOG_DBG("MAIN", "Starting CrossPoint version " CROSSPOINT_VERSION);
|
|
|
|
setupDisplayAndFonts();
|
|
|
|
activityManager.goToBoot();
|
|
|
|
APP_STATE.loadFromFile();
|
|
RECENT_BOOKS.loadFromFile();
|
|
|
|
if (recoveryFirmwareMode) {
|
|
// Skip normal home/reader routing: jump straight into the SD firmware picker.
|
|
activityManager.replaceActivity(
|
|
std::make_unique<SdFirmwareUpdateActivity>(renderer, mappedInputManager, /*recoveryMode=*/true));
|
|
} else if (HalSystem::isRebootFromPanic()) {
|
|
// If we rebooted from a panic, go to crash report screen to show the panic info
|
|
activityManager.goToCrashReport();
|
|
} else if (APP_STATE.openEpubPath.empty() || !APP_STATE.lastSleepFromReader ||
|
|
mappedInputManager.isPressed(MappedInputManager::Button::Back) || APP_STATE.readerActivityLoadCount > 0) {
|
|
// Boot to home screen if no book is open, last sleep was not from reader, back button is held, or reader activity
|
|
// crashed (indicated by readerActivityLoadCount > 0)
|
|
activityManager.goHome();
|
|
} else {
|
|
// Clear app state to avoid getting into a boot loop if the epub doesn't load
|
|
const auto path = APP_STATE.openEpubPath;
|
|
APP_STATE.openEpubPath = "";
|
|
APP_STATE.readerActivityLoadCount++;
|
|
APP_STATE.saveToFile();
|
|
activityManager.goToReader(path);
|
|
}
|
|
|
|
// Ensure we're not still holding the power button before leaving setup
|
|
waitForPowerRelease();
|
|
}
|
|
|
|
void loop() {
|
|
static unsigned long maxLoopDuration = 0;
|
|
const unsigned long loopStartTime = millis();
|
|
static unsigned long lastMemPrint = 0;
|
|
|
|
gpio.update();
|
|
halTiltSensor.update(SETTINGS.tiltPageTurn, SETTINGS.orientation, activityManager.isReaderActivity());
|
|
|
|
renderer.setFadingFix(SETTINGS.fadingFix);
|
|
|
|
if (Serial && millis() - lastMemPrint >= 10000) {
|
|
LOG_INF("MEM", "Free: %d bytes, Total: %d bytes, Min Free: %d bytes, MaxAlloc: %d bytes", ESP.getFreeHeap(),
|
|
ESP.getHeapSize(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap());
|
|
lastMemPrint = millis();
|
|
}
|
|
|
|
// Handle incoming serial commands,
|
|
// nb: we use logSerial from logging to avoid deprecation warnings
|
|
if (logSerial.available() > 0) {
|
|
String line = logSerial.readStringUntil('\n');
|
|
if (line.startsWith("CMD:")) {
|
|
String cmd = line.substring(4);
|
|
cmd.trim();
|
|
if (cmd == "SCREENSHOT") {
|
|
const uint32_t bufferSize = display.getBufferSize();
|
|
logSerial.printf("SCREENSHOT_START:%d\n", bufferSize);
|
|
uint8_t* buf = display.getFrameBuffer();
|
|
logSerial.write(buf, bufferSize);
|
|
logSerial.printf("SCREENSHOT_END\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for any user activity (button press or release) or active background work
|
|
static unsigned long lastActivityTime = millis();
|
|
if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || halTiltSensor.hadActivity() ||
|
|
activityManager.preventAutoSleep()) {
|
|
lastActivityTime = millis(); // Reset inactivity timer
|
|
powerManager.setPowerSaving(false); // Restore normal CPU frequency on user activity
|
|
}
|
|
|
|
static bool screenshotButtonsReleased = true;
|
|
static bool screenshotComboActive = false;
|
|
if (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.isPressed(HalGPIO::BTN_DOWN)) {
|
|
screenshotComboActive = true;
|
|
if (screenshotButtonsReleased) {
|
|
screenshotButtonsReleased = false;
|
|
{
|
|
RenderLock lock;
|
|
ScreenshotUtil::takeScreenshot(renderer);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if (screenshotComboActive) {
|
|
if (gpio.isPressed(HalGPIO::BTN_POWER)) return;
|
|
if (gpio.wasReleased(HalGPIO::BTN_POWER)) {
|
|
screenshotButtonsReleased = true;
|
|
screenshotComboActive = false;
|
|
return;
|
|
}
|
|
screenshotButtonsReleased = true;
|
|
screenshotComboActive = false;
|
|
}
|
|
|
|
const unsigned long sleepTimeoutMs = SETTINGS.getSleepTimeoutMs();
|
|
if (millis() - lastActivityTime >= sleepTimeoutMs) {
|
|
LOG_DBG("SLP", "Auto-sleep triggered after %lu ms of inactivity", sleepTimeoutMs);
|
|
enterDeepSleep();
|
|
// This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start
|
|
return;
|
|
}
|
|
|
|
if (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.getHeldTime() > SETTINGS.getPowerButtonDuration()) {
|
|
// If the screenshot combination is potentially being pressed, don't sleep
|
|
if (gpio.isPressed(HalGPIO::BTN_DOWN)) {
|
|
return;
|
|
}
|
|
enterDeepSleep();
|
|
// This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start
|
|
return;
|
|
}
|
|
|
|
// Refresh screen when power button is short-pressed with FORCE_REFRESH setting.
|
|
if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::FORCE_REFRESH &&
|
|
mappedInputManager.wasReleased(MappedInputManager::Button::Power)) {
|
|
LOG_DBG("MAIN", "Manual screen refresh triggered");
|
|
RenderLock lock;
|
|
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
|
}
|
|
|
|
// Refresh the battery icon when USB is plugged or unplugged.
|
|
// Placed after sleep guards so we never queue a render that won't be processed.
|
|
if (gpio.wasUsbStateChanged()) {
|
|
activityManager.requestUpdate();
|
|
}
|
|
|
|
const unsigned long activityStartTime = millis();
|
|
activityManager.loop();
|
|
const unsigned long activityDuration = millis() - activityStartTime;
|
|
|
|
const unsigned long loopDuration = millis() - loopStartTime;
|
|
if (loopDuration > maxLoopDuration) {
|
|
maxLoopDuration = loopDuration;
|
|
if (maxLoopDuration > 50) {
|
|
LOG_DBG("LOOP", "New max loop duration: %lu ms (activity: %lu ms)", maxLoopDuration, activityDuration);
|
|
}
|
|
}
|
|
|
|
// Add delay at the end of the loop to prevent tight spinning
|
|
// When an activity requests skip loop delay (e.g., webserver running), use yield() for faster response
|
|
// Otherwise, use longer delay to save power
|
|
if (activityManager.skipLoopDelay()) {
|
|
powerManager.setPowerSaving(false); // Make sure we're at full performance when skipLoopDelay is requested
|
|
yield(); // Give FreeRTOS a chance to run tasks, but return immediately
|
|
} else {
|
|
if (millis() - lastActivityTime >= HalPowerManager::IDLE_POWER_SAVING_MS) {
|
|
// If we've been inactive for a while, increase the delay to save power
|
|
powerManager.setPowerSaving(true); // Lower CPU frequency after extended inactivity
|
|
delay(50);
|
|
} else {
|
|
// Short delay to prevent tight loop while still being responsive
|
|
delay(10);
|
|
}
|
|
}
|
|
}
|