From 1ee8b728f9839a89012a52b2096372a5b5ee6ca8 Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Thu, 4 Dec 2025 00:07:25 +1100 Subject: [PATCH] Add file selection screen --- lib/EpdRenderer/EpdRenderer.cpp | 4 +- lib/EpdRenderer/EpdRenderer.h | 2 +- src/CrossPointState.cpp | 46 +++++++++++++++ src/CrossPointState.h | 14 +++++ src/main.cpp | 50 +++++++++-------- src/screens/EpubReaderScreen.cpp | 3 + src/screens/EpubReaderScreen.h | 4 +- src/screens/FileSelectionScreen.cpp | 87 +++++++++++++++++++++++++++++ src/screens/FileSelectionScreen.h | 28 ++++++++++ 9 files changed, 210 insertions(+), 28 deletions(-) create mode 100644 src/CrossPointState.cpp create mode 100644 src/CrossPointState.h create mode 100644 src/screens/FileSelectionScreen.cpp create mode 100644 src/screens/FileSelectionScreen.h diff --git a/lib/EpdRenderer/EpdRenderer.cpp b/lib/EpdRenderer/EpdRenderer.cpp index 9f42ed3..f4b9a83 100644 --- a/lib/EpdRenderer/EpdRenderer.cpp +++ b/lib/EpdRenderer/EpdRenderer.cpp @@ -57,10 +57,10 @@ void EpdRenderer::drawText(const int x, const int y, const char* text, const boo getFontRenderer(bold, italic)->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE); } -void EpdRenderer::drawSmallText(const int x, const int y, const char* text) const { +void EpdRenderer::drawSmallText(const int x, const int y, const char* text, const uint16_t color) const { int ypos = y + smallFont->font->data->advanceY + marginTop; int xpos = x + marginLeft; - smallFont->renderString(text, &xpos, &ypos, GxEPD_BLACK); + smallFont->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE); } void EpdRenderer::drawTextBox(const int x, const int y, const std::string& text, const int width, const int height, diff --git a/lib/EpdRenderer/EpdRenderer.h b/lib/EpdRenderer/EpdRenderer.h index 75119f6..0e058c6 100644 --- a/lib/EpdRenderer/EpdRenderer.h +++ b/lib/EpdRenderer/EpdRenderer.h @@ -26,7 +26,7 @@ class EpdRenderer { int getTextWidth(const char* text, bool bold = false, bool italic = false) const; int getSmallTextWidth(const char* text) const; void drawText(int x, int y, const char* text, bool bold = false, bool italic = false, uint16_t color = 1) const; - void drawSmallText(int x, int y, const char* text) const; + void drawSmallText(int x, int y, const char* text, uint16_t color = 1) const; void drawTextBox(int x, int y, const std::string& text, int width, int height, bool bold = false, bool italic = false) const; void drawLine(int x1, int y1, int x2, int y2, uint16_t color) const; diff --git a/src/CrossPointState.cpp b/src/CrossPointState.cpp new file mode 100644 index 0000000..6f523dc --- /dev/null +++ b/src/CrossPointState.cpp @@ -0,0 +1,46 @@ +#include "CrossPointState.h" + +#include +#include +#include + +#include + +constexpr uint8_t STATE_VERSION = 1; +constexpr char STATE_FILE[] = "/sd/.crosspoint/state.bin"; + +void CrossPointState::serialize(std::ostream& os) const { + serialization::writePod(os, STATE_VERSION); + serialization::writeString(os, openEpubPath); +} + +CrossPointState* CrossPointState::deserialize(std::istream& is) { + const auto state = new CrossPointState(); + + uint8_t version; + serialization::readPod(is, version); + if (version != STATE_VERSION) { + Serial.printf("CrossPointState: Unknown version %u\n", version); + return state; + } + + serialization::readString(is, state->openEpubPath); + return state; +} + +void CrossPointState::saveToFile() const { + std::ofstream outputFile(STATE_FILE); + serialize(outputFile); + outputFile.close(); +} + +CrossPointState* CrossPointState::loadFromFile() { + if (!SD.exists(&STATE_FILE[3])) { + return new CrossPointState(); + } + + std::ifstream inputFile(STATE_FILE); + CrossPointState* state = deserialize(inputFile); + inputFile.close(); + return state; +} diff --git a/src/CrossPointState.h b/src/CrossPointState.h new file mode 100644 index 0000000..dadb3f2 --- /dev/null +++ b/src/CrossPointState.h @@ -0,0 +1,14 @@ +#pragma once +#include +#include + +class CrossPointState { + void serialize(std::ostream& os) const; + static CrossPointState* deserialize(std::istream& is); + + public: + std::string openEpubPath; + ~CrossPointState() = default; + void saveToFile() const; + static CrossPointState* loadFromFile(); +}; diff --git a/src/main.cpp b/src/main.cpp index f3bdf4c..e7cdc74 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,8 +6,10 @@ #include #include "Battery.h" +#include "CrossPointState.h" #include "Input.h" #include "screens/EpubReaderScreen.h" +#include "screens/FileSelectionScreen.h" #include "screens/FullScreenMessageScreen.h" #define SPI_FQ 40000000 @@ -28,6 +30,7 @@ GxEPD2_BW display(GxEPD2 EPD_RST, EPD_BUSY)); auto renderer = new EpdRenderer(&display); Screen* currentScreen; +CrossPointState* appState; // Power button timing // Time required to confirm boot from sleep @@ -102,6 +105,21 @@ void setupSerial() { } } +void onGoHome(); +void onSelectEpubFile(const std::string& path) { + enterNewScreen(new FullScreenMessageScreen(renderer, "Loading...")); + + Epub* epub = loadEpub(path); + if (epub) { + appState->openEpubPath = path; + appState->saveToFile(); + enterNewScreen(new EpubReaderScreen(renderer, epub, onGoHome)); + } else { + enterNewScreen(new FullScreenMessageScreen(renderer, "Failed to load epub")); + } +} +void onGoHome() { enterNewScreen(new FileSelectionScreen(renderer, onSelectEpubFile)); } + void setup() { setupInputPinModes(); @@ -127,37 +145,21 @@ void setup() { display.setTextColor(GxEPD_BLACK); Serial.println("Display initialized"); - enterNewScreen(new FullScreenMessageScreen(renderer, "Loading...", true)); + enterNewScreen(new FullScreenMessageScreen(renderer, "Booting...", true)); // SD Card Initialization SD.begin(SD_SPI_CS, SPI, SPI_FQ); - // TODO: Add a file selection screen, for now just load the first file - File root = SD.open("/"); - String filename; - while (true) { - filename = root.getNextFileName(); - if (!filename) { - break; - } - - if (filename.substring(filename.length() - 5) == ".epub") { - Serial.printf("Found epub: %s\n", filename.c_str()); - break; + appState = CrossPointState::loadFromFile(); + if (!appState->openEpubPath.empty()) { + Epub* epub = loadEpub(appState->openEpubPath); + if (epub) { + enterNewScreen(new EpubReaderScreen(renderer, epub, onGoHome)); + return; } } - if (!filename) { - enterNewScreen(new FullScreenMessageScreen(renderer, "Could not find epub")); - return; - } - - Epub* epub = loadEpub(std::string(filename.c_str())); - if (epub) { - enterNewScreen(new EpubReaderScreen(renderer, epub)); - } else { - enterNewScreen(new FullScreenMessageScreen(renderer, "Failed to load epub")); - } + enterNewScreen(new FileSelectionScreen(renderer, onSelectEpubFile)); } void loop() { diff --git a/src/screens/EpubReaderScreen.cpp b/src/screens/EpubReaderScreen.cpp index 75ec42c..8bb3dd7 100644 --- a/src/screens/EpubReaderScreen.cpp +++ b/src/screens/EpubReaderScreen.cpp @@ -41,6 +41,7 @@ void EpubReaderScreen::onEnter() { void EpubReaderScreen::onExit() { vTaskDelete(displayTaskHandle); + displayTaskHandle = nullptr; xSemaphoreTake(sectionMutex, portMAX_DELAY); vSemaphoreDelete(sectionMutex); sectionMutex = nullptr; @@ -91,6 +92,8 @@ void EpubReaderScreen::handleInput(const Input input) { } updateRequired = true; + } else if (input.button == BACK) { + onGoHome(); } } diff --git a/src/screens/EpubReaderScreen.h b/src/screens/EpubReaderScreen.h index a8ee6c9..b67b7aa 100644 --- a/src/screens/EpubReaderScreen.h +++ b/src/screens/EpubReaderScreen.h @@ -15,6 +15,7 @@ class EpubReaderScreen final : public Screen { int currentSpineIndex = 0; int nextPageNumber = 0; bool updateRequired = false; + const std::function onGoHome; static void taskTrampoline(void* param); [[noreturn]] void displayTaskLoop(); @@ -22,7 +23,8 @@ class EpubReaderScreen final : public Screen { void renderStatusBar() const; public: - explicit EpubReaderScreen(EpdRenderer* renderer, Epub* epub) : Screen(renderer), epub(epub) {} + explicit EpubReaderScreen(EpdRenderer* renderer, Epub* epub, const std::function& onGoHome) + : Screen(renderer), epub(epub), onGoHome(onGoHome) {} ~EpubReaderScreen() override { free(section); } void onEnter() override; void onExit() override; diff --git a/src/screens/FileSelectionScreen.cpp b/src/screens/FileSelectionScreen.cpp new file mode 100644 index 0000000..0c182cc --- /dev/null +++ b/src/screens/FileSelectionScreen.cpp @@ -0,0 +1,87 @@ +#include "FileSelectionScreen.h" + +#include +#include + +void FileSelectionScreen::taskTrampoline(void* param) { + auto* self = static_cast(param); + self->displayTaskLoop(); +} + +void FileSelectionScreen::onEnter() { + files.clear(); + auto root = SD.open("/"); + File file; + while ((file = root.openNextFile())) { + if (file.isDirectory()) { + file.close(); + continue; + } + + auto filename = std::string(file.name()); + if (filename.substr(filename.length() - 5) != ".epub" || filename[0] == '.') { + file.close(); + continue; + } + + files.emplace_back(filename); + file.close(); + } + root.close(); + + // Trigger first update + updateRequired = true; + + xTaskCreate(&FileSelectionScreen::taskTrampoline, "FileSelectionScreenTask", + 1024, // Stack size + this, // Parameters + 1, // Priority + &displayTaskHandle // Task handle + ); +} + +void FileSelectionScreen::onExit() { + vTaskDelete(displayTaskHandle); + displayTaskHandle = nullptr; +} + +void FileSelectionScreen::handleInput(const Input input) { + if (input.button == VOLUME_DOWN) { + selectorIndex = (selectorIndex + 1) % files.size(); + updateRequired = true; + } else if (input.button == VOLUME_UP) { + selectorIndex = (selectorIndex + files.size() - 1) % files.size(); + updateRequired = true; + } else if (input.button == CONFIRM) { + Serial.printf("Selected file: %s\n", files[selectorIndex].c_str()); + onSelect("/" + files[selectorIndex]); + } +} + +void FileSelectionScreen::displayTaskLoop() { + while (true) { + if (updateRequired) { + updateRequired = false; + render(); + } + vTaskDelay(10 / portTICK_PERIOD_MS); + } +} + +void FileSelectionScreen::render() const { + renderer->clearScreen(); + + const auto pageWidth = renderer->getPageWidth(); + const auto titleWidth = renderer->getTextWidth("CrossPoint Reader", true); + renderer->drawText((pageWidth - titleWidth) / 2, 0, "CrossPoint Reader", true); + + // Draw selection + renderer->fillRect(0, 50 + selectorIndex * 20 + 2, pageWidth - 1, 20, 1); + + for (size_t i = 0; i < files.size(); i++) { + const auto file = files[i]; + renderer->drawSmallText(50, 50 + i * 20, file.c_str(), i == selectorIndex ? 0 : 1); + } + + renderer->flushDisplay(); +} diff --git a/src/screens/FileSelectionScreen.h b/src/screens/FileSelectionScreen.h new file mode 100644 index 0000000..80c5974 --- /dev/null +++ b/src/screens/FileSelectionScreen.h @@ -0,0 +1,28 @@ +#pragma once +#include +#include + +#include +#include +#include + +#include "Screen.h" + +class FileSelectionScreen final : public Screen { + TaskHandle_t displayTaskHandle = nullptr; + std::vector files; + int selectorIndex = 0; + bool updateRequired = false; + const std::function onSelect; + + static void taskTrampoline(void* param); + [[noreturn]] void displayTaskLoop(); + void render() const; + + public: + explicit FileSelectionScreen(EpdRenderer* renderer, const std::function& onSelect) + : Screen(renderer), onSelect(onSelect) {} + void onEnter() override; + void onExit() override; + void handleInput(Input input) override; +};