Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfc74f94c2 | ||
|
|
3518cbb56d | ||
|
|
8994953254 | ||
|
|
ead39fd04b |
@@ -59,6 +59,10 @@ back to the other partition using the "Swap boot partition" button here https://
|
|||||||
|
|
||||||
See [Development](#development) below.
|
See [Development](#development) below.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|||||||
78
USER_GUIDE.md
Normal file
78
USER_GUIDE.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# CrossPoint User Guide
|
||||||
|
|
||||||
|
Welcome to the **CrossPoint** firmware. This guide outlines the hardware controls, navigation, and reading features of
|
||||||
|
the device.
|
||||||
|
|
||||||
|
## 1. Hardware Overview
|
||||||
|
|
||||||
|
The device utilises the standard buttons on the Xtink X4 in the same layout:
|
||||||
|
|
||||||
|
### Button Layout
|
||||||
|
| Location | Buttons |
|
||||||
|
|-----------------|--------------------------------------------|
|
||||||
|
| **Bottom Edge** | **Back**, **Confirm**, **Left**, **Right** |
|
||||||
|
| **Right Side** | **Power**, **Volume Up**, **Volume Down** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Power & Startup
|
||||||
|
|
||||||
|
### Power On / Off
|
||||||
|
|
||||||
|
To turn the device on or off, **press and hold the Power button for 1 full second**.
|
||||||
|
|
||||||
|
### First Launch
|
||||||
|
|
||||||
|
Upon turning the device on for the first time, you will be placed on the **Book Selection Screen** (File Browser).
|
||||||
|
|
||||||
|
> **Note:** On subsequent restarts, the firmware will automatically reopen the last book you were reading.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Book Selection
|
||||||
|
|
||||||
|
The Home Screen acts as a folder and file browser.
|
||||||
|
|
||||||
|
* **Navigate List:** Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to move the selection cursor up
|
||||||
|
and down through folders and books.
|
||||||
|
* **Open Selection:** Press **Confirm** to open a folder or read a selected book.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Reading Mode
|
||||||
|
|
||||||
|
Once you have opened a book, the button layout changes to facilitate reading.
|
||||||
|
|
||||||
|
### Page Turning
|
||||||
|
| Action | Buttons |
|
||||||
|
|-------------------|--------------------------------------|
|
||||||
|
| **Previous Page** | Press **Left** _or_ **Volume Up** |
|
||||||
|
| **Next Page** | Press **Right** _or_ **Volume Down** |
|
||||||
|
|
||||||
|
### Chapter Navigation
|
||||||
|
* **Next Chapter:** Press and **hold** the **Right** (or **Volume Down**) button briefly, then release.
|
||||||
|
* **Previous Chapter:** Press and **hold** the **Left** (or **Volume Up**) button briefly, then release.
|
||||||
|
|
||||||
|
### System Navigation
|
||||||
|
* **Return to Home:** Press **Back** to close the book and return to the Book Selection screen.
|
||||||
|
* **Chapter Menu:** Press **Confirm** to open the Table of Contents/Chapter Selection screen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Chapter Selection Screen
|
||||||
|
|
||||||
|
Accessible by pressing **Confirm** while inside a book.
|
||||||
|
|
||||||
|
1. Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to highlight the desired chapter.
|
||||||
|
2. Press **Confirm** to jump to that chapter.
|
||||||
|
3. *Alternatively, press **Back** to cancel and return to your current page.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Current Limitations & Roadmap
|
||||||
|
|
||||||
|
Please note that this firmware is currently in active development. The following features are **not yet supported** but
|
||||||
|
are planned for future updates:
|
||||||
|
|
||||||
|
* **Images:** Embedded images in e-books will not render.
|
||||||
|
* **Text Formatting:** There are currently no settings to adjust font type, size, line spacing, or margins.
|
||||||
@@ -299,6 +299,5 @@ 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());
|
||||||
// not found - default to first item
|
return -1;
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[platformio]
|
[platformio]
|
||||||
crosspoint_version = 0.5.0
|
crosspoint_version = 0.5.1
|
||||||
default_envs = default
|
default_envs = default
|
||||||
|
|
||||||
[base]
|
[base]
|
||||||
|
|||||||
107
src/screens/EpubReaderChapterSelectionScreen.cpp
Normal file
107
src/screens/EpubReaderChapterSelectionScreen.cpp
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
#include "EpubReaderChapterSelectionScreen.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <SD.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
constexpr int PAGE_ITEMS = 24;
|
||||||
|
constexpr int SKIP_PAGE_MS = 700;
|
||||||
|
|
||||||
|
void EpubReaderChapterSelectionScreen::taskTrampoline(void* param) {
|
||||||
|
auto* self = static_cast<EpubReaderChapterSelectionScreen*>(param);
|
||||||
|
self->displayTaskLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderChapterSelectionScreen::onEnter() {
|
||||||
|
if (!epub) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
|
selectorIndex = currentSpineIndex;
|
||||||
|
|
||||||
|
// Trigger first update
|
||||||
|
updateRequired = true;
|
||||||
|
xTaskCreate(&EpubReaderChapterSelectionScreen::taskTrampoline, "EpubReaderChapterSelectionScreenTask",
|
||||||
|
2048, // Stack size
|
||||||
|
this, // Parameters
|
||||||
|
1, // Priority
|
||||||
|
&displayTaskHandle // Task handle
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderChapterSelectionScreen::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 EpubReaderChapterSelectionScreen::handleInput() {
|
||||||
|
const bool prevReleased =
|
||||||
|
inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT);
|
||||||
|
const bool nextReleased =
|
||||||
|
inputManager.wasReleased(InputManager::BTN_DOWN) || inputManager.wasReleased(InputManager::BTN_RIGHT);
|
||||||
|
|
||||||
|
const bool skipPage = inputManager.getHeldTime() > SKIP_PAGE_MS;
|
||||||
|
|
||||||
|
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
||||||
|
onSelectSpineIndex(selectorIndex);
|
||||||
|
} else if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
||||||
|
onGoBack();
|
||||||
|
} else if (prevReleased) {
|
||||||
|
if (skipPage) {
|
||||||
|
selectorIndex =
|
||||||
|
((selectorIndex / PAGE_ITEMS - 1) * PAGE_ITEMS + epub->getSpineItemsCount()) % epub->getSpineItemsCount();
|
||||||
|
} else {
|
||||||
|
selectorIndex = (selectorIndex + epub->getSpineItemsCount() - 1) % epub->getSpineItemsCount();
|
||||||
|
}
|
||||||
|
updateRequired = true;
|
||||||
|
} else if (nextReleased) {
|
||||||
|
if (skipPage) {
|
||||||
|
selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % epub->getSpineItemsCount();
|
||||||
|
} else {
|
||||||
|
selectorIndex = (selectorIndex + 1) % epub->getSpineItemsCount();
|
||||||
|
}
|
||||||
|
updateRequired = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderChapterSelectionScreen::displayTaskLoop() {
|
||||||
|
while (true) {
|
||||||
|
if (updateRequired) {
|
||||||
|
updateRequired = false;
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
renderScreen();
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
}
|
||||||
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderChapterSelectionScreen::renderScreen() {
|
||||||
|
renderer.clearScreen();
|
||||||
|
|
||||||
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
|
renderer.drawCenteredText(READER_FONT_ID, 10, "Select Chapter", true, BOLD);
|
||||||
|
|
||||||
|
const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS;
|
||||||
|
renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 + 2, pageWidth - 1, 30);
|
||||||
|
for (int i = pageStartIndex; i < epub->getSpineItemsCount() && i < pageStartIndex + PAGE_ITEMS; i++) {
|
||||||
|
const int tocIndex = epub->getTocIndexForSpineIndex(i);
|
||||||
|
if (tocIndex == -1) {
|
||||||
|
renderer.drawText(UI_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, "Unnamed", i != selectorIndex);
|
||||||
|
} else {
|
||||||
|
auto item = epub->getTocItem(tocIndex);
|
||||||
|
renderer.drawText(UI_FONT_ID, 20 + (item.level - 1) * 15, 60 + (i % PAGE_ITEMS) * 30, item.title.c_str(),
|
||||||
|
i != selectorIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.displayBuffer();
|
||||||
|
}
|
||||||
38
src/screens/EpubReaderChapterSelectionScreen.h
Normal file
38
src/screens/EpubReaderChapterSelectionScreen.h
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Epub.h>
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "Screen.h"
|
||||||
|
|
||||||
|
class EpubReaderChapterSelectionScreen final : public Screen {
|
||||||
|
std::shared_ptr<Epub> epub;
|
||||||
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
int currentSpineIndex = 0;
|
||||||
|
int selectorIndex = 0;
|
||||||
|
bool updateRequired = false;
|
||||||
|
const std::function<void()> onGoBack;
|
||||||
|
const std::function<void(int newSpineIndex)> onSelectSpineIndex;
|
||||||
|
|
||||||
|
static void taskTrampoline(void* param);
|
||||||
|
[[noreturn]] void displayTaskLoop();
|
||||||
|
void renderScreen();
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit EpubReaderChapterSelectionScreen(GfxRenderer& renderer, InputManager& inputManager,
|
||||||
|
const std::shared_ptr<Epub>& epub, const int currentSpineIndex,
|
||||||
|
const std::function<void()>& onGoBack,
|
||||||
|
const std::function<void(int newSpineIndex)>& onSelectSpineIndex)
|
||||||
|
: Screen(renderer, inputManager),
|
||||||
|
epub(epub),
|
||||||
|
currentSpineIndex(currentSpineIndex),
|
||||||
|
onGoBack(onGoBack),
|
||||||
|
onSelectSpineIndex(onSelectSpineIndex) {}
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void handleInput() override;
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
|
|
||||||
#include "Battery.h"
|
#include "Battery.h"
|
||||||
|
#include "EpubReaderChapterSelectionScreen.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
constexpr int PAGES_PER_REFRESH = 15;
|
constexpr int PAGES_PER_REFRESH = 15;
|
||||||
@@ -65,6 +66,37 @@ void EpubReaderScreen::onExit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void EpubReaderScreen::handleInput() {
|
void EpubReaderScreen::handleInput() {
|
||||||
|
// Pass input responsibility to sub screen if exists
|
||||||
|
if (subScreen) {
|
||||||
|
subScreen->handleInput();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter chapter selection screen
|
||||||
|
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
||||||
|
// Don't start screen transition while rendering
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
subScreen.reset(new EpubReaderChapterSelectionScreen(
|
||||||
|
this->renderer, this->inputManager, epub, currentSpineIndex,
|
||||||
|
[this] {
|
||||||
|
subScreen->onExit();
|
||||||
|
subScreen.reset();
|
||||||
|
updateRequired = true;
|
||||||
|
},
|
||||||
|
[this](const int newSpineIndex) {
|
||||||
|
if (currentSpineIndex != newSpineIndex) {
|
||||||
|
currentSpineIndex = newSpineIndex;
|
||||||
|
nextPageNumber = 0;
|
||||||
|
section.reset();
|
||||||
|
}
|
||||||
|
subScreen->onExit();
|
||||||
|
subScreen.reset();
|
||||||
|
updateRequired = true;
|
||||||
|
}));
|
||||||
|
subScreen->onEnter();
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
}
|
||||||
|
|
||||||
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
||||||
onGoHome();
|
onGoHome();
|
||||||
return;
|
return;
|
||||||
@@ -328,12 +360,21 @@ void EpubReaderScreen::renderStatusBar() const {
|
|||||||
const int titleMarginLeft = 20 + percentageTextWidth + 30 + marginLeft;
|
const int titleMarginLeft = 20 + percentageTextWidth + 30 + marginLeft;
|
||||||
const int titleMarginRight = progressTextWidth + 30 + marginRight;
|
const int titleMarginRight = progressTextWidth + 30 + marginRight;
|
||||||
const int availableTextWidth = GfxRenderer::getScreenWidth() - titleMarginLeft - titleMarginRight;
|
const int availableTextWidth = GfxRenderer::getScreenWidth() - titleMarginLeft - titleMarginRight;
|
||||||
const auto tocItem = epub->getTocItem(epub->getTocIndexForSpineIndex(currentSpineIndex));
|
const int tocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
|
||||||
auto title = tocItem.title;
|
|
||||||
int titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
std::string title;
|
||||||
while (titleWidth > availableTextWidth) {
|
int titleWidth;
|
||||||
title = title.substr(0, title.length() - 8) + "...";
|
if (tocIndex == -1) {
|
||||||
|
title = "Unnamed";
|
||||||
|
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, "Unnamed");
|
||||||
|
} else {
|
||||||
|
const auto tocItem = epub->getTocItem(tocIndex);
|
||||||
|
title = tocItem.title;
|
||||||
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
||||||
|
while (titleWidth > availableTextWidth) {
|
||||||
|
title = title.substr(0, title.length() - 8) + "...";
|
||||||
|
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.drawText(SMALL_FONT_ID, titleMarginLeft + (availableTextWidth - titleWidth) / 2, textY, title.c_str());
|
renderer.drawText(SMALL_FONT_ID, titleMarginLeft + (availableTextWidth - titleWidth) / 2, textY, title.c_str());
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class EpubReaderScreen final : public Screen {
|
|||||||
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;
|
||||||
int currentSpineIndex = 0;
|
int currentSpineIndex = 0;
|
||||||
int nextPageNumber = 0;
|
int nextPageNumber = 0;
|
||||||
int pagesUntilFullRefresh = 0;
|
int pagesUntilFullRefresh = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user