2025-12-03 22:00:29 +11:00
|
|
|
#include <Arduino.h>
|
|
|
|
|
#include <EpdRenderer.h>
|
|
|
|
|
#include <Epub.h>
|
|
|
|
|
#include <GxEPD2_BW.h>
|
|
|
|
|
#include <SD.h>
|
|
|
|
|
#include <SPI.h>
|
|
|
|
|
|
|
|
|
|
#include "Battery.h"
|
2025-12-04 00:07:25 +11:00
|
|
|
#include "CrossPointState.h"
|
2025-12-03 22:00:29 +11:00
|
|
|
#include "Input.h"
|
2025-12-06 00:35:58 +11:00
|
|
|
#include "screens/BootLogoScreen.h"
|
2025-12-03 22:00:29 +11:00
|
|
|
#include "screens/EpubReaderScreen.h"
|
2025-12-04 00:07:25 +11:00
|
|
|
#include "screens/FileSelectionScreen.h"
|
2025-12-03 22:00:29 +11:00
|
|
|
#include "screens/FullScreenMessageScreen.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
|
|
|
|
|
|
|
|
|
|
GxEPD2_BW<GxEPD2_426_GDEQ0426T82, GxEPD2_426_GDEQ0426T82::HEIGHT> display(GxEPD2_426_GDEQ0426T82(EPD_CS, EPD_DC,
|
|
|
|
|
EPD_RST, EPD_BUSY));
|
|
|
|
|
auto renderer = new EpdRenderer(&display);
|
|
|
|
|
Screen* currentScreen;
|
2025-12-04 00:07:25 +11:00
|
|
|
CrossPointState* appState;
|
2025-12-03 22:00:29 +11:00
|
|
|
|
|
|
|
|
// Power button timing
|
|
|
|
|
// Time required to confirm boot from sleep
|
2025-12-04 00:57:32 +11:00
|
|
|
constexpr unsigned long POWER_BUTTON_WAKEUP_MS = 1000;
|
2025-12-03 22:00:29 +11:00
|
|
|
// Time required to enter sleep mode
|
|
|
|
|
constexpr unsigned long POWER_BUTTON_SLEEP_MS = 1000;
|
|
|
|
|
|
|
|
|
|
Epub* loadEpub(const std::string& path) {
|
|
|
|
|
if (!SD.exists(path.c_str())) {
|
|
|
|
|
Serial.println("File does not exist");
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto epub = new Epub(path, "/.crosspoint");
|
|
|
|
|
if (epub->load()) {
|
|
|
|
|
return epub;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Serial.println("Failed to load epub");
|
2025-12-06 02:49:10 +11:00
|
|
|
delete epub;
|
2025-12-03 22:00:29 +11:00
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 01:37:20 +11:00
|
|
|
void exitScreen() {
|
2025-12-03 22:00:29 +11:00
|
|
|
if (currentScreen) {
|
|
|
|
|
currentScreen->onExit();
|
|
|
|
|
delete currentScreen;
|
|
|
|
|
}
|
2025-12-06 01:37:20 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void enterNewScreen(Screen* screen) {
|
2025-12-03 22:00:29 +11:00
|
|
|
currentScreen = screen;
|
|
|
|
|
currentScreen->onEnter();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify long press on wake-up from deep sleep
|
|
|
|
|
void verifyWakeupLongPress() {
|
2025-12-04 00:57:32 +11:00
|
|
|
// Give the user up to 1000ms to start holding the power button, and must hold for POWER_BUTTON_WAKEUP_MS
|
|
|
|
|
const auto start = millis();
|
2025-12-05 17:47:23 +11:00
|
|
|
auto input = getInput(POWER_BUTTON_WAKEUP_MS);
|
2025-12-04 00:57:32 +11:00
|
|
|
while (input.button != POWER && millis() - start < 1000) {
|
|
|
|
|
delay(50);
|
2025-12-05 17:47:23 +11:00
|
|
|
input = getInput(POWER_BUTTON_WAKEUP_MS);
|
2025-12-04 00:57:32 +11:00
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-04 00:57:32 +11:00
|
|
|
if (input.button != POWER || input.pressTime < POWER_BUTTON_WAKEUP_MS) {
|
2025-12-03 22:00:29 +11:00
|
|
|
// Button released too early. Returning to sleep.
|
|
|
|
|
// IMPORTANT: Re-arm the wakeup trigger before sleeping again
|
|
|
|
|
esp_deep_sleep_enable_gpio_wakeup(1ULL << BTN_GPIO3, ESP_GPIO_WAKEUP_GPIO_LOW);
|
|
|
|
|
esp_deep_sleep_start();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-05 17:47:23 +11:00
|
|
|
void waitForNoButton() {
|
|
|
|
|
while (getInput().button != NONE) {
|
|
|
|
|
delay(50);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 22:00:29 +11:00
|
|
|
// Enter deep sleep mode
|
|
|
|
|
void enterDeepSleep() {
|
2025-12-06 01:37:20 +11:00
|
|
|
exitScreen();
|
|
|
|
|
enterNewScreen(new FullScreenMessageScreen(renderer, "Sleeping", BOLD, true, false));
|
2025-12-03 22:00:29 +11:00
|
|
|
|
|
|
|
|
Serial.println("Power button released after a long press. Entering deep sleep.");
|
2025-12-04 00:57:32 +11:00
|
|
|
delay(1000); // Allow Serial buffer to empty and display to update
|
2025-12-03 22:00:29 +11:00
|
|
|
|
|
|
|
|
// Enable Wakeup on LOW (button press)
|
|
|
|
|
esp_deep_sleep_enable_gpio_wakeup(1ULL << BTN_GPIO3, ESP_GPIO_WAKEUP_GPIO_LOW);
|
|
|
|
|
|
|
|
|
|
display.hibernate();
|
|
|
|
|
|
|
|
|
|
// Enter Deep Sleep
|
|
|
|
|
esp_deep_sleep_start();
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-04 00:07:25 +11:00
|
|
|
void onGoHome();
|
|
|
|
|
void onSelectEpubFile(const std::string& path) {
|
2025-12-06 01:37:20 +11:00
|
|
|
exitScreen();
|
2025-12-04 00:07:25 +11:00
|
|
|
enterNewScreen(new FullScreenMessageScreen(renderer, "Loading..."));
|
|
|
|
|
|
|
|
|
|
Epub* epub = loadEpub(path);
|
|
|
|
|
if (epub) {
|
|
|
|
|
appState->openEpubPath = path;
|
|
|
|
|
appState->saveToFile();
|
2025-12-06 01:37:20 +11:00
|
|
|
exitScreen();
|
2025-12-04 00:07:25 +11:00
|
|
|
enterNewScreen(new EpubReaderScreen(renderer, epub, onGoHome));
|
|
|
|
|
} else {
|
2025-12-06 01:37:20 +11:00
|
|
|
exitScreen();
|
|
|
|
|
enterNewScreen(new FullScreenMessageScreen(renderer, "Failed to load epub", REGULAR, false, false));
|
2025-12-06 02:49:10 +11:00
|
|
|
delay(2000);
|
|
|
|
|
onGoHome();
|
2025-12-04 00:07:25 +11:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-06 01:37:20 +11:00
|
|
|
|
|
|
|
|
void onGoHome() {
|
|
|
|
|
exitScreen();
|
|
|
|
|
enterNewScreen(new FileSelectionScreen(renderer, onSelectEpubFile));
|
|
|
|
|
}
|
2025-12-04 00:07:25 +11:00
|
|
|
|
2025-12-03 22:00:29 +11:00
|
|
|
void setup() {
|
|
|
|
|
setupInputPinModes();
|
2025-12-04 00:57:32 +11:00
|
|
|
verifyWakeupLongPress();
|
2025-12-05 17:47:23 +11:00
|
|
|
Serial.begin(115200);
|
|
|
|
|
Serial.println("Booting...");
|
2025-12-03 22:00:29 +11:00
|
|
|
|
|
|
|
|
// Initialize pins
|
|
|
|
|
pinMode(BAT_GPIO0, INPUT);
|
|
|
|
|
|
|
|
|
|
// Initialize SPI with custom pins
|
|
|
|
|
SPI.begin(EPD_SCLK, SD_SPI_MISO, EPD_MOSI, EPD_CS);
|
|
|
|
|
|
|
|
|
|
// Initialize display
|
|
|
|
|
const SPISettings spi_settings(SPI_FQ, MSBFIRST, SPI_MODE0);
|
|
|
|
|
display.init(115200, true, 2, false, SPI, spi_settings);
|
|
|
|
|
display.setRotation(3); // 270 degrees
|
|
|
|
|
display.setTextColor(GxEPD_BLACK);
|
|
|
|
|
Serial.println("Display initialized");
|
|
|
|
|
|
2025-12-06 01:37:20 +11:00
|
|
|
exitScreen();
|
2025-12-06 00:35:58 +11:00
|
|
|
enterNewScreen(new BootLogoScreen(renderer));
|
2025-12-03 22:00:29 +11:00
|
|
|
|
|
|
|
|
// SD Card Initialization
|
|
|
|
|
SD.begin(SD_SPI_CS, SPI, SPI_FQ);
|
|
|
|
|
|
2025-12-04 00:07:25 +11:00
|
|
|
appState = CrossPointState::loadFromFile();
|
|
|
|
|
if (!appState->openEpubPath.empty()) {
|
|
|
|
|
Epub* epub = loadEpub(appState->openEpubPath);
|
|
|
|
|
if (epub) {
|
2025-12-06 01:37:20 +11:00
|
|
|
exitScreen();
|
2025-12-04 00:07:25 +11:00
|
|
|
enterNewScreen(new EpubReaderScreen(renderer, epub, onGoHome));
|
2025-12-05 17:47:23 +11:00
|
|
|
// Ensure we're not still holding the power button before leaving setup
|
|
|
|
|
waitForNoButton();
|
2025-12-04 00:07:25 +11:00
|
|
|
return;
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 01:37:20 +11:00
|
|
|
exitScreen();
|
2025-12-04 00:07:25 +11:00
|
|
|
enterNewScreen(new FileSelectionScreen(renderer, onSelectEpubFile));
|
2025-12-05 17:47:23 +11:00
|
|
|
|
|
|
|
|
// Ensure we're not still holding the power button before leaving setup
|
|
|
|
|
waitForNoButton();
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void loop() {
|
|
|
|
|
delay(50);
|
|
|
|
|
|
|
|
|
|
const Input input = getInput();
|
|
|
|
|
|
|
|
|
|
if (input.button == NONE) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (input.button == POWER && input.pressTime > POWER_BUTTON_SLEEP_MS) {
|
|
|
|
|
enterDeepSleep();
|
|
|
|
|
// This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start
|
|
|
|
|
delay(1000);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentScreen) {
|
|
|
|
|
currentScreen->handleInput(input);
|
|
|
|
|
}
|
|
|
|
|
}
|