## Summary * Unset openEpubPath on boot and set once epub fully loaded ## Additional Context * If an epub was crashing when loading, it was possible to get the device stuck into a loop. There was no way to get back to the home screen as we'd always load you back into old epub * Break this loop by clearing the stored value when we boot, still jumping to the last open epub, but only resetting that value once the epub has been fully loaded
265 lines
8.4 KiB
C++
265 lines
8.4 KiB
C++
#include <Arduino.h>
|
|
#include <EInkDisplay.h>
|
|
#include <Epub.h>
|
|
#include <GfxRenderer.h>
|
|
#include <InputManager.h>
|
|
#include <SD.h>
|
|
#include <SPI.h>
|
|
#include <builtinFonts/bookerly_2b.h>
|
|
#include <builtinFonts/bookerly_bold_2b.h>
|
|
#include <builtinFonts/bookerly_bold_italic_2b.h>
|
|
#include <builtinFonts/bookerly_italic_2b.h>
|
|
#include <builtinFonts/pixelarial14.h>
|
|
#include <builtinFonts/ubuntu_10.h>
|
|
#include <builtinFonts/ubuntu_bold_10.h>
|
|
|
|
#include "Battery.h"
|
|
#include "CrossPointSettings.h"
|
|
#include "CrossPointState.h"
|
|
#include "activities/boot_sleep/BootActivity.h"
|
|
#include "activities/boot_sleep/SleepActivity.h"
|
|
#include "activities/home/HomeActivity.h"
|
|
#include "activities/network/CrossPointWebServerActivity.h"
|
|
#include "activities/reader/ReaderActivity.h"
|
|
#include "activities/settings/SettingsActivity.h"
|
|
#include "activities/util/FullScreenMessageActivity.h"
|
|
#include "config.h"
|
|
|
|
#define SPI_FQ 40000000
|
|
// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults)
|
|
#define EPD_SCLK 8 // SPI Clock
|
|
#define EPD_MOSI 10 // SPI MOSI (Master Out Slave In)
|
|
#define EPD_CS 21 // Chip Select
|
|
#define EPD_DC 4 // Data/Command
|
|
#define EPD_RST 5 // Reset
|
|
#define EPD_BUSY 6 // Busy
|
|
|
|
#define UART0_RXD 20 // Used for USB connection detection
|
|
|
|
#define SD_SPI_CS 12
|
|
#define SD_SPI_MISO 7
|
|
|
|
EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY);
|
|
InputManager inputManager;
|
|
GfxRenderer renderer(einkDisplay);
|
|
Activity* currentActivity;
|
|
|
|
// Fonts
|
|
EpdFont bookerlyFont(&bookerly_2b);
|
|
EpdFont bookerlyBoldFont(&bookerly_bold_2b);
|
|
EpdFont bookerlyItalicFont(&bookerly_italic_2b);
|
|
EpdFont bookerlyBoldItalicFont(&bookerly_bold_italic_2b);
|
|
EpdFontFamily bookerlyFontFamily(&bookerlyFont, &bookerlyBoldFont, &bookerlyItalicFont, &bookerlyBoldItalicFont);
|
|
|
|
EpdFont smallFont(&pixelarial14);
|
|
EpdFontFamily smallFontFamily(&smallFont);
|
|
|
|
EpdFont ubuntu10Font(&ubuntu_10);
|
|
EpdFont ubuntuBold10Font(&ubuntu_bold_10);
|
|
EpdFontFamily ubuntuFontFamily(&ubuntu10Font, &ubuntuBold10Font);
|
|
|
|
// Auto-sleep timeout (10 minutes of inactivity)
|
|
constexpr unsigned long AUTO_SLEEP_TIMEOUT_MS = 10 * 60 * 1000;
|
|
|
|
void exitActivity() {
|
|
if (currentActivity) {
|
|
currentActivity->onExit();
|
|
delete currentActivity;
|
|
currentActivity = nullptr;
|
|
}
|
|
}
|
|
|
|
void enterNewActivity(Activity* activity) {
|
|
currentActivity = activity;
|
|
currentActivity->onEnter();
|
|
}
|
|
|
|
// Verify long press on wake-up from deep sleep
|
|
void verifyWakeupLongPress() {
|
|
// 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;
|
|
|
|
inputManager.update();
|
|
// Verify the user has actually pressed
|
|
while (!inputManager.isPressed(InputManager::BTN_POWER) && millis() - start < 1000) {
|
|
delay(10); // only wait 10ms each iteration to not delay too much in case of short configured duration.
|
|
inputManager.update();
|
|
}
|
|
|
|
if (inputManager.isPressed(InputManager::BTN_POWER)) {
|
|
do {
|
|
delay(10);
|
|
inputManager.update();
|
|
} while (inputManager.isPressed(InputManager::BTN_POWER) &&
|
|
inputManager.getHeldTime() < SETTINGS.getPowerButtonDuration());
|
|
abort = inputManager.getHeldTime() < SETTINGS.getPowerButtonDuration();
|
|
} else {
|
|
abort = true;
|
|
}
|
|
|
|
if (abort) {
|
|
// Button released too early. Returning to sleep.
|
|
// IMPORTANT: Re-arm the wakeup trigger before sleeping again
|
|
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
|
esp_deep_sleep_start();
|
|
}
|
|
}
|
|
|
|
void waitForPowerRelease() {
|
|
inputManager.update();
|
|
while (inputManager.isPressed(InputManager::BTN_POWER)) {
|
|
delay(50);
|
|
inputManager.update();
|
|
}
|
|
}
|
|
|
|
// Enter deep sleep mode
|
|
void enterDeepSleep() {
|
|
exitActivity();
|
|
enterNewActivity(new SleepActivity(renderer, inputManager));
|
|
|
|
Serial.printf("[%lu] [ ] Entering deep sleep.\n", millis());
|
|
delay(1000); // Allow Serial buffer to empty and display to update
|
|
|
|
// Enable Wakeup on LOW (button press)
|
|
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
|
|
|
einkDisplay.deepSleep();
|
|
|
|
// Enter Deep Sleep
|
|
esp_deep_sleep_start();
|
|
}
|
|
|
|
void onGoHome();
|
|
void onGoToReader(const std::string& initialEpubPath) {
|
|
exitActivity();
|
|
enterNewActivity(new ReaderActivity(renderer, inputManager, initialEpubPath, onGoHome));
|
|
}
|
|
void onGoToReaderHome() { onGoToReader(std::string()); }
|
|
|
|
void onGoToFileTransfer() {
|
|
exitActivity();
|
|
enterNewActivity(new CrossPointWebServerActivity(renderer, inputManager, onGoHome));
|
|
}
|
|
|
|
void onGoToSettings() {
|
|
exitActivity();
|
|
enterNewActivity(new SettingsActivity(renderer, inputManager, onGoHome));
|
|
}
|
|
|
|
void onGoHome() {
|
|
exitActivity();
|
|
enterNewActivity(new HomeActivity(renderer, inputManager, onGoToReaderHome, onGoToSettings, onGoToFileTransfer));
|
|
}
|
|
|
|
void setup() {
|
|
Serial.begin(115200);
|
|
|
|
Serial.printf("[%lu] [ ] Starting CrossPoint version " CROSSPOINT_VERSION "\n", millis());
|
|
|
|
inputManager.begin();
|
|
// Initialize pins
|
|
pinMode(BAT_GPIO0, INPUT);
|
|
|
|
// Initialize SPI with custom pins
|
|
SPI.begin(EPD_SCLK, SD_SPI_MISO, EPD_MOSI, EPD_CS);
|
|
|
|
// SD Card Initialization
|
|
if (!SD.begin(SD_SPI_CS, SPI, SPI_FQ)) {
|
|
Serial.printf("[%lu] [ ] SD card initialization failed\n", millis());
|
|
exitActivity();
|
|
enterNewActivity(new FullScreenMessageActivity(renderer, inputManager, "SD card error", BOLD));
|
|
return;
|
|
}
|
|
|
|
SETTINGS.loadFromFile();
|
|
|
|
// verify power button press duration after we've read settings.
|
|
verifyWakeupLongPress();
|
|
|
|
// Initialize display
|
|
einkDisplay.begin();
|
|
Serial.printf("[%lu] [ ] Display initialized\n", millis());
|
|
|
|
renderer.insertFont(READER_FONT_ID, bookerlyFontFamily);
|
|
renderer.insertFont(UI_FONT_ID, ubuntuFontFamily);
|
|
renderer.insertFont(SMALL_FONT_ID, smallFontFamily);
|
|
Serial.printf("[%lu] [ ] Fonts setup\n", millis());
|
|
|
|
exitActivity();
|
|
enterNewActivity(new BootActivity(renderer, inputManager));
|
|
|
|
APP_STATE.loadFromFile();
|
|
if (APP_STATE.openEpubPath.empty()) {
|
|
onGoHome();
|
|
} 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.saveToFile();
|
|
onGoToReader(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;
|
|
|
|
inputManager.update();
|
|
|
|
if (Serial && millis() - lastMemPrint >= 10000) {
|
|
Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(),
|
|
ESP.getHeapSize(), ESP.getMinFreeHeap());
|
|
lastMemPrint = millis();
|
|
}
|
|
|
|
// Check for any user activity (button press or release)
|
|
static unsigned long lastActivityTime = millis();
|
|
if (inputManager.wasAnyPressed() || inputManager.wasAnyReleased()) {
|
|
lastActivityTime = millis(); // Reset inactivity timer
|
|
}
|
|
|
|
if (millis() - lastActivityTime >= AUTO_SLEEP_TIMEOUT_MS) {
|
|
Serial.printf("[%lu] [SLP] Auto-sleep triggered after %lu ms of inactivity\n", millis(), AUTO_SLEEP_TIMEOUT_MS);
|
|
enterDeepSleep();
|
|
// This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start
|
|
return;
|
|
}
|
|
|
|
if (inputManager.wasReleased(InputManager::BTN_POWER) &&
|
|
inputManager.getHeldTime() > SETTINGS.getPowerButtonDuration()) {
|
|
enterDeepSleep();
|
|
// This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start
|
|
return;
|
|
}
|
|
|
|
const unsigned long activityStartTime = millis();
|
|
if (currentActivity) {
|
|
currentActivity->loop();
|
|
}
|
|
const unsigned long activityDuration = millis() - activityStartTime;
|
|
|
|
const unsigned long loopDuration = millis() - loopStartTime;
|
|
if (loopDuration > maxLoopDuration) {
|
|
maxLoopDuration = loopDuration;
|
|
if (maxLoopDuration > 50) {
|
|
Serial.printf("[%lu] [LOOP] New max loop duration: %lu ms (activity: %lu ms)\n", millis(), 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 (currentActivity && currentActivity->skipLoopDelay()) {
|
|
yield(); // Give FreeRTOS a chance to run tasks, but return immediately
|
|
} else {
|
|
delay(10); // Normal delay when no activity requires fast response
|
|
}
|
|
}
|