Merge master

This commit is contained in:
Brendan O'Leary 2025-12-17 18:44:05 -05:00
commit 08227a07fa
38 changed files with 536 additions and 287 deletions

View File

@ -73,10 +73,8 @@ bool Epub::parseContentOpf(const std::string& contentOpfFilePath) {
coverImageItem = opfParser.items.at(opfParser.coverItemId); coverImageItem = opfParser.items.at(opfParser.coverItemId);
} }
if (opfParser.items.count("ncx")) { if (!opfParser.tocNcxPath.empty()) {
tocNcxItem = opfParser.items.at("ncx"); tocNcxItem = opfParser.tocNcxPath;
} else if (opfParser.items.count("ncxtoc")) {
tocNcxItem = opfParser.items.at("ncxtoc");
} }
for (auto& spineRef : opfParser.spineRefs) { for (auto& spineRef : opfParser.spineRefs) {
@ -150,6 +148,18 @@ bool Epub::load() {
return false; return false;
} }
// determine size of spine items
size_t spineItemsCount = getSpineItemsCount();
size_t spineItemsSize = 0;
for (size_t i = 0; i < spineItemsCount; i++) {
std::string spineItem = getSpineItem(i);
size_t s = 0;
getItemSize(spineItem, &s);
spineItemsSize += s;
cumulativeSpineItemSize.emplace_back(spineItemsSize);
}
Serial.printf("[%lu] [EBP] Book size: %u\n", millis(), spineItemsSize);
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str()); Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
return true; return true;
@ -257,6 +267,8 @@ bool Epub::getItemSize(const std::string& itemHref, size_t* size) const {
int Epub::getSpineItemsCount() const { return spine.size(); } int Epub::getSpineItemsCount() const { return spine.size(); }
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return cumulativeSpineItemSize.at(spineIndex); }
std::string& Epub::getSpineItem(const int spineIndex) { std::string& Epub::getSpineItem(const int spineIndex) {
if (spineIndex < 0 || spineIndex >= spine.size()) { if (spineIndex < 0 || spineIndex >= spine.size()) {
Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex); Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex);
@ -304,3 +316,14 @@ int Epub::getTocIndexForSpineIndex(const int spineIndex) const {
Serial.printf("[%lu] [EBP] TOC item not found\n", millis()); Serial.printf("[%lu] [EBP] TOC item not found\n", millis());
return -1; return -1;
} }
size_t Epub::getBookSize() const { return getCumulativeSpineItemSize(getSpineItemsCount() - 1); }
// Calculate progress in book
uint8_t Epub::calculateProgress(const int currentSpineIndex, const float currentSpineRead) {
size_t prevChapterSize = (currentSpineIndex >= 1) ? getCumulativeSpineItemSize(currentSpineIndex - 1) : 0;
size_t curChapterSize = getCumulativeSpineItemSize(currentSpineIndex) - prevChapterSize;
size_t bookSize = getBookSize();
size_t sectionProgSize = currentSpineRead * curChapterSize;
return round(static_cast<float>(prevChapterSize + sectionProgSize) / bookSize * 100.0);
}

View File

@ -20,6 +20,8 @@ class Epub {
std::string filepath; std::string filepath;
// the spine of the EPUB file // the spine of the EPUB file
std::vector<std::pair<std::string, std::string>> spine; std::vector<std::pair<std::string, std::string>> spine;
// the file size of the spine items (proxy to book progress)
std::vector<size_t> cumulativeSpineItemSize;
// the toc of the EPUB file // the toc of the EPUB file
std::vector<EpubTocEntry> toc; std::vector<EpubTocEntry> toc;
// the base path for items in the EPUB file // the base path for items in the EPUB file
@ -51,8 +53,12 @@ class Epub {
bool getItemSize(const std::string& itemHref, size_t* size) const; bool getItemSize(const std::string& itemHref, size_t* size) const;
std::string& getSpineItem(int spineIndex); std::string& getSpineItem(int spineIndex);
int getSpineItemsCount() const; int getSpineItemsCount() const;
EpubTocEntry& getTocItem(int tocTndex); size_t getCumulativeSpineItemSize(const int spineIndex) const;
EpubTocEntry& getTocItem(int tocIndex);
int getTocItemsCount() const; int getTocItemsCount() const;
int getSpineIndexForTocIndex(int tocIndex) const; int getSpineIndexForTocIndex(int tocIndex) const;
int getTocIndexForSpineIndex(int spineIndex) const; int getTocIndexForSpineIndex(int spineIndex) const;
size_t getBookSize() const;
uint8_t calculateProgress(const int currentSpineIndex, const float currentSpineRead);
}; };

View File

@ -3,7 +3,9 @@
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <Serialization.h> #include <Serialization.h>
namespace {
constexpr uint8_t PAGE_FILE_VERSION = 3; constexpr uint8_t PAGE_FILE_VERSION = 3;
}
void PageLine::render(GfxRenderer& renderer, const int fontId) { block->render(renderer, fontId, xPos, yPos); } void PageLine::render(GfxRenderer& renderer, const int fontId) { block->render(renderer, fontId, xPos, yPos); }

View File

@ -9,7 +9,9 @@
#include "Page.h" #include "Page.h"
#include "parsers/ChapterHtmlSlimParser.h" #include "parsers/ChapterHtmlSlimParser.h"
namespace {
constexpr uint8_t SECTION_FILE_VERSION = 5; constexpr uint8_t SECTION_FILE_VERSION = 5;
}
void Section::onPageComplete(std::unique_ptr<Page> page) { void Section::onPageComplete(std::unique_ptr<Page> page) {
const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin"; const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin";

View File

@ -3,6 +3,10 @@
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <ZipFile.h> #include <ZipFile.h>
namespace {
constexpr const char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml";
}
bool ContentOpfParser::setup() { bool ContentOpfParser::setup() {
parser = XML_ParserCreate(nullptr); parser = XML_ParserCreate(nullptr);
if (!parser) { if (!parser) {
@ -111,16 +115,28 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
if (self->state == IN_MANIFEST && (strcmp(name, "item") == 0 || strcmp(name, "opf:item") == 0)) { if (self->state == IN_MANIFEST && (strcmp(name, "item") == 0 || strcmp(name, "opf:item") == 0)) {
std::string itemId; std::string itemId;
std::string href; std::string href;
std::string mediaType;
for (int i = 0; atts[i]; i += 2) { for (int i = 0; atts[i]; i += 2) {
if (strcmp(atts[i], "id") == 0) { if (strcmp(atts[i], "id") == 0) {
itemId = atts[i + 1]; itemId = atts[i + 1];
} else if (strcmp(atts[i], "href") == 0) { } else if (strcmp(atts[i], "href") == 0) {
href = self->baseContentPath + atts[i + 1]; href = self->baseContentPath + atts[i + 1];
} else if (strcmp(atts[i], "media-type") == 0) {
mediaType = atts[i + 1];
} }
} }
self->items[itemId] = href; self->items[itemId] = href;
if (mediaType == MEDIA_TYPE_NCX) {
if (self->tocNcxPath.empty()) {
self->tocNcxPath = href;
} else {
Serial.printf("[%lu] [COF] Warning: Multiple NCX files found in manifest. Ignoring duplicate: %s\n", millis(),
href.c_str());
}
}
return; return;
} }

View File

@ -10,9 +10,11 @@
// Initialize the static instance // Initialize the static instance
CrossPointSettings CrossPointSettings::instance; CrossPointSettings CrossPointSettings::instance;
namespace {
constexpr uint8_t SETTINGS_FILE_VERSION = 1; constexpr uint8_t SETTINGS_FILE_VERSION = 1;
constexpr uint8_t SETTINGS_COUNT = 2; constexpr uint8_t SETTINGS_COUNT = 2;
constexpr char SETTINGS_FILE[] = "/sd/.crosspoint/settings.bin"; constexpr char SETTINGS_FILE[] = "/sd/.crosspoint/settings.bin";
} // namespace
bool CrossPointSettings::saveToFile() const { bool CrossPointSettings::saveToFile() const {
// Make sure the directory exists // Make sure the directory exists

View File

@ -6,8 +6,12 @@
#include <fstream> #include <fstream>
namespace {
constexpr uint8_t STATE_FILE_VERSION = 1; constexpr uint8_t STATE_FILE_VERSION = 1;
constexpr char STATE_FILE[] = "/sd/.crosspoint/state.bin"; constexpr char STATE_FILE[] = "/sd/.crosspoint/state.bin";
} // namespace
CrossPointState CrossPointState::instance;
bool CrossPointState::saveToFile() const { bool CrossPointState::saveToFile() const {
std::ofstream outputFile(STATE_FILE); std::ofstream outputFile(STATE_FILE);

View File

@ -3,11 +3,20 @@
#include <string> #include <string>
class CrossPointState { class CrossPointState {
// Static instance
static CrossPointState instance;
public: public:
std::string openEpubPath; std::string openEpubPath;
~CrossPointState() = default; ~CrossPointState() = default;
// Get singleton instance
static CrossPointState& getInstance() { return instance; }
bool saveToFile() const; bool saveToFile() const;
bool loadFromFile(); bool loadFromFile();
}; };
// Helper macro to access settings
#define APP_STATE CrossPointState::getInstance()

18
src/activities/Activity.h Normal file
View File

@ -0,0 +1,18 @@
#pragma once
#include <InputManager.h>
class GfxRenderer;
class Activity {
protected:
GfxRenderer& renderer;
InputManager& inputManager;
public:
explicit Activity(GfxRenderer& renderer, InputManager& inputManager)
: renderer(renderer), inputManager(inputManager) {}
virtual ~Activity() = default;
virtual void onEnter() {}
virtual void onExit() {}
virtual void loop() {}
};

View File

@ -0,0 +1,21 @@
#include "ActivityWithSubactivity.h"
void ActivityWithSubactivity::exitActivity() {
if (subActivity) {
subActivity->onExit();
subActivity.reset();
}
}
void ActivityWithSubactivity::enterNewActivity(Activity* activity) {
subActivity.reset(activity);
subActivity->onEnter();
}
void ActivityWithSubactivity::loop() {
if (subActivity) {
subActivity->loop();
}
}
void ActivityWithSubactivity::onExit() { exitActivity(); }

View File

@ -0,0 +1,17 @@
#pragma once
#include <memory>
#include "Activity.h"
class ActivityWithSubactivity : public Activity {
protected:
std::unique_ptr<Activity> subActivity = nullptr;
void exitActivity();
void enterNewActivity(Activity* activity);
public:
explicit ActivityWithSubactivity(GfxRenderer& renderer, InputManager& inputManager)
: Activity(renderer, inputManager) {}
void loop() override;
void onExit() override;
};

View File

@ -1,11 +1,11 @@
#include "BootLogoScreen.h" #include "BootActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include "config.h" #include "config.h"
#include "images/CrossLarge.h" #include "images/CrossLarge.h"
void BootLogoScreen::onEnter() { void BootActivity::onEnter() {
const auto pageWidth = GfxRenderer::getScreenWidth(); const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight(); const auto pageHeight = GfxRenderer::getScreenHeight();

View File

@ -0,0 +1,8 @@
#pragma once
#include "../Activity.h"
class BootActivity final : public Activity {
public:
explicit BootActivity(GfxRenderer& renderer, InputManager& inputManager) : Activity(renderer, inputManager) {}
void onEnter() override;
};

View File

@ -1,4 +1,4 @@
#include "SleepScreen.h" #include "SleepActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
@ -6,7 +6,7 @@
#include "config.h" #include "config.h"
#include "images/CrossLarge.h" #include "images/CrossLarge.h"
void SleepScreen::onEnter() { void SleepActivity::onEnter() {
const auto pageWidth = GfxRenderer::getScreenWidth(); const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight(); const auto pageHeight = GfxRenderer::getScreenHeight();

View File

@ -0,0 +1,8 @@
#pragma once
#include "../Activity.h"
class SleepActivity final : public Activity {
public:
explicit SleepActivity(GfxRenderer& renderer, InputManager& inputManager) : Activity(renderer, inputManager) {}
void onEnter() override;
};

View File

@ -0,0 +1,103 @@
#include "HomeActivity.h"
#include <GfxRenderer.h>
#include <SD.h>
#include "config.h"
namespace {
constexpr int menuItemCount = 2;
}
void HomeActivity::taskTrampoline(void* param) {
auto* self = static_cast<HomeActivity*>(param);
self->displayTaskLoop();
}
void HomeActivity::onEnter() {
renderingMutex = xSemaphoreCreateMutex();
selectorIndex = 0;
// Trigger first update
updateRequired = true;
xTaskCreate(&HomeActivity::taskTrampoline, "HomeActivityTask",
2048, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
}
void HomeActivity::onExit() {
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void HomeActivity::loop() {
const bool prevPressed =
inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT);
const bool nextPressed =
inputManager.wasPressed(InputManager::BTN_DOWN) || inputManager.wasPressed(InputManager::BTN_RIGHT);
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
if (selectorIndex == 0) {
onReaderOpen();
} else if (selectorIndex == 1) {
onSettingsOpen();
}
} else if (prevPressed) {
selectorIndex = (selectorIndex + menuItemCount - 1) % menuItemCount;
updateRequired = true;
} else if (nextPressed) {
selectorIndex = (selectorIndex + 1) % menuItemCount;
updateRequired = true;
}
}
void HomeActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void HomeActivity::render() const {
renderer.clearScreen();
const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight();
renderer.drawCenteredText(READER_FONT_ID, 10, "CrossPoint Reader", true, BOLD);
// Draw selection
renderer.fillRect(0, 60 + selectorIndex * 30 + 2, pageWidth - 1, 30);
renderer.drawText(UI_FONT_ID, 20, 60, "Read", selectorIndex != 0);
renderer.drawText(UI_FONT_ID, 20, 90, "Settings", selectorIndex != 1);
renderer.drawRect(25, pageHeight - 40, 106, 40);
renderer.drawText(UI_FONT_ID, 25 + (105 - renderer.getTextWidth(UI_FONT_ID, "Back")) / 2, pageHeight - 35, "Back");
renderer.drawRect(130, pageHeight - 40, 106, 40);
renderer.drawText(UI_FONT_ID, 130 + (105 - renderer.getTextWidth(UI_FONT_ID, "Confirm")) / 2, pageHeight - 35,
"Confirm");
renderer.drawRect(245, pageHeight - 40, 106, 40);
renderer.drawText(UI_FONT_ID, 245 + (105 - renderer.getTextWidth(UI_FONT_ID, "Left")) / 2, pageHeight - 35, "Left");
renderer.drawRect(350, pageHeight - 40, 106, 40);
renderer.drawText(UI_FONT_ID, 350 + (105 - renderer.getTextWidth(UI_FONT_ID, "Right")) / 2, pageHeight - 35, "Right");
renderer.displayBuffer();
}

View File

@ -0,0 +1,29 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
#include "../Activity.h"
class HomeActivity final : public Activity {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
int selectorIndex = 0;
bool updateRequired = false;
const std::function<void()> onReaderOpen;
const std::function<void()> onSettingsOpen;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render() const;
public:
explicit HomeActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onReaderOpen,
const std::function<void()>& onSettingsOpen)
: Activity(renderer, inputManager), onReaderOpen(onReaderOpen), onSettingsOpen(onSettingsOpen) {}
void onEnter() override;
void onExit() override;
void loop() override;
};

View File

@ -227,7 +227,7 @@ void WifiScreen::checkConnectionStatus() {
} }
} }
void WifiScreen::handleInput() { void WifiScreen::loop() {
// Check scan progress // Check scan progress
if (state == WifiScreenState::SCANNING) { if (state == WifiScreenState::SCANNING) {
processWifiScanResults(); processWifiScanResults();

View File

@ -10,7 +10,7 @@
#include <vector> #include <vector>
#include "OnScreenKeyboard.h" #include "OnScreenKeyboard.h"
#include "Screen.h" #include "../Activity.h"
// Structure to hold WiFi network information // Structure to hold WiFi network information
struct WifiNetworkInfo { struct WifiNetworkInfo {
@ -32,7 +32,7 @@ enum class WifiScreenState {
FORGET_PROMPT // Asking user if they want to forget the network FORGET_PROMPT // Asking user if they want to forget the network
}; };
class WifiScreen final : public Screen { class WifiScreen final : public Activity {
TaskHandle_t displayTaskHandle = nullptr; TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr; SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false; bool updateRequired = false;
@ -86,8 +86,8 @@ class WifiScreen final : public Screen {
public: public:
explicit WifiScreen(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoBack) explicit WifiScreen(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoBack)
: Screen(renderer, inputManager), onGoBack(onGoBack) {} : Activity(renderer, inputManager), onGoBack(onGoBack) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void handleInput() override; void loop() override;
}; };

View File

@ -1,4 +1,4 @@
#include "EpubReaderScreen.h" #include "EpubReaderActivity.h"
#include <Epub/Page.h> #include <Epub/Page.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
@ -6,23 +6,25 @@
#include "Battery.h" #include "Battery.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "EpubReaderChapterSelectionScreen.h" #include "EpubReaderChapterSelectionActivity.h"
#include "config.h" #include "config.h"
constexpr int PAGES_PER_REFRESH = 15; namespace {
constexpr unsigned long SKIP_CHAPTER_MS = 700; constexpr int pagesPerRefresh = 15;
constexpr unsigned long skipChapterMs = 700;
constexpr float lineCompression = 0.95f; constexpr float lineCompression = 0.95f;
constexpr int marginTop = 8; constexpr int marginTop = 8;
constexpr int marginRight = 10; constexpr int marginRight = 10;
constexpr int marginBottom = 22; constexpr int marginBottom = 22;
constexpr int marginLeft = 10; constexpr int marginLeft = 10;
} // namespace
void EpubReaderScreen::taskTrampoline(void* param) { void EpubReaderActivity::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderScreen*>(param); auto* self = static_cast<EpubReaderActivity*>(param);
self->displayTaskLoop(); self->displayTaskLoop();
} }
void EpubReaderScreen::onEnter() { void EpubReaderActivity::onEnter() {
if (!epub) { if (!epub) {
return; return;
} }
@ -44,7 +46,7 @@ void EpubReaderScreen::onEnter() {
// Trigger first update // Trigger first update
updateRequired = true; updateRequired = true;
xTaskCreate(&EpubReaderScreen::taskTrampoline, "EpubReaderScreenTask", xTaskCreate(&EpubReaderActivity::taskTrampoline, "EpubReaderActivityTask",
8192, // Stack size 8192, // Stack size
this, // Parameters this, // Parameters
1, // Priority 1, // Priority
@ -52,7 +54,7 @@ void EpubReaderScreen::onEnter() {
); );
} }
void EpubReaderScreen::onExit() { void EpubReaderActivity::onExit() {
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD // Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) { if (displayTaskHandle) {
@ -65,22 +67,22 @@ void EpubReaderScreen::onExit() {
epub.reset(); epub.reset();
} }
void EpubReaderScreen::handleInput() { void EpubReaderActivity::loop() {
// Pass input responsibility to sub screen if exists // Pass input responsibility to sub activity if exists
if (subScreen) { if (subAcitivity) {
subScreen->handleInput(); subAcitivity->loop();
return; return;
} }
// Enter chapter selection screen // Enter chapter selection activity
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) { if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
// Don't start screen transition while rendering // Don't start activity transition while rendering
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
subScreen.reset(new EpubReaderChapterSelectionScreen( subAcitivity.reset(new EpubReaderChapterSelectionActivity(
this->renderer, this->inputManager, epub, currentSpineIndex, this->renderer, this->inputManager, epub, currentSpineIndex,
[this] { [this] {
subScreen->onExit(); subAcitivity->onExit();
subScreen.reset(); subAcitivity.reset();
updateRequired = true; updateRequired = true;
}, },
[this](const int newSpineIndex) { [this](const int newSpineIndex) {
@ -89,16 +91,16 @@ void EpubReaderScreen::handleInput() {
nextPageNumber = 0; nextPageNumber = 0;
section.reset(); section.reset();
} }
subScreen->onExit(); subAcitivity->onExit();
subScreen.reset(); subAcitivity.reset();
updateRequired = true; updateRequired = true;
})); }));
subScreen->onEnter(); subAcitivity->onEnter();
xSemaphoreGive(renderingMutex); xSemaphoreGive(renderingMutex);
} }
if (inputManager.wasPressed(InputManager::BTN_BACK)) { if (inputManager.wasPressed(InputManager::BTN_BACK)) {
onGoHome(); onGoBack();
return; return;
} }
@ -119,7 +121,7 @@ void EpubReaderScreen::handleInput() {
return; return;
} }
const bool skipChapter = inputManager.getHeldTime() > SKIP_CHAPTER_MS; const bool skipChapter = inputManager.getHeldTime() > skipChapterMs;
if (skipChapter) { if (skipChapter) {
// We don't want to delete the section mid-render, so grab the semaphore // We don't want to delete the section mid-render, so grab the semaphore
@ -165,7 +167,7 @@ void EpubReaderScreen::handleInput() {
} }
} }
void EpubReaderScreen::displayTaskLoop() { void EpubReaderActivity::displayTaskLoop() {
while (true) { while (true) {
if (updateRequired) { if (updateRequired) {
updateRequired = false; updateRequired = false;
@ -178,7 +180,7 @@ void EpubReaderScreen::displayTaskLoop() {
} }
// TODO: Failure handling // TODO: Failure handling
void EpubReaderScreen::renderScreen() { void EpubReaderActivity::renderScreen() {
if (!epub) { if (!epub) {
return; return;
} }
@ -285,12 +287,12 @@ void EpubReaderScreen::renderScreen() {
f.close(); f.close();
} }
void EpubReaderScreen::renderContents(std::unique_ptr<Page> page) { void EpubReaderActivity::renderContents(std::unique_ptr<Page> page) {
page->render(renderer, READER_FONT_ID); page->render(renderer, READER_FONT_ID);
renderStatusBar(); renderStatusBar();
if (pagesUntilFullRefresh <= 1) { if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(EInkDisplay::HALF_REFRESH); renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
pagesUntilFullRefresh = PAGES_PER_REFRESH; pagesUntilFullRefresh = pagesPerRefresh;
} else { } else {
renderer.displayBuffer(); renderer.displayBuffer();
pagesUntilFullRefresh--; pagesUntilFullRefresh--;
@ -322,10 +324,16 @@ void EpubReaderScreen::renderContents(std::unique_ptr<Page> page) {
renderer.restoreBwBuffer(); renderer.restoreBwBuffer();
} }
void EpubReaderScreen::renderStatusBar() const { void EpubReaderActivity::renderStatusBar() const {
constexpr auto textY = 776; constexpr auto textY = 776;
// Calculate progress in book
float sectionChapterProg = static_cast<float>(section->currentPage) / section->pageCount;
uint8_t bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg);
// Right aligned text for progress counter // Right aligned text for progress counter
const std::string progress = std::to_string(section->currentPage + 1) + " / " + std::to_string(section->pageCount); const std::string progress = std::to_string(section->currentPage + 1) + "/" + std::to_string(section->pageCount) +
" " + std::to_string(bookProgress) + "%";
const auto progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progress.c_str()); const auto progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progress.c_str());
renderer.drawText(SMALL_FONT_ID, GfxRenderer::getScreenWidth() - marginRight - progressTextWidth, textY, renderer.drawText(SMALL_FONT_ID, GfxRenderer::getScreenWidth() - marginRight - progressTextWidth, textY,
progress.c_str()); progress.c_str());

View File

@ -5,19 +5,19 @@
#include <freertos/semphr.h> #include <freertos/semphr.h>
#include <freertos/task.h> #include <freertos/task.h>
#include "Screen.h" #include "../Activity.h"
class EpubReaderScreen final : public Screen { class EpubReaderActivity final : public Activity {
std::shared_ptr<Epub> epub; std::shared_ptr<Epub> epub;
std::unique_ptr<Section> section = nullptr; std::unique_ptr<Section> section = nullptr;
TaskHandle_t displayTaskHandle = nullptr; TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr; SemaphoreHandle_t renderingMutex = nullptr;
std::unique_ptr<Screen> subScreen = nullptr; std::unique_ptr<Activity> subAcitivity = nullptr;
int currentSpineIndex = 0; int currentSpineIndex = 0;
int nextPageNumber = 0; int nextPageNumber = 0;
int pagesUntilFullRefresh = 0; int pagesUntilFullRefresh = 0;
bool updateRequired = false; bool updateRequired = false;
const std::function<void()> onGoHome; const std::function<void()> onGoBack;
static void taskTrampoline(void* param); static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop(); [[noreturn]] void displayTaskLoop();
@ -26,10 +26,10 @@ class EpubReaderScreen final : public Screen {
void renderStatusBar() const; void renderStatusBar() const;
public: public:
explicit EpubReaderScreen(GfxRenderer& renderer, InputManager& inputManager, std::unique_ptr<Epub> epub, explicit EpubReaderActivity(GfxRenderer& renderer, InputManager& inputManager, std::unique_ptr<Epub> epub,
const std::function<void()>& onGoHome) const std::function<void()>& onGoBack)
: Screen(renderer, inputManager), epub(std::move(epub)), onGoHome(onGoHome) {} : Activity(renderer, inputManager), epub(std::move(epub)), onGoBack(onGoBack) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void handleInput() override; void loop() override;
}; };

View File

@ -1,4 +1,4 @@
#include "EpubReaderChapterSelectionScreen.h" #include "EpubReaderChapterSelectionActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <SD.h> #include <SD.h>
@ -8,12 +8,12 @@
constexpr int PAGE_ITEMS = 24; constexpr int PAGE_ITEMS = 24;
constexpr int SKIP_PAGE_MS = 700; constexpr int SKIP_PAGE_MS = 700;
void EpubReaderChapterSelectionScreen::taskTrampoline(void* param) { void EpubReaderChapterSelectionActivity::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderChapterSelectionScreen*>(param); auto* self = static_cast<EpubReaderChapterSelectionActivity*>(param);
self->displayTaskLoop(); self->displayTaskLoop();
} }
void EpubReaderChapterSelectionScreen::onEnter() { void EpubReaderChapterSelectionActivity::onEnter() {
if (!epub) { if (!epub) {
return; return;
} }
@ -23,7 +23,7 @@ void EpubReaderChapterSelectionScreen::onEnter() {
// Trigger first update // Trigger first update
updateRequired = true; updateRequired = true;
xTaskCreate(&EpubReaderChapterSelectionScreen::taskTrampoline, "EpubReaderChapterSelectionScreenTask", xTaskCreate(&EpubReaderChapterSelectionActivity::taskTrampoline, "EpubReaderChapterSelectionActivityTask",
2048, // Stack size 2048, // Stack size
this, // Parameters this, // Parameters
1, // Priority 1, // Priority
@ -31,7 +31,7 @@ void EpubReaderChapterSelectionScreen::onEnter() {
); );
} }
void EpubReaderChapterSelectionScreen::onExit() { void EpubReaderChapterSelectionActivity::onExit() {
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD // Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) { if (displayTaskHandle) {
@ -42,7 +42,7 @@ void EpubReaderChapterSelectionScreen::onExit() {
renderingMutex = nullptr; renderingMutex = nullptr;
} }
void EpubReaderChapterSelectionScreen::handleInput() { void EpubReaderChapterSelectionActivity::loop() {
const bool prevReleased = const bool prevReleased =
inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT); inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT);
const bool nextReleased = const bool nextReleased =
@ -72,7 +72,7 @@ void EpubReaderChapterSelectionScreen::handleInput() {
} }
} }
void EpubReaderChapterSelectionScreen::displayTaskLoop() { void EpubReaderChapterSelectionActivity::displayTaskLoop() {
while (true) { while (true) {
if (updateRequired) { if (updateRequired) {
updateRequired = false; updateRequired = false;
@ -84,7 +84,7 @@ void EpubReaderChapterSelectionScreen::displayTaskLoop() {
} }
} }
void EpubReaderChapterSelectionScreen::renderScreen() { void EpubReaderChapterSelectionActivity::renderScreen() {
renderer.clearScreen(); renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();

View File

@ -6,9 +6,9 @@
#include <memory> #include <memory>
#include "Screen.h" #include "../Activity.h"
class EpubReaderChapterSelectionScreen final : public Screen { class EpubReaderChapterSelectionActivity final : public Activity {
std::shared_ptr<Epub> epub; std::shared_ptr<Epub> epub;
TaskHandle_t displayTaskHandle = nullptr; TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr; SemaphoreHandle_t renderingMutex = nullptr;
@ -23,16 +23,16 @@ class EpubReaderChapterSelectionScreen final : public Screen {
void renderScreen(); void renderScreen();
public: public:
explicit EpubReaderChapterSelectionScreen(GfxRenderer& renderer, InputManager& inputManager, explicit EpubReaderChapterSelectionActivity(GfxRenderer& renderer, InputManager& inputManager,
const std::shared_ptr<Epub>& epub, const int currentSpineIndex, const std::shared_ptr<Epub>& epub, const int currentSpineIndex,
const std::function<void()>& onGoBack, const std::function<void()>& onGoBack,
const std::function<void(int newSpineIndex)>& onSelectSpineIndex) const std::function<void(int newSpineIndex)>& onSelectSpineIndex)
: Screen(renderer, inputManager), : Activity(renderer, inputManager),
epub(epub), epub(epub),
currentSpineIndex(currentSpineIndex), currentSpineIndex(currentSpineIndex),
onGoBack(onGoBack), onGoBack(onGoBack),
onSelectSpineIndex(onSelectSpineIndex) {} onSelectSpineIndex(onSelectSpineIndex) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void handleInput() override; void loop() override;
}; };

View File

@ -1,4 +1,4 @@
#include "FileSelectionScreen.h" #include "FileSelectionActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <SD.h> #include <SD.h>
@ -15,12 +15,12 @@ void sortFileList(std::vector<std::string>& strs) {
}); });
} }
void FileSelectionScreen::taskTrampoline(void* param) { void FileSelectionActivity::taskTrampoline(void* param) {
auto* self = static_cast<FileSelectionScreen*>(param); auto* self = static_cast<FileSelectionActivity*>(param);
self->displayTaskLoop(); self->displayTaskLoop();
} }
void FileSelectionScreen::loadFiles() { void FileSelectionActivity::loadFiles() {
files.clear(); files.clear();
selectorIndex = 0; selectorIndex = 0;
auto root = SD.open(basepath.c_str()); auto root = SD.open(basepath.c_str());
@ -42,7 +42,7 @@ void FileSelectionScreen::loadFiles() {
sortFileList(files); sortFileList(files);
} }
void FileSelectionScreen::onEnter() { void FileSelectionActivity::onEnter() {
renderingMutex = xSemaphoreCreateMutex(); renderingMutex = xSemaphoreCreateMutex();
basepath = "/"; basepath = "/";
@ -52,7 +52,7 @@ void FileSelectionScreen::onEnter() {
// Trigger first update // Trigger first update
updateRequired = true; updateRequired = true;
xTaskCreate(&FileSelectionScreen::taskTrampoline, "FileSelectionScreenTask", xTaskCreate(&FileSelectionActivity::taskTrampoline, "FileSelectionActivityTask",
2048, // Stack size 2048, // Stack size
this, // Parameters this, // Parameters
1, // Priority 1, // Priority
@ -60,7 +60,7 @@ void FileSelectionScreen::onEnter() {
); );
} }
void FileSelectionScreen::onExit() { void FileSelectionActivity::onExit() {
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD // Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) { if (displayTaskHandle) {
@ -72,7 +72,7 @@ void FileSelectionScreen::onExit() {
files.clear(); files.clear();
} }
void FileSelectionScreen::handleInput() { void FileSelectionActivity::loop() {
const bool prevPressed = const bool prevPressed =
inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT); inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT);
const bool nextPressed = const bool nextPressed =
@ -98,8 +98,8 @@ void FileSelectionScreen::handleInput() {
loadFiles(); loadFiles();
updateRequired = true; updateRequired = true;
} else { } else {
// At root level, go to settings // At root level, go back home
onSettingsOpen(); onGoHome();
} }
} else if (prevPressed) { } else if (prevPressed) {
selectorIndex = (selectorIndex + files.size() - 1) % files.size(); selectorIndex = (selectorIndex + files.size() - 1) % files.size();
@ -110,7 +110,7 @@ void FileSelectionScreen::handleInput() {
} }
} }
void FileSelectionScreen::displayTaskLoop() { void FileSelectionActivity::displayTaskLoop() {
while (true) { while (true) {
if (updateRequired) { if (updateRequired) {
updateRequired = false; updateRequired = false;
@ -122,14 +122,14 @@ void FileSelectionScreen::displayTaskLoop() {
} }
} }
void FileSelectionScreen::render() const { void FileSelectionActivity::render() const {
renderer.clearScreen(); renderer.clearScreen();
const auto pageWidth = GfxRenderer::getScreenWidth(); const auto pageWidth = GfxRenderer::getScreenWidth();
renderer.drawCenteredText(READER_FONT_ID, 10, "CrossPoint Reader", true, BOLD); renderer.drawCenteredText(READER_FONT_ID, 10, "CrossPoint Reader", true, BOLD);
// Help text // Help text
renderer.drawText(SMALL_FONT_ID, 20, GfxRenderer::getScreenHeight() - 30, "Press BACK for Settings"); renderer.drawText(SMALL_FONT_ID, 20, GfxRenderer::getScreenHeight() - 30, "Press BACK for Home");
if (files.empty()) { if (files.empty()) {
renderer.drawText(UI_FONT_ID, 20, 60, "No EPUBs found"); renderer.drawText(UI_FONT_ID, 20, 60, "No EPUBs found");

View File

@ -7,9 +7,9 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "Screen.h" #include "../Activity.h"
class FileSelectionScreen final : public Screen { class FileSelectionActivity final : public Activity {
TaskHandle_t displayTaskHandle = nullptr; TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr; SemaphoreHandle_t renderingMutex = nullptr;
std::string basepath = "/"; std::string basepath = "/";
@ -17,7 +17,7 @@ class FileSelectionScreen final : public Screen {
int selectorIndex = 0; int selectorIndex = 0;
bool updateRequired = false; bool updateRequired = false;
const std::function<void(const std::string&)> onSelect; const std::function<void(const std::string&)> onSelect;
const std::function<void()> onSettingsOpen; const std::function<void()> onGoHome;
static void taskTrampoline(void* param); static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop(); [[noreturn]] void displayTaskLoop();
@ -25,11 +25,11 @@ class FileSelectionScreen final : public Screen {
void loadFiles(); void loadFiles();
public: public:
explicit FileSelectionScreen(GfxRenderer& renderer, InputManager& inputManager, explicit FileSelectionActivity(GfxRenderer& renderer, InputManager& inputManager,
const std::function<void(const std::string&)>& onSelect, const std::function<void(const std::string&)>& onSelect,
const std::function<void()>& onSettingsOpen) const std::function<void()>& onGoHome)
: Screen(renderer, inputManager), onSelect(onSelect), onSettingsOpen(onSettingsOpen) {} : Activity(renderer, inputManager), onSelect(onSelect), onGoHome(onGoHome) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void handleInput() override; void loop() override;
}; };

View File

@ -0,0 +1,68 @@
#include "ReaderActivity.h"
#include <SD.h>
#include "CrossPointState.h"
#include "Epub.h"
#include "EpubReaderActivity.h"
#include "FileSelectionActivity.h"
#include "activities/util/FullScreenMessageActivity.h"
std::unique_ptr<Epub> ReaderActivity::loadEpub(const std::string& path) {
if (!SD.exists(path.c_str())) {
Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str());
return nullptr;
}
auto epub = std::unique_ptr<Epub>(new Epub(path, "/.crosspoint"));
if (epub->load()) {
return epub;
}
Serial.printf("[%lu] [ ] Failed to load epub\n", millis());
return nullptr;
}
void ReaderActivity::onSelectEpubFile(const std::string& path) {
exitActivity();
enterNewActivity(new FullScreenMessageActivity(renderer, inputManager, "Loading..."));
auto epub = loadEpub(path);
if (epub) {
APP_STATE.openEpubPath = path;
APP_STATE.saveToFile();
onGoToEpubReader(std::move(epub));
} else {
exitActivity();
enterNewActivity(new FullScreenMessageActivity(renderer, inputManager, "Failed to load epub", REGULAR,
EInkDisplay::HALF_REFRESH));
delay(2000);
onGoToFileSelection();
}
}
void ReaderActivity::onGoToFileSelection() {
exitActivity();
enterNewActivity(new FileSelectionActivity(
renderer, inputManager, [this](const std::string& path) { onSelectEpubFile(path); }, onGoBack));
}
void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) {
exitActivity();
enterNewActivity(new EpubReaderActivity(renderer, inputManager, std::move(epub), [this] { onGoToFileSelection(); }));
}
void ReaderActivity::onEnter() {
if (initialEpubPath.empty()) {
onGoToFileSelection();
return;
}
auto epub = loadEpub(initialEpubPath);
if (!epub) {
onGoBack();
return;
}
onGoToEpubReader(std::move(epub));
}

View File

@ -0,0 +1,24 @@
#pragma once
#include <memory>
#include "../ActivityWithSubactivity.h"
class Epub;
class ReaderActivity final : public ActivityWithSubactivity {
std::string initialEpubPath;
const std::function<void()> onGoBack;
static std::unique_ptr<Epub> loadEpub(const std::string& path);
void onSelectEpubFile(const std::string& path);
void onGoToFileSelection();
void onGoToEpubReader(std::unique_ptr<Epub> epub);
public:
explicit ReaderActivity(GfxRenderer& renderer, InputManager& inputManager, std::string initialEpubPath,
const std::function<void()>& onGoBack)
: ActivityWithSubactivity(renderer, inputManager),
initialEpubPath(std::move(initialEpubPath)),
onGoBack(onGoBack) {}
void onEnter() override;
};

View File

@ -1,4 +1,4 @@
#include "SettingsScreen.h" #include "SettingsActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
@ -7,17 +7,16 @@
// Define the static settings list // Define the static settings list
const SettingInfo SettingsScreen::settingsList[SettingsScreen::settingsCount] = { const SettingInfo SettingsActivity::settingsList[settingsCount] = {
{"White Sleep Screen", SettingType::TOGGLE, &CrossPointSettings::whiteSleepScreen}, {"White Sleep Screen", &CrossPointSettings::whiteSleepScreen},
{"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing}, {"Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing}};
{"WiFi", SettingType::ACTION, nullptr}};
void SettingsScreen::taskTrampoline(void* param) { void SettingsActivity::taskTrampoline(void* param) {
auto* self = static_cast<SettingsScreen*>(param); auto* self = static_cast<SettingsActivity*>(param);
self->displayTaskLoop(); self->displayTaskLoop();
} }
void SettingsScreen::onEnter() { void SettingsActivity::onEnter() {
renderingMutex = xSemaphoreCreateMutex(); renderingMutex = xSemaphoreCreateMutex();
// Reset selection to first item // Reset selection to first item
@ -26,7 +25,7 @@ void SettingsScreen::onEnter() {
// Trigger first update // Trigger first update
updateRequired = true; updateRequired = true;
xTaskCreate(&SettingsScreen::taskTrampoline, "SettingsScreenTask", xTaskCreate(&SettingsActivity::taskTrampoline, "SettingsActivityTask",
2048, // Stack size 2048, // Stack size
this, // Parameters this, // Parameters
1, // Priority 1, // Priority
@ -34,7 +33,7 @@ void SettingsScreen::onEnter() {
); );
} }
void SettingsScreen::onExit() { void SettingsActivity::onExit() {
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD // Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) { if (displayTaskHandle) {
@ -45,10 +44,10 @@ void SettingsScreen::onExit() {
renderingMutex = nullptr; renderingMutex = nullptr;
} }
void SettingsScreen::handleInput() { void SettingsActivity::loop() {
// Handle actions with early return // Handle actions with early return
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) { if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
activateCurrentSetting(); toggleCurrentSetting();
updateRequired = true; updateRequired = true;
return; return;
} }
@ -73,7 +72,7 @@ void SettingsScreen::handleInput() {
} }
} }
void SettingsScreen::activateCurrentSetting() { void SettingsActivity::toggleCurrentSetting() {
// Validate index // Validate index
if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) { if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) {
return; return;
@ -81,31 +80,6 @@ void SettingsScreen::activateCurrentSetting() {
const auto& setting = settingsList[selectedSettingIndex]; const auto& setting = settingsList[selectedSettingIndex];
if (setting.type == SettingType::TOGGLE) {
toggleCurrentSetting();
// Trigger a redraw of the entire screen
updateRequired = true;
} else if (setting.type == SettingType::ACTION) {
// Handle action settings
if (std::string(setting.name) == "WiFi") {
onGoWifi();
}
}
}
void SettingsScreen::toggleCurrentSetting() {
// Validate index
if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) {
return;
}
const auto& setting = settingsList[selectedSettingIndex];
// Only toggle if it's a toggle type and has a value pointer
if (setting.type != SettingType::TOGGLE || setting.valuePtr == nullptr) {
return;
}
// Toggle the boolean value using the member pointer // Toggle the boolean value using the member pointer
bool currentValue = SETTINGS.*(setting.valuePtr); bool currentValue = SETTINGS.*(setting.valuePtr);
SETTINGS.*(setting.valuePtr) = !currentValue; SETTINGS.*(setting.valuePtr) = !currentValue;
@ -114,7 +88,7 @@ void SettingsScreen::toggleCurrentSetting() {
SETTINGS.saveToFile(); SETTINGS.saveToFile();
} }
void SettingsScreen::displayTaskLoop() { void SettingsActivity::displayTaskLoop() {
while (true) { while (true) {
if (updateRequired) { if (updateRequired) {
updateRequired = false; updateRequired = false;
@ -126,7 +100,7 @@ void SettingsScreen::displayTaskLoop() {
} }
} }
void SettingsScreen::render() const { void SettingsActivity::render() const {
renderer.clearScreen(); renderer.clearScreen();
const auto pageWidth = GfxRenderer::getScreenWidth(); const auto pageWidth = GfxRenderer::getScreenWidth();
@ -149,12 +123,10 @@ void SettingsScreen::render() const {
// Draw setting name // Draw setting name
renderer.drawText(UI_FONT_ID, 20, settingY, settingsList[i].name); renderer.drawText(UI_FONT_ID, 20, settingY, settingsList[i].name);
// Draw value based on setting type // Draw value (all settings are toggles now)
if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) { if (settingsList[i].valuePtr != nullptr) {
bool value = SETTINGS.*(settingsList[i].valuePtr); bool value = SETTINGS.*(settingsList[i].valuePtr);
renderer.drawText(UI_FONT_ID, pageWidth - 80, settingY, value ? "ON" : "OFF"); renderer.drawText(UI_FONT_ID, pageWidth - 80, settingY, value ? "ON" : "OFF");
} else if (settingsList[i].type == SettingType::ACTION) {
renderer.drawText(UI_FONT_ID, pageWidth - 80, settingY, ">");
} }
} }

View File

@ -8,43 +8,36 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "Screen.h" #include "../Activity.h"
class CrossPointSettings; class CrossPointSettings;
// Enum to distinguish setting types
enum class SettingType { TOGGLE, ACTION };
// Structure to hold setting information // Structure to hold setting information
struct SettingInfo { struct SettingInfo {
const char* name; // Display name of the setting const char* name; // Display name of the setting
SettingType type; // Type of setting uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings
uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings (for TOGGLE)
}; };
class SettingsScreen final : public Screen { class SettingsActivity final : public Activity {
TaskHandle_t displayTaskHandle = nullptr; TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr; SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false; bool updateRequired = false;
int selectedSettingIndex = 0; // Currently selected setting int selectedSettingIndex = 0; // Currently selected setting
const std::function<void()> onGoHome; const std::function<void()> onGoHome;
const std::function<void()> onGoWifi;
// Static settings list // Static settings list
static constexpr int settingsCount = 3; // Number of settings static constexpr int settingsCount = 2; // Number of settings
static const SettingInfo settingsList[settingsCount]; static const SettingInfo settingsList[settingsCount];
static void taskTrampoline(void* param); static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop(); [[noreturn]] void displayTaskLoop();
void render() const; void render() const;
void toggleCurrentSetting(); void toggleCurrentSetting();
void activateCurrentSetting();
public: public:
explicit SettingsScreen(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoHome, explicit SettingsActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoHome)
const std::function<void()>& onGoWifi) : Activity(renderer, inputManager), onGoHome(onGoHome) {}
: Screen(renderer, inputManager), onGoHome(onGoHome), onGoWifi(onGoWifi) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void handleInput() override; void loop() override;
}; };

View File

@ -1,10 +1,10 @@
#include "FullScreenMessageScreen.h" #include "FullScreenMessageActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include "config.h" #include "config.h"
void FullScreenMessageScreen::onEnter() { void FullScreenMessageActivity::onEnter() {
const auto height = renderer.getLineHeight(UI_FONT_ID); const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (GfxRenderer::getScreenHeight() - height) / 2; const auto top = (GfxRenderer::getScreenHeight() - height) / 2;

View File

@ -0,0 +1,21 @@
#pragma once
#include <EInkDisplay.h>
#include <EpdFontFamily.h>
#include <string>
#include <utility>
#include "../Activity.h"
class FullScreenMessageActivity final : public Activity {
std::string text;
EpdFontStyle style;
EInkDisplay::RefreshMode refreshMode;
public:
explicit FullScreenMessageActivity(GfxRenderer& renderer, InputManager& inputManager, std::string text,
const EpdFontStyle style = REGULAR,
const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH)
: Activity(renderer, inputManager), text(std::move(text)), style(style), refreshMode(refreshMode) {}
void onEnter() override;
};

View File

@ -17,15 +17,13 @@
#include "Battery.h" #include "Battery.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "CrossPointState.h" #include "CrossPointState.h"
#include "CrossPointWebServer.h" #include "activities/boot_sleep/BootActivity.h"
#include "activities/boot_sleep/SleepActivity.h"
#include "activities/home/HomeActivity.h"
#include "activities/reader/ReaderActivity.h"
#include "activities/settings/SettingsActivity.h"
#include "activities/util/FullScreenMessageActivity.h"
#include "config.h" #include "config.h"
#include "screens/BootLogoScreen.h"
#include "screens/EpubReaderScreen.h"
#include "screens/FileSelectionScreen.h"
#include "screens/FullScreenMessageScreen.h"
#include "screens/SettingsScreen.h"
#include "screens/SleepScreen.h"
#include "screens/WifiScreen.h"
#define SPI_FQ 40000000 #define SPI_FQ 40000000
// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults) // Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults)
@ -44,8 +42,7 @@
EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY); EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY);
InputManager inputManager; InputManager inputManager;
GfxRenderer renderer(einkDisplay); GfxRenderer renderer(einkDisplay);
Screen* currentScreen; Activity* currentActivity;
CrossPointState appState;
// Fonts // Fonts
EpdFont bookerlyFont(&bookerly_2b); EpdFont bookerlyFont(&bookerly_2b);
@ -69,31 +66,16 @@ constexpr unsigned long POWER_BUTTON_SLEEP_MS = 500;
// Auto-sleep timeout (10 minutes of inactivity) // Auto-sleep timeout (10 minutes of inactivity)
constexpr unsigned long AUTO_SLEEP_TIMEOUT_MS = 10 * 60 * 1000; constexpr unsigned long AUTO_SLEEP_TIMEOUT_MS = 10 * 60 * 1000;
std::unique_ptr<Epub> loadEpub(const std::string& path) { void exitActivity() {
if (!SD.exists(path.c_str())) { if (currentActivity) {
Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str()); currentActivity->onExit();
return nullptr; delete currentActivity;
}
auto epub = std::unique_ptr<Epub>(new Epub(path, "/.crosspoint"));
if (epub->load()) {
return epub;
}
Serial.printf("[%lu] [ ] Failed to load epub\n", millis());
return nullptr;
}
void exitScreen() {
if (currentScreen) {
currentScreen->onExit();
delete currentScreen;
} }
} }
void enterNewScreen(Screen* screen) { void enterNewActivity(Activity* activity) {
currentScreen = screen; currentActivity = activity;
currentScreen->onEnter(); currentActivity->onEnter();
} }
// Verify long press on wake-up from deep sleep // Verify long press on wake-up from deep sleep
@ -137,8 +119,8 @@ void waitForPowerRelease() {
// Enter deep sleep mode // Enter deep sleep mode
void enterDeepSleep() { void enterDeepSleep() {
exitScreen(); exitActivity();
enterNewScreen(new SleepScreen(renderer, inputManager)); enterNewActivity(new SleepActivity(renderer, inputManager));
Serial.printf("[%lu] [ ] Power button released after a long press. Entering deep sleep.\n", millis()); Serial.printf("[%lu] [ ] Power button released after a long press. Entering deep sleep.\n", millis());
delay(1000); // Allow Serial buffer to empty and display to update delay(1000); // Allow Serial buffer to empty and display to update
@ -153,40 +135,20 @@ void enterDeepSleep() {
} }
void onGoHome(); void onGoHome();
void onSelectEpubFile(const std::string& path) { void onGoToReader(const std::string& initialEpubPath) {
exitScreen(); exitActivity();
enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Loading...")); enterNewActivity(new ReaderActivity(renderer, inputManager, initialEpubPath, onGoHome));
auto epub = loadEpub(path);
if (epub) {
appState.openEpubPath = path;
appState.saveToFile();
exitScreen();
enterNewScreen(new EpubReaderScreen(renderer, inputManager, std::move(epub), onGoHome));
} else {
exitScreen();
enterNewScreen(
new FullScreenMessageScreen(renderer, inputManager, "Failed to load epub", REGULAR, EInkDisplay::HALF_REFRESH));
delay(2000);
onGoHome();
}
}
void onGoToSettings();
void onGoToWifi() {
exitScreen();
enterNewScreen(new WifiScreen(renderer, inputManager, onGoToSettings));
} }
void onGoToReaderHome() { onGoToReader(std::string()); }
void onGoToSettings() { void onGoToSettings() {
exitScreen(); exitActivity();
enterNewScreen(new SettingsScreen(renderer, inputManager, onGoHome, onGoToWifi)); enterNewActivity(new SettingsActivity(renderer, inputManager, onGoHome));
} }
void onGoHome() { void onGoHome() {
exitScreen(); exitActivity();
enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile, onGoToSettings)); enterNewActivity(new HomeActivity(renderer, inputManager, onGoToReaderHome, onGoToSettings));
} }
void setup() { void setup() {
@ -212,28 +174,20 @@ void setup() {
renderer.insertFont(SMALL_FONT_ID, smallFontFamily); renderer.insertFont(SMALL_FONT_ID, smallFontFamily);
Serial.printf("[%lu] [ ] Fonts setup\n", millis()); Serial.printf("[%lu] [ ] Fonts setup\n", millis());
exitScreen(); exitActivity();
enterNewScreen(new BootLogoScreen(renderer, inputManager)); enterNewActivity(new BootActivity(renderer, inputManager));
// SD Card Initialization // SD Card Initialization
SD.begin(SD_SPI_CS, SPI, SPI_FQ); SD.begin(SD_SPI_CS, SPI, SPI_FQ);
SETTINGS.loadFromFile(); SETTINGS.loadFromFile();
appState.loadFromFile(); APP_STATE.loadFromFile();
if (!appState.openEpubPath.empty()) { if (APP_STATE.openEpubPath.empty()) {
auto epub = loadEpub(appState.openEpubPath); onGoHome();
if (epub) { } else {
exitScreen(); onGoToReader(APP_STATE.openEpubPath);
enterNewScreen(new EpubReaderScreen(renderer, inputManager, std::move(epub), onGoHome));
// Ensure we're not still holding the power button before leaving setup
waitForPowerRelease();
return;
}
} }
exitScreen();
enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile, onGoToSettings));
// Ensure we're not still holding the power button before leaving setup // Ensure we're not still holding the power button before leaving setup
waitForPowerRelease(); waitForPowerRelease();
} }
@ -248,11 +202,6 @@ void loop() {
lastMemPrint = millis(); lastMemPrint = millis();
} }
// Handle web server clients if WiFi is connected
if (WiFi.status() == WL_CONNECTED) {
crossPointWebServer.handleClient();
}
inputManager.update(); inputManager.update();
// Check for any user activity (button press or release) // Check for any user activity (button press or release)
@ -274,7 +223,7 @@ void loop() {
return; return;
} }
if (currentScreen) { if (currentActivity) {
currentScreen->handleInput(); currentActivity->loop();
} }
} }

View File

@ -1,8 +0,0 @@
#pragma once
#include "Screen.h"
class BootLogoScreen final : public Screen {
public:
explicit BootLogoScreen(GfxRenderer& renderer, InputManager& inputManager) : Screen(renderer, inputManager) {}
void onEnter() override;
};

View File

@ -1,21 +0,0 @@
#pragma once
#include <EInkDisplay.h>
#include <EpdFontFamily.h>
#include <string>
#include <utility>
#include "Screen.h"
class FullScreenMessageScreen final : public Screen {
std::string text;
EpdFontStyle style;
EInkDisplay::RefreshMode refreshMode;
public:
explicit FullScreenMessageScreen(GfxRenderer& renderer, InputManager& inputManager, std::string text,
const EpdFontStyle style = REGULAR,
const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH)
: Screen(renderer, inputManager), text(std::move(text)), style(style), refreshMode(refreshMode) {}
void onEnter() override;
};

View File

@ -1,17 +0,0 @@
#pragma once
#include <InputManager.h>
class GfxRenderer;
class Screen {
protected:
GfxRenderer& renderer;
InputManager& inputManager;
public:
explicit Screen(GfxRenderer& renderer, InputManager& inputManager) : renderer(renderer), inputManager(inputManager) {}
virtual ~Screen() = default;
virtual void onEnter() {}
virtual void onExit() {}
virtual void handleInput() {}
};

View File

@ -1,8 +0,0 @@
#pragma once
#include "Screen.h"
class SleepScreen final : public Screen {
public:
explicit SleepScreen(GfxRenderer& renderer, InputManager& inputManager) : Screen(renderer, inputManager) {}
void onEnter() override;
};