You've already forked crosspoint-reader
mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-04-29 10:26:52 -07:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0a7088fcc | |||
| e79f86fa24 | |||
| fd933ba368 | |||
| c14dc4c109 | |||
| cd4303a8fb | |||
| 961a704aaa | |||
| 7061d054ef | |||
| 39bf8a5609 | |||
| a78897478f | |||
| 4f0a39ab03 | |||
| d97a436a48 | |||
| 6114b80e5d | |||
| d4eb089e49 | |||
| 9a1548189c | |||
| cbea838f91 |
@@ -1 +0,0 @@
|
||||
../../.skills/SKILL.md
|
||||
@@ -44,14 +44,8 @@ jobs:
|
||||
with:
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
version: "latest"
|
||||
enable-cache: false
|
||||
|
||||
- name: Install PlatformIO Core
|
||||
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
|
||||
run: pip install --upgrade platformio
|
||||
|
||||
- name: Run cppcheck
|
||||
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
|
||||
@@ -67,14 +61,8 @@ jobs:
|
||||
with:
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
version: "latest"
|
||||
enable-cache: false
|
||||
|
||||
- name: Install PlatformIO Core
|
||||
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
|
||||
run: pip install --upgrade platformio
|
||||
|
||||
- name: Build CrossPoint
|
||||
run: |
|
||||
|
||||
@@ -12,18 +12,19 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pip
|
||||
~/.platformio/.cache
|
||||
key: ${{ runner.os }}-pio
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
version: "latest"
|
||||
enable-cache: false
|
||||
|
||||
- name: Install PlatformIO Core
|
||||
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
|
||||
run: pip install --upgrade platformio
|
||||
|
||||
- name: Build CrossPoint
|
||||
run: pio run -e gh_release
|
||||
|
||||
@@ -12,18 +12,19 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pip
|
||||
~/.platformio/.cache
|
||||
key: ${{ runner.os }}-pio
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
version: "latest"
|
||||
enable-cache: false
|
||||
|
||||
- name: Install PlatformIO Core
|
||||
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
|
||||
run: pip install --upgrade platformio
|
||||
|
||||
- name: Extract env
|
||||
run: |
|
||||
|
||||
+1
-6
@@ -3,15 +3,10 @@
|
||||
.DS_Store
|
||||
.vscode
|
||||
lib/EpdFont/fontsrc
|
||||
lib/I18n/I18nKeys.h
|
||||
lib/I18n/I18nStrings.h
|
||||
lib/I18n/I18nStrings.cpp
|
||||
*.generated.h
|
||||
.vs
|
||||
build
|
||||
**/__pycache__/
|
||||
/compile_commands.json
|
||||
/.cache
|
||||
.history/
|
||||
/.venv
|
||||
*.local*
|
||||
/notes.md
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@ Xteink firmware. It aims to match or improve upon the standard EPUB reading expe
|
||||
|
||||
E-paper devices are fantastic for reading, but most commercially available readers are closed systems with limited
|
||||
customisation. The **Xteink X4** is an affordable, e-paper device, however the official firmware remains closed.
|
||||
CrossPoint exists partly as a fun side-project and partly to open up the ecosystem and truly unlock the device's
|
||||
CrossPoint exists partly as a fun side-project and partly to open up the ecosystem and truely unlock the device's
|
||||
potential.
|
||||
|
||||
CrossPoint Reader aims to:
|
||||
@@ -26,7 +26,7 @@ This project is **not affiliated with Xteink**; it's built as a community projec
|
||||
## Features & Usage
|
||||
|
||||
- [x] EPUB parsing and rendering (EPUB 2 and EPUB 3)
|
||||
- [x] Image support within EPUB
|
||||
- [ ] Image support within EPUB
|
||||
- [x] Saved reading position
|
||||
- [x] File explorer with file picker
|
||||
- [x] Basic EPUB picker from root directory
|
||||
@@ -36,7 +36,6 @@ This project is **not affiliated with Xteink**; it's built as a community projec
|
||||
- [x] Cover sleep screen
|
||||
- [x] Wifi book upload
|
||||
- [x] Wifi OTA updates
|
||||
- [x] KOReader Sync integration for cross-device reading progress
|
||||
- [x] Configurable font, layout, and display options
|
||||
- [ ] User provided fonts
|
||||
- [ ] Full UTF support
|
||||
@@ -44,8 +43,7 @@ This project is **not affiliated with Xteink**; it's built as a community projec
|
||||
|
||||
Multi-language support: Read EPUBs in various languages, including English, Spanish, French, German, Italian, Portuguese, Russian, Ukrainian, Polish, Swedish, Norwegian, [and more](./USER_GUIDE.md#supported-languages).
|
||||
|
||||
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint, including the
|
||||
[KOReader Sync quick setup](./USER_GUIDE.md#365-koreader-sync-quick-setup).
|
||||
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint.
|
||||
|
||||
For more details about the scope of the project, see the [SCOPE.md](SCOPE.md) document.
|
||||
|
||||
@@ -53,7 +51,7 @@ For more details about the scope of the project, see the [SCOPE.md](SCOPE.md) do
|
||||
|
||||
### Web (latest firmware)
|
||||
|
||||
1. Connect your Xteink X4 to your computer via USB-C and wake/unlock the device
|
||||
1. Connect your Xteink X4 to your computer via USB-C
|
||||
2. Go to https://xteink.dve.al/ and click "Flash CrossPoint firmware"
|
||||
|
||||
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
|
||||
@@ -68,24 +66,6 @@ back to the other partition using the "Swap boot partition" button here https://
|
||||
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
|
||||
back to the other partition using the "Swap boot partition" button here https://xteink.dve.al/debug.
|
||||
|
||||
### Command line (specific firmware version)
|
||||
|
||||
1. Install [`esptool`](https://github.com/espressif/esptool) :
|
||||
```bash
|
||||
pip install esptool
|
||||
```
|
||||
2. Download the `firmware.bin` file from the release of your choice via the [releases page](https://github.com/crosspoint-reader/crosspoint-reader/releases)
|
||||
3. Connect your Xteink X4 to your computer via USB-C.
|
||||
4. Note the device location. On Linux, run `dmesg` after connecting. On MacOS, run :
|
||||
```bash
|
||||
log stream --predicate 'subsystem == "com.apple.iokit"' --info
|
||||
```
|
||||
5. Flash the firmware :
|
||||
```bash
|
||||
esptool.py --chip esp32c3 --port /dev/ttyACM0 --baud 921600 write_flash 0x10000 /path/to/firmware.bin
|
||||
```
|
||||
Change `/dev/ttyACM0` to the device for your system.
|
||||
|
||||
### Manual
|
||||
|
||||
See [Development](#development) below.
|
||||
@@ -174,12 +154,10 @@ For more details on the internal file structures, see the [file formats document
|
||||
|
||||
Contributions are very welcome!
|
||||
|
||||
If you are new to the codebase, start with the [contributing docs](./docs/contributing/README.md).
|
||||
|
||||
If you're looking for a way to help out, take a look at the [ideas discussion board](https://github.com/crosspoint-reader/crosspoint-reader/discussions/categories/ideas).
|
||||
If there's something there you'd like to work on, leave a comment so that we can avoid duplicated effort.
|
||||
|
||||
Everyone here is a volunteer, so please be respectful and patient. For more details on our governance and community
|
||||
Everyone here is a volunteer, so please be respectful and patient. For more details on our goverance and community
|
||||
principles, please see [GOVERNANCE.md](GOVERNANCE.md).
|
||||
|
||||
### To submit a contribution:
|
||||
|
||||
@@ -25,14 +25,6 @@ usability over "swiss-army-knife" functionality.
|
||||
* **Library Management:** E.g. Simple, intuitive ways to organize and navigate a collection of books.
|
||||
* **Local Transfer:** E.g. Simple, "pull" based book loading via a basic web-server or public and widely-used standards.
|
||||
* **Language Support:** E.g. Support for multiple languages both in the reader and in the interfaces.
|
||||
* **Reference Tools:** E.g. Local dictionary lookup. Providing quick, offline definitions to enhance comprehension
|
||||
without breaking focus.
|
||||
* **Clock Display (device dependent):**
|
||||
|
||||
| Device | Scope |
|
||||
| -- | -- |
|
||||
| X3 | The X3 uses a dedicated DS3231 RTC, which maintains accurate time across sleep cycles and can be treated as a reliable wall clock. |
|
||||
| X4 | The X4 relies on the ESP32-C3's internal RTC, which drifts significantly during deep sleep. NTP sync could correct this, with an appropriate user experience around connecting to the internet on wake or on demand. This causes some tension with the **Active Connectivity** section below, so please open a discussion about this UX if it's a feature you would find useful. |
|
||||
|
||||
### Out-of-Scope
|
||||
|
||||
@@ -42,14 +34,8 @@ usability over "swiss-army-knife" functionality.
|
||||
* **Active Connectivity:** No RSS readers, News aggregators, or Web browsers. Background Wi-Fi tasks drain the battery
|
||||
and complicate the single-core CPU's execution.
|
||||
* **Media Playback:** No Audio players or Audio-books.
|
||||
* **Complex Annotation:** No typed out notes. These features are better suited for devices with better input
|
||||
capabilities and more powerful chips.
|
||||
|
||||
### In-scope — Technically Unsupported
|
||||
|
||||
*These features align with CrossPoint's goals but are impractical on the current hardware or produce poor UX.*
|
||||
|
||||
* **PDF Rendering:** PDFs are fixed-layout documents, so rendering them requires displaying pages as images rather than reflowable text — resulting in constant panning and zooming that makes for a poor reading experience on e-ink.
|
||||
* **Complex Reader Features:** No highlighting, notes, or dictionary lookup. These features are better suited for
|
||||
devices with better input capabilities and more powerful chips.
|
||||
|
||||
## 3. Idea Evaluation
|
||||
|
||||
|
||||
+69
-232
@@ -10,18 +10,12 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
|
||||
- [First Launch](#first-launch)
|
||||
- [3. Screens](#3-screens)
|
||||
- [3.1 Home Screen](#31-home-screen)
|
||||
- [3.2 Reading Mode](#32-reading-mode)
|
||||
- [3.3 Browse Files Screen](#33-browse-files-screen)
|
||||
- [3.4 Recent Books Screen](#34-recent-books-screen)
|
||||
- [3.5 File Transfer Screen](#35-file-transfer-screen)
|
||||
- [3.5.1 Calibre Wireless Transfers](#351-calibre-wireless-transfers)
|
||||
- [3.6 Settings](#36-settings)
|
||||
- [3.6.1 Display](#361-display)
|
||||
- [3.6.2 Reader](#362-reader)
|
||||
- [3.6.3 Controls](#363-controls)
|
||||
- [3.6.4 System](#364-system)
|
||||
- [3.6.5 KOReader Sync Quick Setup](#365-koreader-sync-quick-setup)
|
||||
- [3.7 Sleep Screen](#37-sleep-screen)
|
||||
- [3.2 Book Selection](#32-book-selection)
|
||||
- [3.3 Reading Mode](#33-reading-mode)
|
||||
- [3.4 File Upload Screen](#34-file-upload-screen)
|
||||
- [3.4.1 Calibre Wireless Transfers](#341-calibre-wireless-transfers)
|
||||
- [3.5 Settings](#35-settings)
|
||||
- [3.6 Sleep Screen](#36-sleep-screen)
|
||||
- [4. Reading Mode](#4-reading-mode)
|
||||
- [Page Turning](#page-turning)
|
||||
- [Chapter Navigation](#chapter-navigation)
|
||||
@@ -34,7 +28,7 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
|
||||
|
||||
## 1. Hardware Overview
|
||||
|
||||
The device utilises the standard buttons on the Xteink X4 (in the same layout as the manufacturer firmware, by default):
|
||||
The device utilises the standard buttons on the Xtink X4 (in the same layout as the manufacturer firmware, by default):
|
||||
|
||||
### Button Layout
|
||||
| Location | Buttons |
|
||||
@@ -42,12 +36,7 @@ The device utilises the standard buttons on the Xteink X4 (in the same layout as
|
||||
| **Bottom Edge** | **Back**, **Confirm**, **Left**, **Right** |
|
||||
| **Right Side** | **Power**, **Volume Up**, **Volume Down**, **Reset** |
|
||||
|
||||
Button layout can be customized in the **[Controls Settings](#363-controls)**.
|
||||
|
||||
### Taking a Screenshot
|
||||
When the Power Button and Volume Down button are pressed at the same time, it will take a screenshot and save it in the folder `screenshots/`.
|
||||
|
||||
Alternatively, while reading a book, press the **Confirm** button to open the reader menu and select **Take screenshot**.
|
||||
Button layout can be customized in **[Settings](#35-settings)**.
|
||||
|
||||
---
|
||||
|
||||
@@ -56,9 +45,9 @@ Alternatively, while reading a book, press the **Confirm** button to open the re
|
||||
### Power On / Off
|
||||
|
||||
To turn the device on or off, **press and hold the Power button for approximately half a second**.
|
||||
In the **[Controls Settings](#363-controls)** you can configure the power button to turn the device off with a short press instead of a long one.
|
||||
In **[Settings](#35-settings)** you can configure the power button to turn the device off with a short press instead of a long one.
|
||||
|
||||
To reboot the device (for example after a firmware update or if it's frozen), press and release the Reset button, and then quickly press and hold the Power button for a few seconds.
|
||||
To reboot the device (for example if it's frozen, or after a firmware update), press and release the Reset button, and then quickly press and hold the Power button for a few seconds.
|
||||
|
||||
### First Launch
|
||||
|
||||
@@ -73,34 +62,29 @@ Upon turning the device on for the first time, you will be placed on the **[Home
|
||||
|
||||
### 3.1 Home Screen
|
||||
|
||||
The Home screen is the main entry point to the firmware. From here you can navigate to **[Reading Mode](#4-reading-mode)** with the most recently read book, the **[Browse Files](#33-browse-files-screen)** screen, the **[Recent Books](#34-recent-books-screen)** screen, the **[File Transfer](#35-file-transfer-screen)** screen, or **[Settings](#36-settings)**.
|
||||
The Home Screen is the main entry point to the firmware. From here you can navigate to **[Reading Mode](#4-reading-mode)** with the most recently read book, **[Book Selection](#32-book-selection)**, **[Settings](#35-settings)**, or the **[File Upload](#34-file-upload-screen)** screen.
|
||||
|
||||
### 3.2 Reading Mode
|
||||
### 3.2 Book Selection
|
||||
|
||||
The Book Selection 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. You can also long-press these buttons to scroll a full page up or down.
|
||||
* **Open Selection:** Press **Confirm** to open a folder or read a selected book.
|
||||
|
||||
### 3.3 Reading Mode
|
||||
|
||||
See [Reading Mode](#4-reading-mode) below for more information.
|
||||
|
||||
### 3.3 Browse Files Screen
|
||||
### 3.4 File Upload Screen
|
||||
|
||||
The Browse Files screen acts as a file and folder 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. You can also long-press these buttons to scroll a full page up or down.
|
||||
* **Open Selection:** Press **Confirm** to open a folder or read a selected book.
|
||||
* **Delete Files:** Hold and release **Confirm** to delete the selected file. You will be given an option to either confirm or cancel deletion. Folder deletion is not supported.
|
||||
|
||||
### 3.4 Recent Books Screen
|
||||
|
||||
The Recent Books screen lists the most recently opened books in a chronological view, displaying title and author.
|
||||
|
||||
### 3.5 File Transfer Screen
|
||||
|
||||
The File Transfer screen allows you to upload new e-books to the device. When you enter the screen, you'll be prompted with a WiFi selection dialog and then your X4 will start hosting a web server.
|
||||
The File Upload screen allows you to upload new e-books to the device. When you enter the screen, you'll be prompted with a WiFi selection dialog and then your X4 will start hosting a web server.
|
||||
|
||||
See the [webserver docs](./docs/webserver.md) for more information on how to connect to the web server and upload files.
|
||||
|
||||
> [!TIP]
|
||||
> Advanced users can also manage files programmatically or via the command line using `curl`. See the [webserver docs](./docs/webserver.md) for details.
|
||||
|
||||
### 3.5.1 Calibre Wireless Transfers
|
||||
### 3.4.1 Calibre Wireless Transfers
|
||||
|
||||
CrossPoint supports sending books from Calibre using the CrossPoint Reader device plugin.
|
||||
|
||||
@@ -112,26 +96,23 @@ CrossPoint supports sending books from Calibre using the CrossPoint Reader devic
|
||||
3. Make sure your computer is on the same WiFi network.
|
||||
4. In Calibre, click "Send to device" to transfer books.
|
||||
|
||||
### 3.6 Settings
|
||||
### 3.5 Settings
|
||||
|
||||
The Settings screen allows you to configure the device's behavior. There are a few settings you can adjust:
|
||||
|
||||
#### 3.6.1 Display
|
||||
|
||||
- **Sleep Screen**: Which sleep screen to display when the device sleeps:
|
||||
- "Dark" (default) - The default dark Crosspoint logo sleep screen
|
||||
- "Light" - The same default sleep screen, on a white background
|
||||
- "Custom" - Custom images from the SD card; see [Sleep Screen](#37-sleep-screen) below for more information
|
||||
- "Custom" - Custom images from the SD card; see [Sleep Screen](#36-sleep-screen) below for more information
|
||||
- "Cover" - The book cover image (Note: this is experimental and may not work as expected)
|
||||
- "None" - A blank screen
|
||||
- "Cover + Custom" - The book cover image, falls back to "Custom" behavior
|
||||
- "Cover + Custom" - The book cover image, fallbacks to "Custom" behavior
|
||||
- **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected:
|
||||
- "Fit" (default) - Scale the image down to fit centered on the screen, padding with white borders as necessary
|
||||
- "Crop" - Scale the image down and crop as necessary to try to fill the screen (Note: this is experimental and may not work as expected)
|
||||
- **Sleep Screen Cover Filter**: What filter will be applied to the book cover when "Cover" sleep screen is selected:
|
||||
- "Crop" - Scale the image down and crop as necessary to try to to fill the screen (Note: this is experimental and may not work as expected)
|
||||
- **Sleep Screen Cover Filter**: What filter will be applied to the book cover when "Cover" sleep screen is selected
|
||||
- "None" (default) - The cover image will be converted to a grayscale image and displayed as it is
|
||||
- "Contrast" - The image will be displayed as a black & white image without grayscale conversion
|
||||
- "Inverted" - The image will be inverted as in white & black and will be displayed without grayscale conversion
|
||||
- "Inverted" - The image will be inverted as in white&black and will be displayed without grayscale conversion
|
||||
- **Status Bar**: Configure the status bar displayed while reading:
|
||||
- "None" - No status bar
|
||||
- "No Progress" - Show status bar without reading progress
|
||||
@@ -139,205 +120,61 @@ The Settings screen allows you to configure the device's behavior. There are a f
|
||||
- "Full w/ Book Bar" - Show status bar with book progress (as bar)
|
||||
- "Book Bar Only" - Show book progress (as bar)
|
||||
- "Full w/ Chapter Bar" - Show status bar with chapter progress (as bar)
|
||||
- **Hide Battery %**: Configure where to suppress the battery percentage display in the status bar; the battery icon will still be shown:
|
||||
- "Never" (default) - Always show battery percentage
|
||||
- **Hide Battery %**: Configure where to suppress the battery pecentage display in the status bar; the battery icon will still be shown:
|
||||
- "Never" - Always show battery percentage (default)
|
||||
- "In Reader" - Show battery percentage everywhere except in reading mode
|
||||
- "Always" - Always hide battery percentage
|
||||
- **Refresh Frequency**: Set how often the screen does a full refresh while reading to reduce ghosting; options are every 1, 5, 10, 15, or 30 pages.
|
||||
|
||||
- **UI Theme**: Set which UI theme to use:
|
||||
- "Classic" - The original Crosspoint theme
|
||||
- "Lyra" - The new theme for Crosspoint featuring rounded elements and menu icons
|
||||
- "Lyra Extended" - Lyra, but displays 3 books instead of 1 on the **[Home Screen](#31-home-screen)**
|
||||
- **Sunlight Fading Fix**: Configure whether to enable a software-fix for the issue where white X4 models may fade when used in direct sunlight:
|
||||
- "OFF" (default) - Disable the fix
|
||||
- "ON" - Enable the fix
|
||||
|
||||
#### 3.6.2 Reader
|
||||
- **Reader Font Family**: Choose the font used for reading:
|
||||
- "Bookerly" (default) - Amazon's reading font
|
||||
- "Noto Sans" - Google's sans-serif font
|
||||
- "Open Dyslexic" - Font designed for readers with dyslexia
|
||||
- **Reader Font Size**: Adjust the text size for reading; options are "Small", "Medium" (default), "Large", or "X Large".
|
||||
|
||||
- **Reader Line Spacing**: Adjust the spacing between lines; options are "Tight", "Normal" (default), or "Wide".
|
||||
- **Reader Screen Margin**: Controls the screen margins in Reading Mode between 5 and 40 pixels in 5-pixel increments.
|
||||
- **Reader Paragraph Alignment**: Set the alignment of paragraphs; options are "Justified" (default), "Left", "Center", or "Right".
|
||||
- **Embedded Style**: Whether to use the EPUB file's embedded HTML and CSS stylisation and formatting; options are "ON" or "OFF".
|
||||
- **Hyphenation**: Whether to hyphenate text in Reading Mode; options are "ON" or "OFF".
|
||||
- **Extra Paragraph Spacing**: If enabled, vertical space will be added between paragraphs in the book. If disabled, paragraphs will not have vertical space between them, but will have first-line indentation.
|
||||
- **Text Anti-Aliasing**: Whether to show smooth grey edges (anti-aliasing) on text in reading mode. Note this slows down page turns slightly.
|
||||
- **Short Power Button Click**: Controls the effect of a short click of the power button:
|
||||
- "Ignore" - Require a long press to turn off the device
|
||||
- "Sleep" - A short press powers the device off
|
||||
- "Page Turn" - A short press in reading mode turns to the next page; a long press turns the device off
|
||||
- **Reading Orientation**: Set the screen orientation for reading EPUB files:
|
||||
- "Portrait" (default) - Standard portrait orientation
|
||||
- "Landscape CW" - Landscape, rotated clockwise
|
||||
- "Inverted" - Portrait, upside down
|
||||
- "Landscape CCW" - Landscape, rotated counter-clockwise
|
||||
- **Extra Paragraph Spacing**: Set how to handle paragraph breaks:
|
||||
- "ON" - Vertical space will be added between paragraphs in Reading Mode
|
||||
- "OFF" - Paragraphs will not have vertical space added, but will have first-line indentation
|
||||
- **Text Anti-Aliasing**: Whether to show smooth grey edges (anti-aliasing) on text in reading mode. Note this slows down page turns slightly.
|
||||
|
||||
#### 3.6.3 Controls
|
||||
|
||||
- **Remap Front Buttons**: A menu for customising the function of each bottom edge button.
|
||||
- **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from "Prev/Next" (default) to "Next/Prev". This change is only in effect when reading.
|
||||
|
||||
- **Long-press Chapter Skip**: Set whether long-pressing page turn buttons skips to the next/previous chapter:
|
||||
- **Front Button Layout**: Configure the order of the bottom edge buttons:
|
||||
- Back, Confirm, Left, Right (default)
|
||||
- Left, Right, Back, Confirm
|
||||
- Left, Back, Confirm, Right
|
||||
- Back, Confirm, Right, Left
|
||||
- **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
|
||||
- **Long-press Chapter Skip**: Set whether long-pressing page turn buttons skip to the next/previous chapter.
|
||||
- "Chapter Skip" (default) - Long-pressing skips to next/previous chapter
|
||||
- "Page Scroll" - Long-pressing scrolls a page up/down
|
||||
- **Short Power Button Click**: Controls the effect of a short click of the power button:
|
||||
- "Ignore" (default) - Require a long press to turn off the device
|
||||
- "Sleep" - A short press puts the device into sleep mode
|
||||
- "Page Turn" - A short press in reading mode turns to the next page; a long press turns the device off
|
||||
|
||||
#### 3.6.4 System
|
||||
|
||||
- **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep; options are 1, 5, 10 (default), 15 or 30 minutes.
|
||||
|
||||
- **WiFi Networks**: Connect to WiFi networks for file transfers and firmware updates.
|
||||
- **KOReader Sync**: Options for setting up KOReader for syncing book progress.
|
||||
- Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
|
||||
- **Reader Font Family**: Choose the font used for reading:
|
||||
- "Bookerly" (default) - Amazon's reading font
|
||||
- "Noto Sans" - Google's sans-serif font
|
||||
- "Open Dyslexic" - Font designed for readers with dyslexia
|
||||
- **Reader Font Size**: Adjust the text size for reading; options are "Small", "Medium", "Large", or "X Large".
|
||||
- **Reader Line Spacing**: Adjust the spacing between lines; options are "Tight", "Normal", or "Wide".
|
||||
- **Reader Screen Margin**: Controls the screen margins in reader mode between 5 and 40 pixels in 5 pixel increments.
|
||||
- **Reader Paragraph Alignment**: Set the alignment of paragraphs; options are "Justified" (default), "Left", "Center", or "Right".
|
||||
- **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep.
|
||||
- **Refresh Frequency**: Set how often the screen does a full refresh while reading to reduce ghosting.
|
||||
- **Sunlight Fading Fix**: Configure whether to enable a software-fix for the issue where white X4 models may fade when used in direct sunlight
|
||||
- "OFF" (default) - Disable the fix
|
||||
- "ON" - Enable the fix
|
||||
- **OPDS Browser**: Configure OPDS server settings for browsing and downloading books. Set the server URL (for Calibre Content Server, add `/opds` to the end), and optionally configure username and password for servers requiring authentication. Note: Only HTTP Basic authentication is supported. If using Calibre Content Server with authentication enabled, you must set it to use Basic authentication instead of the default Digest authentication.
|
||||
- **Clear Reading Cache**: Clear the internal SD card cache.
|
||||
- **Check for updates**: Check for Crosspoint firmware updates over WiFi.
|
||||
- **Language**: Set the system language (see **[Supported Languages](#supported-languages)** for more information).
|
||||
- **Check for updates**: Check for firmware updates over WiFi.
|
||||
|
||||
#### 3.6.5 KOReader Sync Quick Setup
|
||||
### 3.6 Sleep Screen
|
||||
|
||||
CrossPoint can sync reading progress with KOReader-compatible sync servers.
|
||||
It also interoperates with KOReader apps/devices when they use the same server and credentials.
|
||||
You can customize the sleep screen by placing custom images in specific locations on the SD card:
|
||||
|
||||
##### Option A: Free Public Server (`sync.koreader.rocks`)
|
||||
|
||||
1. Register a user once (only if needed):
|
||||
|
||||
```bash
|
||||
USERNAME="user"
|
||||
PASSWORD="pass"
|
||||
PASSWORD_MD5="$(printf '%s' "$PASSWORD" | openssl md5 | awk '{print $2}')"
|
||||
|
||||
curl -i "https://sync.koreader.rocks/users/create" \
|
||||
-H "Accept: application/vnd.koreader.v1+json" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD_MD5\"}"
|
||||
```
|
||||
|
||||
Already have KOReader Sync credentials? Skip registration; basic sync only requires using the same existing username/password on all devices.
|
||||
|
||||
When this returns `HTTP 402` with `{"code":2002,"message":"Username is already registered."}`, pick a different username or use that existing account.
|
||||
|
||||
2. On each CrossPoint device:
|
||||
- Go to **Settings -> System -> KOReader Sync**.
|
||||
- Set **Username** and **Password** (enter the plain password; CrossPoint computes MD5 internally, and use the same values on all devices).
|
||||
- Set **Sync Server URL** to `https://sync.koreader.rocks`, or leave it empty (both use the same default KOReader sync server).
|
||||
- Run **Authenticate**.
|
||||
|
||||
3. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**.
|
||||
- Choose **Apply Remote** to jump to remote progress.
|
||||
- Choose **Upload Local** to push current progress.
|
||||
|
||||
##### Option B: Self-Hosted Server (Docker Compose)
|
||||
|
||||
1. Start a sync server:
|
||||
|
||||
```bash
|
||||
mkdir -p kosync-quickstart
|
||||
cd kosync-quickstart
|
||||
|
||||
cat > compose.yaml <<'YAML'
|
||||
services:
|
||||
kosync:
|
||||
image: koreader/kosync:latest
|
||||
ports:
|
||||
- "7200:7200"
|
||||
- "17200:17200"
|
||||
volumes:
|
||||
- ./data/redis:/var/lib/redis
|
||||
environment:
|
||||
- ENABLE_USER_REGISTRATION=true
|
||||
restart: unless-stopped
|
||||
YAML
|
||||
|
||||
# Docker
|
||||
docker compose up -d
|
||||
|
||||
# Podman (alternative)
|
||||
podman compose up -d
|
||||
```
|
||||
- **Single Image:** Place a file named `sleep.bmp` in the root directory.
|
||||
- **Multiple Images:** Create a `sleep` directory in the root of the SD card and place any number of `.bmp` images inside. If images are found in this directory, they will take priority over the `sleep.bmp` file, and one will be randomly selected each time the device sleeps.
|
||||
|
||||
> [!NOTE]
|
||||
> `ENABLE_USER_REGISTRATION=true` is convenient for first setup. After creating your users, set it to `false` (or remove it) to avoid unexpected registrations.
|
||||
|
||||
2. Verify the server:
|
||||
|
||||
```bash
|
||||
curl -H "Accept: application/vnd.koreader.v1+json" "http://<server-ip>:17200/healthcheck"
|
||||
# Expected: {"state":"OK"}
|
||||
```
|
||||
|
||||
3. Register a user once.
|
||||
CrossPoint authenticates against KOReader Sync (`koreader/kosync`) using an MD5 key, so register using the MD5 of your password:
|
||||
|
||||
> [!WARNING]
|
||||
> Sending a reusable MD5-derived password over plain HTTP is insecure.
|
||||
> Create unique sync-only credentials and do not reuse main account passwords.
|
||||
> Prefer `https://<server-ip>:7200` whenever traffic leaves a fully trusted LAN or when using untrusted networks.
|
||||
> Use `curl -k` only for self-signed certificate testing.
|
||||
|
||||
```bash
|
||||
USERNAME="user"
|
||||
PASSWORD="pass"
|
||||
PASSWORD_MD5="$(printf '%s' "$PASSWORD" | openssl md5 | awk '{print $2}')"
|
||||
|
||||
curl -i "http://<server-ip>:17200/users/create" \
|
||||
-H "Accept: application/vnd.koreader.v1+json" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD_MD5\"}"
|
||||
```
|
||||
|
||||
If this returns `HTTP 402` with `{"code":2002,"message":"Username is already registered."}`, the account already exists.
|
||||
|
||||
4. On each CrossPoint device:
|
||||
- Go to **Settings -> System -> KOReader Sync**.
|
||||
- Set **Username** and **Password** (enter the plain password; CrossPoint computes MD5 internally, and use the same values on all devices).
|
||||
- Set **Sync Server URL** to `http://<server-ip>:17200`.
|
||||
- Run **Authenticate**.
|
||||
|
||||
If you use the HTTPS listener, use `https://<server-ip>:7200` (`curl -k` only for self-signed certificate testing).
|
||||
|
||||
5. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**.
|
||||
- Choose **Apply Remote** to jump to remote progress.
|
||||
- Choose **Upload Local** to push current progress.
|
||||
|
||||
### 3.7 Sleep Screen
|
||||
|
||||
The **Sleep Screen** setting controls what is displayed when the device goes to sleep:
|
||||
|
||||
| Mode | Behavior |
|
||||
|------|----------|
|
||||
| **Dark** (default) | The CrossPoint logo on a dark background. |
|
||||
| **Light** | The CrossPoint logo on a white background. |
|
||||
| **Custom** | A custom image from the SD card (see below). Falls back to **Dark** if no custom image is found. |
|
||||
| **Cover** | The cover of the currently open book. Falls back to **Dark** if no book is open. |
|
||||
| **Cover + Custom** | The cover of the currently open book. Falls back to **Custom** behavior if no book is open. |
|
||||
| **None** | A blank screen. |
|
||||
|
||||
#### Cover settings
|
||||
|
||||
When using **Cover** or **Cover + Custom**, two additional settings apply:
|
||||
|
||||
- **Sleep Screen Cover Mode**: **Fit** (scale to fit, white borders) or **Crop** (scale and crop to fill the screen).
|
||||
- **Sleep Screen Cover Filter**: **None** (grayscale), **Contrast** (black & white), or **Inverted** (inverted black & white).
|
||||
|
||||
#### Custom images
|
||||
|
||||
To use custom sleep images, set the sleep screen mode to **Custom** or **Cover + Custom**, then place images on the SD card:
|
||||
|
||||
- **Multiple Images (recommended):** Create a `.sleep` directory in the root of the SD card and place any number of `.bmp` images inside. One will be randomly selected each time the device sleeps. (A directory named `sleep` is also accepted as a fallback.)
|
||||
- **Single Image:** Place a file named `sleep.bmp` in the root directory. This is used as a fallback if no valid images are found in the `.sleep`/`sleep` directory.
|
||||
> You'll need to set the **Sleep Screen** setting to **Custom** in order to use these images.
|
||||
|
||||
> [!TIP]
|
||||
> For best results:
|
||||
> - Use uncompressed BMP files with 24-bit color depth
|
||||
> - X4: Use a resolution of 480x800 pixels to match the device's screen resolution.
|
||||
> - X3: Use a resolution of 528x792 pixels to match the device's screen resolution.
|
||||
> - Use a resolution of 480x800 pixels to match the device's screen resolution.
|
||||
|
||||
---
|
||||
|
||||
@@ -351,7 +188,7 @@ Once you have opened a book, the button layout changes to facilitate reading.
|
||||
| **Previous Page** | Press **Left** _or_ **Volume Up** |
|
||||
| **Next Page** | Press **Right** _or_ **Volume Down** |
|
||||
|
||||
The role of the volume (side) buttons can be swapped in the **[Controls Settings](#363-controls)**.
|
||||
The role of the volume (side) buttons can be swapped in **[Settings](#35-settings)**.
|
||||
|
||||
If the **Short Power Button Click** setting is set to "Page Turn", you can also turn to the next page by briefly pressing the Power button.
|
||||
|
||||
@@ -359,13 +196,13 @@ If the **Short Power Button Click** setting is set to "Page Turn", you can also
|
||||
* **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.
|
||||
|
||||
This feature can be disabled in the **[Controls Settings](#363-controls)** to help avoid changing chapters by mistake.
|
||||
This feature can be disabled in **[Settings](#35-settings)** to help avoid changing chapters by mistake.
|
||||
|
||||
|
||||
### System Navigation
|
||||
* **Return to Home:** Press the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.
|
||||
* **Return to Browse Files:** Press and hold the **Back** button to close the book and return to the **[Browse Files](#33-browse-files-screen)** screen.
|
||||
* **Chapter Menu:** Press **Confirm** to open the **[Table of Contents/Chapter Selection](#5-chapter-selection-screen)** screen.
|
||||
* **Return to Book Selection:** Press **Back** to close the book and return to the **[Book Selection](#32-book-selection)** screen.
|
||||
* **Return to Home:** Press and **hold** the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.
|
||||
* **Chapter Menu:** Press **Confirm** to open the **[Table of Contents/Chapter Selection](#5-chapter-selection-screen)**.
|
||||
|
||||
### Supported Languages
|
||||
|
||||
|
||||
+3
-27
@@ -1,33 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Check if clang-format is available and pick the preferred binary.
|
||||
if command -v clang-format-21 >/dev/null 2>&1; then
|
||||
CLANG_FORMAT_BIN="clang-format-21"
|
||||
elif command -v clang-format >/dev/null 2>&1; then
|
||||
CLANG_FORMAT_BIN="clang-format"
|
||||
else
|
||||
printf "'clang-format' not found in current environment\n"
|
||||
printf "Install clang-format-21 (recommended), clang, clang-tools, or clang-format depending on your distro/os and tooling requirements\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
#!/bin/bash
|
||||
|
||||
GIT_LS_FILES_FLAGS=""
|
||||
if [[ "${1:-}" == "-g" ]]; then
|
||||
if [[ "$1" == "-g" ]]; then
|
||||
GIT_LS_FILES_FLAGS="--modified"
|
||||
fi
|
||||
|
||||
CLANG_FORMAT_VERSION_RAW="$(${CLANG_FORMAT_BIN} --version)"
|
||||
CLANG_FORMAT_MAJOR="$(printf '%s\n' "${CLANG_FORMAT_VERSION_RAW}" | grep -oE '[0-9]+' | head -n1)"
|
||||
|
||||
if [[ -z "${CLANG_FORMAT_MAJOR}" || "${CLANG_FORMAT_MAJOR}" -lt 21 ]]; then
|
||||
echo "Error: ${CLANG_FORMAT_BIN} is too old: ${CLANG_FORMAT_VERSION_RAW}"
|
||||
echo "This repository's .clang-format requires clang-format 21 or newer."
|
||||
echo "Install clang-format-21 and rerun ./bin/clang-format-fix"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Main Logic ---
|
||||
|
||||
# Format all files (or only modified files if -g is passed)
|
||||
@@ -41,5 +18,4 @@ git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \
|
||||
| grep -E '\.(c|cpp|h|hpp)$' \
|
||||
| grep -v -E '^lib/EpdFont/builtinFonts/' \
|
||||
| grep -v -E '^lib/Epub/Epub/hyphenation/generated/' \
|
||||
| grep -v -E '^lib/uzlib/' \
|
||||
| xargs -r "${CLANG_FORMAT_BIN}" -style=file -i
|
||||
| xargs -r clang-format -style=file -i
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Runs clang-format -i on project *.cpp and *.h files.
|
||||
|
||||
.DESCRIPTION
|
||||
Formats all C/C++ source and header files in the repository, excluding
|
||||
generated, vendored, and build directories (open-x4-sdk, builtinFonts,
|
||||
hyphenation tries, uzlib, .pio, *.generated.h).
|
||||
|
||||
The clang-format binary path is resolved once and cached in
|
||||
bin/clang-format-fix.local. On first run it checks a default path,
|
||||
then PATH, then common install locations. Edit the .local file to
|
||||
override manually.
|
||||
|
||||
.PARAMETER g
|
||||
Format only git-modified files (git diff --name-only HEAD) instead of
|
||||
the full tree.
|
||||
|
||||
.PARAMETER h
|
||||
Show this help text.
|
||||
|
||||
.EXAMPLE
|
||||
.\clang-format-fix.ps1
|
||||
Format all files.
|
||||
|
||||
.EXAMPLE
|
||||
.\clang-format-fix.ps1 -g
|
||||
Format only git-modified files.
|
||||
#>
|
||||
|
||||
param(
|
||||
[switch]$g,
|
||||
[switch]$h
|
||||
)
|
||||
|
||||
if ($h) {
|
||||
Get-Help $PSCommandPath -Detailed
|
||||
return
|
||||
}
|
||||
|
||||
$repoRoot = (Resolve-Path "$PSScriptRoot\..").Path
|
||||
$configFile = Join-Path $PSScriptRoot 'clang-format-fix.local'
|
||||
$defaultPath = 'C:\Program Files\LLVM\bin\clang-format.exe'
|
||||
|
||||
$candidatePaths = @(
|
||||
'C:\Program Files\LLVM\bin\clang-format.exe'
|
||||
'C:\Program Files (x86)\LLVM\bin\clang-format.exe'
|
||||
'C:\msys64\ucrt64\bin\clang-format.exe'
|
||||
'C:\msys64\mingw64\bin\clang-format.exe'
|
||||
"$env:LOCALAPPDATA\LLVM\bin\clang-format.exe"
|
||||
)
|
||||
|
||||
function Find-ClangFormat {
|
||||
# Try PATH first
|
||||
$inPath = Get-Command clang-format -ErrorAction SilentlyContinue
|
||||
if ($inPath) { return $inPath.Source }
|
||||
|
||||
# Try candidate paths
|
||||
foreach ($p in $candidatePaths) {
|
||||
if (Test-Path $p) { return $p }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Resolve-ClangFormat {
|
||||
# 1. Read from config if present
|
||||
if (Test-Path $configFile) {
|
||||
$saved = (Get-Content $configFile -Raw).Trim()
|
||||
if ($saved -and (Test-Path $saved)) { return $saved }
|
||||
Write-Host "Configured path no longer valid: $saved"
|
||||
}
|
||||
|
||||
# 2. Check default
|
||||
if (Test-Path $defaultPath) {
|
||||
$defaultPath | Set-Content $configFile
|
||||
Write-Host "Saved clang-format path to $configFile"
|
||||
return $defaultPath
|
||||
}
|
||||
|
||||
# 3. Search PATH and candidate locations
|
||||
$found = Find-ClangFormat
|
||||
if ($found) {
|
||||
$found | Set-Content $configFile
|
||||
Write-Host "Found clang-format at $found - saved to $configFile"
|
||||
return $found
|
||||
}
|
||||
|
||||
Write-Error "clang-format not found. Install LLVM or add clang-format to PATH."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$clangFormat = Resolve-ClangFormat
|
||||
|
||||
$exclude = @(
|
||||
'open-x4-sdk'
|
||||
'lib\EpdFont\builtinFonts'
|
||||
'lib\Epub\Epub\hyphenation\generated'
|
||||
'lib\uzlib'
|
||||
'.pio'
|
||||
'.venv'
|
||||
)
|
||||
|
||||
function Test-Excluded($fullPath) {
|
||||
foreach ($ex in $exclude) {
|
||||
if ($fullPath -like "*\$ex\*") { return $true }
|
||||
}
|
||||
if ($fullPath -like '*.generated.h') { return $true }
|
||||
return $false
|
||||
}
|
||||
|
||||
if ($g) {
|
||||
# Only git-modified *.cpp / *.h files
|
||||
# Covers both staged and unstaged changes
|
||||
$files = @(git -C $repoRoot diff --name-only HEAD) +
|
||||
@(git -C $repoRoot diff --name-only --cached) |
|
||||
Sort-Object -Unique |
|
||||
Where-Object { $_ -match '\.(cpp|h)$' } |
|
||||
ForEach-Object { Get-Item (Join-Path $repoRoot $_) -ErrorAction SilentlyContinue } |
|
||||
Where-Object { $_ -and -not (Test-Excluded $_.FullName) }
|
||||
} else {
|
||||
$files = Get-ChildItem -Path $repoRoot -Recurse -Include *.cpp, *.h -File |
|
||||
Where-Object { -not (Test-Excluded $_.FullName) }
|
||||
}
|
||||
|
||||
$files = @($files)
|
||||
|
||||
if ($files.Count -eq 0) {
|
||||
Write-Host 'No files to format.'
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "Formatting $($files.Count) files..."
|
||||
$i = 0
|
||||
$changed = 0
|
||||
$failures = 0
|
||||
foreach ($f in $files) {
|
||||
$i++
|
||||
$rel = $f.FullName.Substring($repoRoot.Length + 1)
|
||||
$hashBefore = (Get-FileHash $f.FullName -Algorithm MD5).Hash
|
||||
& $clangFormat -i $f.FullName
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
$failures++
|
||||
Write-Host " [$i/$($files.Count)] $rel (FAILED, exit code $LASTEXITCODE)"
|
||||
continue
|
||||
}
|
||||
$hashAfter = (Get-FileHash $f.FullName -Algorithm MD5).Hash
|
||||
if ($hashBefore -ne $hashAfter) {
|
||||
$changed++
|
||||
Write-Host " [$i/$($files.Count)] $rel (changed)"
|
||||
} else {
|
||||
Write-Host " [$i/$($files.Count)] $rel"
|
||||
}
|
||||
}
|
||||
Write-Host "Done. $changed/$($files.Count) files changed, $failures failed."
|
||||
if ($failures -gt 0) { exit 1 }
|
||||
@@ -1,472 +0,0 @@
|
||||
# Activity & ActivityManager Migration Guide
|
||||
|
||||
This document explains the refactoring from the original per-activity render task model to the centralized `ActivityManager` introduced in [PR #1016](https://github.com/crosspoint-reader/crosspoint-reader/pull/1016). It covers the architectural differences, what changed for activity authors, and the FreeRTOS task and locking model that underpins the system.
|
||||
|
||||
## Overview of Changes
|
||||
|
||||
| Aspect | Old Model | New Model |
|
||||
|--------|-----------|-----------|
|
||||
| Render task | One per activity (8KB stack each) | Single shared task in `ActivityManager` |
|
||||
| Render mutex | Per-activity `renderingMutex` | Single global mutex in `ActivityManager` |
|
||||
| `RenderLock` | Inner class of `Activity` | Standalone class, acquires global mutex |
|
||||
| Subactivities | `ActivityWithSubactivity` base class | Activity stack managed by `ActivityManager` |
|
||||
| Navigation | Free functions in `main.cpp` | `activityManager.goHome()`, `goToReader()`, etc. |
|
||||
| Subactivity results | Callback lambdas stored in parent | `startActivityForResult()` / `setResult()` / `finish()` |
|
||||
| `requestUpdate()` | Notifies activity's own render task | Delegates to `ActivityManager` (immediate or deferred) |
|
||||
|
||||
## Architecture
|
||||
|
||||
### Old Model: Per-Activity Render Tasks
|
||||
|
||||
Each activity created its own FreeRTOS render task on entry and destroyed it on exit:
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Main Task (Arduino loop) │
|
||||
│ ┌───────────────────────────────────────────────────┐ │
|
||||
│ │ currentActivity->loop() │ │
|
||||
│ │ ├── handle input │ │
|
||||
│ │ ├── update state (under RenderLock) │ │
|
||||
│ │ └── requestUpdate() ──notify──► Render Task │ │
|
||||
│ │ (per-activity)│ │
|
||||
│ │ 8KB stack │ │
|
||||
│ │ owns mutex │ │
|
||||
│ └───────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ActivityWithSubactivity: │
|
||||
│ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Parent │────►│ SubActivity │ │
|
||||
│ │ (has render │ │ (has own │ │
|
||||
│ │ task) │ │ render task) │ │
|
||||
│ └──────────────┘ └──────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Problems with this approach:
|
||||
|
||||
- **8KB per render task**: Each activity allocated an 8KB FreeRTOS stack for its render task, even though only one renders at a time
|
||||
- **Dangerous deletion patterns**: `exitActivity()` + `enterNewActivity()` in callbacks led to `delete this` situations where the caller was destroyed while its code was still on the stack
|
||||
- **Subactivity coupling**: Parents stored callbacks to child results, creating tight coupling and lifetime hazards
|
||||
|
||||
### New Model: Centralized ActivityManager
|
||||
|
||||
A single `ActivityManager` owns the render task and manages an activity stack:
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ Main Task (Arduino loop) │
|
||||
│ │
|
||||
│ activityManager.loop() │
|
||||
│ │ │
|
||||
│ ├── currentActivity->loop() │
|
||||
│ │ ├── handle input │
|
||||
│ │ ├── update state (under RenderLock) │
|
||||
│ │ └── requestUpdate() │
|
||||
│ │ │
|
||||
│ ├── process pending actions (Push / Pop / Replace) │
|
||||
│ │ │
|
||||
│ └── if requestedUpdate: ──notify──► Render Task │
|
||||
│ (single, shared) │
|
||||
│ 8KB stack │
|
||||
│ global mutex │
|
||||
│ │
|
||||
│ Activity Stack: │
|
||||
│ ┌──────────┬──────────┬──────────┐ ┌──────────┐ │
|
||||
│ │ Home │ Settings │ Wifi │ │ Keyboard │ │
|
||||
│ │ (stack) │ (stack) │ (stack) │ │ (current)│ │
|
||||
│ └──────────┴──────────┴──────────┘ └──────────┘ │
|
||||
│ stackActivities[] currentActivity │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
### 1. Change Base Class
|
||||
|
||||
If your activity extended `ActivityWithSubactivity`, change it to extend `Activity`:
|
||||
|
||||
```cpp
|
||||
// BEFORE
|
||||
class MyActivity final : public ActivityWithSubactivity {
|
||||
MyActivity(GfxRenderer& r, MappedInputManager& m, std::function<void()> goBack)
|
||||
: ActivityWithSubactivity("MyActivity", r, m), goBack(goBack) {}
|
||||
};
|
||||
|
||||
// AFTER
|
||||
class MyActivity final : public Activity {
|
||||
MyActivity(GfxRenderer& r, MappedInputManager& m)
|
||||
: Activity("MyActivity", r, m) {}
|
||||
};
|
||||
```
|
||||
|
||||
Note that navigation callbacks like `goBack` are no longer stored — use `finish()` or `activityManager.goHome()` instead.
|
||||
|
||||
### 2. Replace Navigation Functions
|
||||
|
||||
The free functions `exitActivity()` / `enterNewActivity()` in `main.cpp` are gone. Use `ActivityManager` methods:
|
||||
|
||||
```cpp
|
||||
// BEFORE (in main.cpp or via stored callbacks)
|
||||
exitActivity();
|
||||
enterNewActivity(new SettingsActivity(renderer, mappedInput, onGoHome));
|
||||
|
||||
// AFTER (from any Activity method)
|
||||
activityManager.goToSettings();
|
||||
// or for arbitrary navigation:
|
||||
activityManager.replaceActivity(std::make_unique<MyActivity>(renderer, mappedInput));
|
||||
```
|
||||
|
||||
`replaceActivity()` destroys the current activity and clears the stack. Use it for top-level navigation (home, reader, settings, etc.).
|
||||
|
||||
### 3. Replace Subactivity Pattern
|
||||
|
||||
The `enterNewActivity()` / `exitActivity()` subactivity pattern is replaced by a stack with typed results:
|
||||
|
||||
```cpp
|
||||
// BEFORE
|
||||
void MyActivity::launchWifi() {
|
||||
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
||||
[this](bool connected) { onWifiDone(connected); }));
|
||||
}
|
||||
// Child calls: onComplete(true); // triggers callback, which may call exitActivity()
|
||||
|
||||
// AFTER
|
||||
void MyActivity::launchWifi() {
|
||||
startActivityForResult(
|
||||
std::make_unique<WifiSelectionActivity>(renderer, mappedInput),
|
||||
[this](const ActivityResult& result) {
|
||||
if (result.isCancelled) return;
|
||||
auto& wifi = std::get<WifiResult>(result.data);
|
||||
onWifiDone(wifi.connected);
|
||||
});
|
||||
}
|
||||
// Child calls:
|
||||
// setResult(WifiResult{.connected = true, .ssid = ssid});
|
||||
// finish();
|
||||
```
|
||||
|
||||
Key differences:
|
||||
|
||||
- **`startActivityForResult()`** pushes the current activity onto the stack and launches the child
|
||||
- **`setResult()`** stores a typed result on the child activity
|
||||
- **`finish()`** signals the manager to pop the child, call the result handler, and resume the parent
|
||||
- The parent is never deleted during this process — it's safely stored on the stack
|
||||
|
||||
### 4. Update `render()` Signature
|
||||
|
||||
The `RenderLock` type changed from `Activity::RenderLock` (inner class) to standalone `RenderLock`:
|
||||
|
||||
```cpp
|
||||
// BEFORE
|
||||
void render(Activity::RenderLock&&) override;
|
||||
|
||||
// AFTER
|
||||
void render(RenderLock&&) override;
|
||||
```
|
||||
|
||||
Include `RenderLock.h` if not transitively included via `Activity.h`.
|
||||
|
||||
### 5. Update `onEnter()` / `onExit()`
|
||||
|
||||
Activities no longer create or destroy render tasks:
|
||||
|
||||
```cpp
|
||||
// BEFORE
|
||||
void MyActivity::onEnter() {
|
||||
Activity::onEnter(); // created render task + logged
|
||||
// ... allocate resources
|
||||
requestUpdate();
|
||||
}
|
||||
void MyActivity::onExit() {
|
||||
// ... free resources
|
||||
Activity::onExit(); // acquired RenderLock, deleted render task
|
||||
}
|
||||
|
||||
// AFTER
|
||||
void MyActivity::onEnter() {
|
||||
Activity::onEnter(); // just logs
|
||||
// ... allocate resources
|
||||
requestUpdate();
|
||||
}
|
||||
void MyActivity::onExit() {
|
||||
// ... free resources
|
||||
Activity::onExit(); // just logs
|
||||
}
|
||||
```
|
||||
|
||||
The render task lifecycle is handled entirely by `ActivityManager::begin()`.
|
||||
|
||||
### 6. Update `requestUpdate()` Calls
|
||||
|
||||
The signature changed to accept an `immediate` flag:
|
||||
|
||||
```cpp
|
||||
// BEFORE
|
||||
void requestUpdate(); // always immediate notification to per-activity render task
|
||||
|
||||
// AFTER
|
||||
void requestUpdate(bool immediate = false);
|
||||
// immediate=false (default): deferred until end of current loop iteration
|
||||
// immediate=true: sends notification to render task right away
|
||||
```
|
||||
|
||||
**When to use `immediate`**: Almost never. Deferred updates are batched — if `loop()` triggers multiple state changes that each call `requestUpdate()`, only one render happens. Use `immediate` only when you need the render to start before the current function returns (e.g., before a blocking network call).
|
||||
|
||||
**`requestUpdateAndWait()`**: Blocks the calling task until the render completes. Use sparingly — it's designed for cases where you need the screen to reflect new state before proceeding (e.g., showing "Checking for update..." before calling a network API).
|
||||
|
||||
### 7. Remove Stored Navigation Callbacks
|
||||
|
||||
Old activities often stored `std::function` callbacks for navigation:
|
||||
|
||||
```cpp
|
||||
// BEFORE
|
||||
class SettingsActivity : public ActivityWithSubactivity {
|
||||
const std::function<void()> goBack; // stored callback
|
||||
const std::function<void()> goHome; // stored callback
|
||||
public:
|
||||
SettingsActivity(GfxRenderer& r, MappedInputManager& m,
|
||||
std::function<void()> goBack, std::function<void()> goHome)
|
||||
: ActivityWithSubactivity("Settings", r, m), goBack(goBack), goHome(goHome) {}
|
||||
};
|
||||
|
||||
// AFTER
|
||||
class SettingsActivity : public Activity {
|
||||
public:
|
||||
SettingsActivity(GfxRenderer& r, MappedInputManager& m)
|
||||
: Activity("Settings", r, m) {}
|
||||
// Use finish() to go back, activityManager.goHome() to go home
|
||||
};
|
||||
```
|
||||
|
||||
This removes `std::function` overhead (~2-4KB per unique signature) and eliminates lifetime risks from captured `this` pointers.
|
||||
|
||||
## Technical Details
|
||||
|
||||
### FreeRTOS Task Model
|
||||
|
||||
The firmware runs on an ESP32-C3, a single-core RISC-V microcontroller. FreeRTOS provides cooperative and preemptive multitasking on this single core — only one task executes at any moment, and the scheduler switches between tasks at yield points (blocking calls, `vTaskDelay`, `taskYIELD`) or when a tick interrupt promotes a higher-priority task.
|
||||
|
||||
There are two tasks relevant to the activity system:
|
||||
|
||||
```text
|
||||
┌──────────────────────┐ ┌──────────────────────────┐
|
||||
│ Main Task │ │ Render Task │
|
||||
│ (Arduino loop) │ │ (ActivityManager-owned) │
|
||||
│ Priority: 1 │ │ Priority: 1 │
|
||||
│ │ │ │
|
||||
│ Runs: │ │ Runs: │
|
||||
│ - gpio.update() │ │ - ulTaskNotifyTake() │
|
||||
│ - activity->loop() │ │ (blocks until notified)│
|
||||
│ - pending actions │ │ - RenderLock (mutex) │
|
||||
│ - sleep/power mgmt │ │ - activity->render() │
|
||||
│ - requestUpdate →────┼─────┼─► xTaskNotify() │
|
||||
│ (end of loop) │ │ │
|
||||
└──────────────────────┘ └──────────────────────────┘
|
||||
```
|
||||
|
||||
Both tasks run at priority 1. Since the ESP32-C3 is single-core, they alternate execution: the main task runs `loop()`, then at the end of the loop iteration, notifies the render task if an update was requested. The render task wakes, acquires the mutex, calls `render()`, releases the mutex, and blocks again.
|
||||
|
||||
Do not use `xTaskCreate` inside activities. If you have a use case that seems to require a background task, open a discussion to propose a lifecycle-aware `Worker` abstraction first.
|
||||
|
||||
### The Render Mutex and RenderLock
|
||||
|
||||
A single FreeRTOS mutex (`renderingMutex`) protects shared state between `loop()` and `render()`. Since these run on different tasks, any state read by `render()` and written by `loop()` must be guarded.
|
||||
|
||||
`RenderLock` is an RAII wrapper:
|
||||
|
||||
```cpp
|
||||
// Standalone class (not tied to any specific activity)
|
||||
class RenderLock {
|
||||
bool isLocked = false;
|
||||
public:
|
||||
explicit RenderLock(); // acquires activityManager.renderingMutex
|
||||
explicit RenderLock(Activity&); // same — Activity& param kept for compatibility
|
||||
~RenderLock(); // releases mutex if still held
|
||||
void unlock(); // early release
|
||||
};
|
||||
```
|
||||
|
||||
**Usage patterns:**
|
||||
|
||||
```cpp
|
||||
// In loop(): protect state mutations that render() reads
|
||||
void MyActivity::loop() {
|
||||
if (somethingChanged) {
|
||||
RenderLock lock;
|
||||
state = newState; // safe — render() can't run while lock is held
|
||||
}
|
||||
requestUpdate(); // trigger render after lock is released
|
||||
}
|
||||
|
||||
// In render(): lock is passed in, held for duration of render
|
||||
void MyActivity::render(RenderLock&&) {
|
||||
// Lock is held — safe to read shared state
|
||||
renderer.clearScreen();
|
||||
renderer.drawText(..., stateString, ...);
|
||||
renderer.displayBuffer();
|
||||
// Lock released when RenderLock destructor runs
|
||||
}
|
||||
```
|
||||
|
||||
**Critical rule**: Never call `requestUpdateAndWait()` while holding a `RenderLock`. The render task needs the mutex to call `render()`, so holding it while waiting for the render to complete is a deadlock:
|
||||
|
||||
```text
|
||||
Main Task Render Task
|
||||
────────── ───────────
|
||||
RenderLock lock; (blocked on mutex)
|
||||
requestUpdateAndWait();
|
||||
→ notify render task
|
||||
→ block waiting for
|
||||
render to complete → wakes up
|
||||
→ tries to acquire mutex
|
||||
→ DEADLOCK: main holds mutex,
|
||||
waits for render; render
|
||||
waits for mutex
|
||||
```
|
||||
|
||||
### requestUpdate() vs requestUpdateAndWait()
|
||||
|
||||
```text
|
||||
requestUpdate(false) requestUpdate(true)
|
||||
───────────────── ─────────────────
|
||||
Sets flag only. Notifies render task
|
||||
Render happens after immediately.
|
||||
loop() returns and Render may start
|
||||
ActivityManager checks before the calling
|
||||
the flag. function returns.
|
||||
(Does NOT wait for
|
||||
render to complete.)
|
||||
|
||||
requestUpdateAndWait()
|
||||
──────────────────────
|
||||
Notifies render task AND
|
||||
blocks calling task until
|
||||
render is done. Uses
|
||||
FreeRTOS direct-to-task
|
||||
notification on the
|
||||
caller's task handle.
|
||||
```
|
||||
|
||||
`requestUpdateAndWait()` flow in detail:
|
||||
|
||||
```text
|
||||
Calling Task Render Task
|
||||
──────────── ───────────
|
||||
requestUpdateAndWait()
|
||||
├─ assert: not render task
|
||||
├─ assert: not holding RenderLock
|
||||
├─ store waitingTaskHandle
|
||||
├─ xTaskNotify(renderTask) → wakes render task
|
||||
└─ ulTaskNotifyTake() ─┐
|
||||
(blocked) │ RenderLock lock;
|
||||
│ activity->render();
|
||||
│ // render complete
|
||||
│ taskENTER_CRITICAL
|
||||
│ waiter = waitingTaskHandle
|
||||
│ waitingTaskHandle = nullptr
|
||||
│ taskEXIT_CRITICAL
|
||||
│ xTaskNotify(waiter) ───┐
|
||||
│ │
|
||||
┌──────────────────────┘ │
|
||||
│ (woken by notification) ◄────────────────────────┘
|
||||
└─ return
|
||||
```
|
||||
|
||||
### Activity Lifecycle Under ActivityManager
|
||||
|
||||
```text
|
||||
activityManager.replaceActivity(make_unique<MyActivity>(...))
|
||||
│
|
||||
▼
|
||||
╔═══════════════════════════════════════════════════╗
|
||||
║ pendingAction = Replace ║
|
||||
║ pendingActivity = MyActivity ║
|
||||
╚═══════════════════════════════════════════════════╝
|
||||
│
|
||||
▼ (next loop iteration)
|
||||
ActivityManager::loop()
|
||||
│
|
||||
├── currentActivity->loop() // old activity's last loop
|
||||
│
|
||||
├── process pending action:
|
||||
│ ├── RenderLock lock;
|
||||
│ ├── oldActivity->onExit() // cleanup under lock
|
||||
│ ├── delete oldActivity
|
||||
│ ├── clear stack
|
||||
│ ├── currentActivity = MyActivity
|
||||
│ ├── lock.unlock()
|
||||
│ └── MyActivity->onEnter() // init new activity
|
||||
│
|
||||
└── if requestedUpdate:
|
||||
└── notify render task
|
||||
```
|
||||
|
||||
For push/pop (subactivity) navigation:
|
||||
|
||||
```text
|
||||
Parent calls: startActivityForResult(make_unique<Child>(...), handler)
|
||||
│
|
||||
▼
|
||||
╔══════════════════════════════════════╗
|
||||
║ pendingAction = Push ║
|
||||
║ pendingActivity = Child ║
|
||||
║ parent->resultHandler = handler ║
|
||||
╚══════════════════════════════════════╝
|
||||
│
|
||||
▼ (next loop iteration)
|
||||
├── Parent moved to stackActivities[]
|
||||
├── currentActivity = Child
|
||||
└── Child->onEnter()
|
||||
|
||||
... child runs ...
|
||||
|
||||
Child calls: setResult(MyResult{...}); finish();
|
||||
│
|
||||
▼
|
||||
╔══════════════════════════════════════╗
|
||||
║ pendingAction = Pop ║
|
||||
║ child->result = MyResult{...} ║
|
||||
╚══════════════════════════════════════╝
|
||||
│
|
||||
▼ (next loop iteration)
|
||||
├── result = child->result
|
||||
├── Child->onExit(); delete Child
|
||||
├── currentActivity = Parent (popped from stack)
|
||||
├── Parent->resultHandler(result)
|
||||
└── requestUpdate() // automatic re-render for parent
|
||||
```
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
**Calling `finish()` and continuing to access `this`**: `finish()` sets `pendingAction = Pop` but does not immediately destroy the activity. The activity is destroyed on the next `ActivityManager::loop()` iteration. It's safe to access member variables after `finish()` within the same function, but don't rely on the activity surviving past the current `loop()` call.
|
||||
|
||||
**Modifying shared state without `RenderLock`**: If `render()` reads a variable and `loop()` writes it, the write must be under a `RenderLock`. Without it, `render()` could see a half-written value (e.g., a partially updated string or struct).
|
||||
|
||||
**Creating background tasks that outlive the activity**: Any FreeRTOS task created in `onEnter()` must be deleted in `onExit()` before the activity is destroyed. The `ActivityManager` does not track or clean up background tasks.
|
||||
|
||||
**Holding `RenderLock` across blocking calls**: The render task is blocked on the mutex while you hold the lock. Keep critical sections short — acquire, mutate state, release, then do blocking work.
|
||||
|
||||
```cpp
|
||||
// WRONG — blocks render for the entire network call
|
||||
void MyActivity::doNetworkStuff() {
|
||||
RenderLock lock;
|
||||
state = LOADING;
|
||||
auto result = http.get(url); // blocks for seconds with lock held
|
||||
state = DONE;
|
||||
}
|
||||
|
||||
// CORRECT — release lock before blocking
|
||||
void MyActivity::doNetworkStuff() {
|
||||
{
|
||||
RenderLock lock;
|
||||
state = LOADING;
|
||||
}
|
||||
requestUpdate(true); // render "Loading..." immediately, before we block
|
||||
auto result = http.get(url); // lock is not held
|
||||
{
|
||||
RenderLock lock;
|
||||
state = DONE;
|
||||
}
|
||||
requestUpdate();
|
||||
}
|
||||
```
|
||||
@@ -1,11 +0,0 @@
|
||||
# Contributing Docs
|
||||
|
||||
This section is a lightweight contributor guide for CrossPoint Reader.
|
||||
It is written for software developers who may be new to embedded development.
|
||||
|
||||
- [Getting Started](./getting-started.md)
|
||||
- [Architecture Overview](./architecture.md)
|
||||
- [Development Workflow](./development-workflow.md)
|
||||
- [Testing and Debugging](./testing-debugging.md)
|
||||
|
||||
If you are new, start with [Getting Started](./getting-started.md).
|
||||
@@ -1,199 +0,0 @@
|
||||
# Architecture Overview
|
||||
|
||||
CrossPoint is firmware for the Xteink X4 (unaffiliated with Xteink), built with PlatformIO targeting the ESP32-C3 microcontroller.
|
||||
|
||||
At a high level, it is firmware that uses an activity-driven application architecture loop with persistent settings/state, SD-card-first caching, and a rendering pipeline optimized for e-ink constraints.
|
||||
|
||||
## System at a glance
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Hardware: ESP32-C3 + SD + E-ink + Buttons] --> B[open-x4-sdk HAL]
|
||||
B --> C[src/main.cpp runtime loop]
|
||||
C --> D[Activities layer]
|
||||
C --> E[State and settings]
|
||||
D --> F[Reader flows]
|
||||
D --> G[Home/Library/Settings flows]
|
||||
D --> H[Network/Web server flows]
|
||||
F --> I[lib/Epub parsing + layout + hyphenation]
|
||||
I --> J[SD cache in .crosspoint]
|
||||
D --> K[GfxRenderer]
|
||||
K --> L[E-ink display buffer]
|
||||
```
|
||||
|
||||
## Runtime lifecycle
|
||||
|
||||
Primary entry point is `src/main.cpp`.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Boot] --> B[Init GPIO and optional serial]
|
||||
B --> C[Init SD storage]
|
||||
C --> D[Load settings and app state]
|
||||
D --> E[Init display and fonts]
|
||||
E --> F{Resume reader?}
|
||||
F -->|No| G[Enter Home activity]
|
||||
F -->|Yes| H[Enter Reader activity]
|
||||
G --> I[Main loop]
|
||||
H --> I
|
||||
I --> J[Poll input and run current activity]
|
||||
J --> K{Sleep condition met?}
|
||||
K -->|No| I
|
||||
K -->|Yes| L[Persist state and enter deep sleep]
|
||||
```
|
||||
|
||||
In each loop iteration, the firmware updates input, runs the active activity, handles auto-sleep/power behavior, and applies a short delay policy to balance responsiveness and power.
|
||||
|
||||
## Activity model
|
||||
|
||||
Activities are screen-level controllers deriving from `src/activities/Activity.h`.
|
||||
Some flows use `src/activities/ActivityWithSubactivity.h` to host nested activities.
|
||||
|
||||
- `onEnter()` and `onExit()` manage setup/teardown
|
||||
- `loop()` handles per-frame behavior
|
||||
- `skipLoopDelay()` and `preventAutoSleep()` are used by long-running flows (for example web server mode)
|
||||
|
||||
Top-level activity groups:
|
||||
|
||||
- `src/activities/home/`: home and library navigation
|
||||
- `src/activities/reader/`: EPUB/XTC/TXT reading flows
|
||||
- `src/activities/settings/`: settings menus and configuration
|
||||
- `src/activities/network/`: WiFi selection, AP/STA mode, file transfer server
|
||||
- `src/activities/boot_sleep/`: boot and sleep transitions
|
||||
|
||||
## Reader and content pipeline
|
||||
|
||||
Reader orchestration starts in `src/activities/reader/ReaderActivity.h` and dispatches to format-specific readers.
|
||||
EPUB processing is implemented in `lib/Epub/`.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Select book] --> B[ReaderActivity]
|
||||
B --> C{Format}
|
||||
C -->|EPUB| D[lib/Epub/Epub]
|
||||
C -->|XTC| E[lib/Xtc reader]
|
||||
C -->|TXT| F[lib/Txt reader]
|
||||
D --> G[Parse OPF/TOC/CSS]
|
||||
G --> H[Layout pages/sections]
|
||||
H --> I[Write section and metadata caches]
|
||||
I --> J[Render current page via GfxRenderer]
|
||||
```
|
||||
|
||||
Why caching matters:
|
||||
|
||||
- RAM is limited on ESP32-C3, so expensive parsed/layout data is persisted to SD
|
||||
- repeat opens/page navigation can reuse cached data instead of full reparsing
|
||||
|
||||
## Reader internals call graph
|
||||
|
||||
This diagram zooms into the EPUB path to show the main control and data flow from activity entry to on-screen draw.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[ReaderActivity onEnter] --> B{File type}
|
||||
B -->|EPUB| C[Create Epub object]
|
||||
B -->|XTC/TXT| Z[Use format-specific reader]
|
||||
|
||||
C --> D[Epub load]
|
||||
D --> E[Locate container and OPF]
|
||||
E --> F[Build or load BookMetadataCache]
|
||||
F --> G[Load TOC and spine]
|
||||
G --> H[Load or parse CSS rules]
|
||||
|
||||
H --> I[EpubReaderActivity]
|
||||
I --> J{Section cache exists for current settings?}
|
||||
J -->|Yes| K[Read section bin from SD cache]
|
||||
J -->|No| L[Parse chapter HTML and layout text]
|
||||
L --> M[Apply typography settings and hyphenation]
|
||||
M --> N[Write section cache bin]
|
||||
|
||||
K --> O[Build page model]
|
||||
N --> O
|
||||
O --> P[GfxRenderer draw calls]
|
||||
P --> Q[HAL display framebuffer update]
|
||||
Q --> R[E-ink refresh policy]
|
||||
|
||||
S[SETTINGS singleton] -. influences .-> J
|
||||
S -. influences .-> M
|
||||
T[APP_STATE singleton] -. persists .-> U[Reading progress and resume context]
|
||||
U -. used by .-> I
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- "section cache exists" depends on cache-busting parameters such as font and layout-related settings
|
||||
- rendering favors reusing precomputed layout data to keep page turns responsive on constrained hardware
|
||||
- progress/session state is persisted so the reader can reopen at the last position after reboot/sleep
|
||||
|
||||
## State and persistence
|
||||
|
||||
Two singletons are central:
|
||||
|
||||
- `src/CrossPointSettings.h` (`SETTINGS`): user preferences and behavior flags
|
||||
- `src/CrossPointState.h` (`APP_STATE`): runtime/session state such as current book and sleep context
|
||||
|
||||
Typical persisted areas on SD:
|
||||
|
||||
```text
|
||||
/.crosspoint/
|
||||
epub_<hash>/
|
||||
book.bin
|
||||
progress.bin
|
||||
cover.bmp
|
||||
sections/*.bin
|
||||
settings.bin
|
||||
state.bin
|
||||
```
|
||||
|
||||
For binary cache formats, see `docs/file-formats.md`.
|
||||
|
||||
## Networking architecture
|
||||
|
||||
Network file transfer is controlled by `src/activities/network/CrossPointWebServerActivity.h` and served by `src/network/CrossPointWebServer.h`.
|
||||
|
||||
Modes:
|
||||
|
||||
- STA: join existing WiFi network
|
||||
- AP: create hotspot
|
||||
|
||||
Server behavior:
|
||||
|
||||
- HTTP server on port 80
|
||||
- WebSocket upload server on port 81
|
||||
- file operations backed by SD storage
|
||||
- activity requests faster loop responsiveness while server is running
|
||||
|
||||
Endpoint reference: `docs/webserver-endpoints.md`.
|
||||
|
||||
## Build-time generated assets
|
||||
|
||||
Some sources are generated and should not be edited manually.
|
||||
|
||||
- `scripts/build_html.py` generates `src/network/html/*.generated.h` from HTML files
|
||||
- `scripts/generate_hyphenation_trie.py` generates hyphenation headers under `lib/Epub/Epub/hyphenation/generated/`
|
||||
|
||||
When editing related source assets, regenerate via normal build steps/scripts.
|
||||
|
||||
## Key directories
|
||||
|
||||
- `src/`: app orchestration, settings/state, and activity implementations
|
||||
- `src/network/`: web server and OTA/update networking
|
||||
- `src/components/`: theming and shared UI components
|
||||
- `lib/Epub/`: EPUB parser, layout, CSS handling, and hyphenation
|
||||
- `lib/`: supporting libraries (fonts, text, filesystem helpers, etc.)
|
||||
- `open-x4-sdk/`: hardware SDK submodule (display, input, storage, battery)
|
||||
- `docs/`: user and technical documentation
|
||||
|
||||
## Embedded constraints that shape design
|
||||
|
||||
- constrained RAM drives SD-first caching and careful allocations
|
||||
- e-ink refresh cost drives render/update batching choices
|
||||
- main loop responsiveness matters for input, power handling, and watchdog safety
|
||||
- background/network flows must cooperate with sleep and loop timing logic
|
||||
|
||||
## Scope guardrails
|
||||
|
||||
Before implementing larger ideas, check:
|
||||
|
||||
- [SCOPE.md](../../SCOPE.md)
|
||||
- [GOVERNANCE.md](../../GOVERNANCE.md)
|
||||
@@ -1,43 +0,0 @@
|
||||
# Development Workflow
|
||||
|
||||
This page defines the expected local workflow before opening a pull request.
|
||||
|
||||
## 1) Fork and create a focused branch
|
||||
|
||||
- Fork the repository to your own GitHub account
|
||||
- Clone your fork locally and add the upstream repository if needed
|
||||
|
||||
- Branch from `master`
|
||||
- Keep each PR focused on one fix or feature area
|
||||
|
||||
## 2) Implement with scope in mind
|
||||
|
||||
- Confirm your idea is in project scope: [SCOPE.md](../../SCOPE.md)
|
||||
- Prefer incremental changes over broad refactors
|
||||
|
||||
## 3) Run local checks
|
||||
|
||||
```sh
|
||||
./bin/clang-format-fix
|
||||
pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
|
||||
pio run
|
||||
```
|
||||
|
||||
CI enforces formatting, static analysis, and build checks.
|
||||
Use clang-format 21+ locally to match CI.
|
||||
If `clang-format` is missing or too old locally, see [Getting Started](./getting-started.md).
|
||||
|
||||
## 4) Open the PR
|
||||
|
||||
- Use a semantic title (example: `fix: avoid crash when opening malformed epub`)
|
||||
- Fill out `.github/PULL_REQUEST_TEMPLATE.md`
|
||||
- Describe the problem, approach, and any tradeoffs
|
||||
- Include reproduction and verification steps for bug fixes
|
||||
|
||||
## 5) Review etiquette
|
||||
|
||||
- Be explicit and concise in responses
|
||||
- Keep discussions technical and respectful
|
||||
- Assume good intent and focus on code-level feedback
|
||||
|
||||
For community expectations, see [GOVERNANCE.md](../../GOVERNANCE.md).
|
||||
@@ -1,80 +0,0 @@
|
||||
# Getting Started
|
||||
|
||||
This guide helps you build and run CrossPoint locally.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- PlatformIO Core (`pio`) or VS Code + PlatformIO IDE
|
||||
- Python 3.8+
|
||||
- `clang-format` 21+ in your `PATH` (CI uses clang-format 21)
|
||||
- USB-C cable
|
||||
- Xteink X4 device for hardware testing
|
||||
|
||||
If `./bin/clang-format-fix` fails with either of these errors, install clang-format 21:
|
||||
|
||||
- `clang-format: No such file or directory`
|
||||
- `.clang-format: error: unknown key 'AlignFunctionDeclarations'`
|
||||
|
||||
Examples:
|
||||
|
||||
```sh
|
||||
# Debian/Ubuntu (try this first)
|
||||
sudo apt-get update && sudo apt-get install -y clang-format-21
|
||||
|
||||
# If the package is unavailable, add LLVM apt repo and retry
|
||||
wget https://apt.llvm.org/llvm.sh
|
||||
chmod +x llvm.sh
|
||||
sudo ./llvm.sh 21
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang-format-21
|
||||
|
||||
# macOS (Homebrew)
|
||||
brew install clang-format
|
||||
```
|
||||
|
||||
Then verify:
|
||||
|
||||
```sh
|
||||
clang-format-21 --version
|
||||
```
|
||||
|
||||
The reported major version must be 21 or newer.
|
||||
|
||||
## Clone and initialize
|
||||
|
||||
```sh
|
||||
git clone --recursive https://github.com/crosspoint-reader/crosspoint-reader
|
||||
cd crosspoint-reader
|
||||
```
|
||||
|
||||
If you already cloned without submodules:
|
||||
|
||||
```sh
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
pio run
|
||||
```
|
||||
|
||||
## Flash
|
||||
|
||||
```sh
|
||||
pio run --target upload
|
||||
```
|
||||
|
||||
## First checks before opening a PR
|
||||
|
||||
```sh
|
||||
./bin/clang-format-fix
|
||||
pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
|
||||
pio run
|
||||
```
|
||||
|
||||
## What to read next
|
||||
|
||||
- [Architecture Overview](./architecture.md)
|
||||
- [Development Workflow](./development-workflow.md)
|
||||
- [Testing and Debugging](./testing-debugging.md)
|
||||
@@ -1,48 +0,0 @@
|
||||
# Testing and Debugging
|
||||
|
||||
CrossPoint runs on real hardware, so debugging usually combines local build checks and on-device logs.
|
||||
|
||||
## Local checks
|
||||
|
||||
Make sure `clang-format` 21+ is installed and available in `PATH` before running the formatting step.
|
||||
If needed, see [Getting Started](./getting-started.md).
|
||||
|
||||
```sh
|
||||
./bin/clang-format-fix
|
||||
pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
|
||||
pio run
|
||||
```
|
||||
|
||||
## Flash and monitor
|
||||
|
||||
Flash firmware:
|
||||
|
||||
```sh
|
||||
pio run --target upload
|
||||
```
|
||||
|
||||
Open serial monitor:
|
||||
|
||||
```sh
|
||||
pio device monitor
|
||||
```
|
||||
|
||||
Optional enhanced monitor:
|
||||
|
||||
```sh
|
||||
python3 -m pip install pyserial colorama matplotlib
|
||||
python3 scripts/debugging_monitor.py
|
||||
```
|
||||
|
||||
## Useful bug report contents
|
||||
|
||||
- Firmware version and build environment
|
||||
- Exact steps to reproduce
|
||||
- Expected vs actual behavior
|
||||
- Serial logs from boot through failure
|
||||
- Whether issue reproduces after clearing `.crosspoint/` cache on SD card
|
||||
|
||||
## Common troubleshooting references
|
||||
|
||||
- [User Guide troubleshooting section](../../USER_GUIDE.md#7-troubleshooting-issues--escaping-bootloop)
|
||||
- [Webserver troubleshooting](../troubleshooting.md)
|
||||
@@ -45,9 +45,22 @@ byte arrays, and emits headers under
|
||||
`SerializedHyphenationPatterns` descriptor so the reader can keep the automaton
|
||||
in flash.
|
||||
|
||||
A convenient script `update_hyphenation.sh` is used to update all languages.
|
||||
To use it, run:
|
||||
To refresh the firmware assets after updating the `.bin` files, run:
|
||||
|
||||
```sh
|
||||
./scripts/update_hypenation.sh
|
||||
```
|
||||
./scripts/generate_hyphenation_trie.py \
|
||||
--input lib/Epub/Epub/hyphenation/tries/en.bin \
|
||||
--output lib/Epub/Epub/hyphenation/generated/hyph-en.trie.h
|
||||
|
||||
./scripts/generate_hyphenation_trie.py \
|
||||
--input lib/Epub/Epub/hyphenation/tries/fr.bin \
|
||||
--output lib/Epub/Epub/hyphenation/generated/hyph-fr.trie.h
|
||||
|
||||
./scripts/generate_hyphenation_trie.py \
|
||||
--input lib/Epub/Epub/hyphenation/tries/de.bin \
|
||||
--output lib/Epub/Epub/hyphenation/generated/hyph-de.trie.h
|
||||
|
||||
./scripts/generate_hyphenation_trie.py \
|
||||
--input lib/Epub/Epub/hyphenation/tries/ru.bin \
|
||||
--output lib/Epub/Epub/hyphenation/generated/hyph-ru.trie.h
|
||||
```
|
||||
|
||||
-241
@@ -1,241 +0,0 @@
|
||||
# Internationalization (I18N)
|
||||
|
||||
This guide explains the multi-language support system in CrossPoint Reader.
|
||||
|
||||
## Supported Languages
|
||||
|
||||
- English
|
||||
- French
|
||||
- German
|
||||
- Portuguese
|
||||
- Spanish
|
||||
- Swedish
|
||||
- Czech
|
||||
- Russian
|
||||
- Ukrainian
|
||||
- Polish
|
||||
- Danish
|
||||
- Turkish
|
||||
|
||||
---
|
||||
|
||||
|
||||
## For Developers
|
||||
|
||||
### Translation System Architecture
|
||||
|
||||
The I18N system uses **per-language YAML files** to maintain translations and a Python script to generate C++ code:
|
||||
|
||||
```
|
||||
lib/I18n/
|
||||
├── translations/ # One YAML file per language
|
||||
│ ├── english.yaml
|
||||
│ ├── spanish.yaml
|
||||
│ ├── french.yaml
|
||||
│ └── ...
|
||||
├── I18n.h
|
||||
├── I18n.cpp
|
||||
├── I18nKeys.h # Enums (auto-generated)
|
||||
├── I18nStrings.h # String array declarations (auto-generated)
|
||||
└── I18nStrings.cpp # String array definitions (auto-generated)
|
||||
|
||||
scripts/
|
||||
└── gen_i18n.py # Code generator script
|
||||
```
|
||||
|
||||
**Key principle:** All translations are managed in the YAML files under `lib/I18n/translations/`. The Python script generates the necessary C++ code automatically.
|
||||
|
||||
---
|
||||
|
||||
### YAML File Format
|
||||
|
||||
Each language has its own file in `lib/I18n/translations/` (e.g. `spanish.yaml`).
|
||||
|
||||
A file looks like this:
|
||||
|
||||
```yaml
|
||||
_language_name: "Español"
|
||||
_language_code: "ES"
|
||||
_order: "1"
|
||||
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "BOOTING"
|
||||
STR_BROWSE_FILES: "Buscar archivos"
|
||||
```
|
||||
|
||||
**Metadata keys** (prefixed with `_`):
|
||||
- `_language_name` — Native display name shown to the user (e.g. "Français")
|
||||
- `_language_code` — C++ enum name (e.g. "FR"). Please use the [ISO Code](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) of the language. Must be a valid C++ identifier.
|
||||
- `_order` — Controls the position in the Language enum (English is always 0)
|
||||
|
||||
**Rules:**
|
||||
- Use UTF-8 encoding
|
||||
- Every line must follow the format: `KEY: "value"`
|
||||
- Keys must be valid C++ identifiers (uppercase, starts with STR_)
|
||||
- Keys must be unique within a file
|
||||
- String values must be quoted
|
||||
- Use `\n` for newlines, `\\` for literal backslashes, `\"` for literal quotes inside values
|
||||
|
||||
---
|
||||
|
||||
### Adding New Strings
|
||||
|
||||
To add a new translatable string:
|
||||
|
||||
#### 1. Edit the English YAML file
|
||||
|
||||
Add the key to `lib/I18n/translations/english.yaml`:
|
||||
|
||||
```yaml
|
||||
STR_MY_NEW_STRING: "My New String"
|
||||
```
|
||||
|
||||
Then add translations in each language file. If a key is missing from a
|
||||
language file, the generator will automatically use the English text as a
|
||||
fallback (and print a warning).
|
||||
|
||||
#### 2. Run the generator script
|
||||
|
||||
```bash
|
||||
python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/
|
||||
```
|
||||
|
||||
This automatically:
|
||||
- Fills missing translations from English
|
||||
- Updates the `StrId` enum in `I18nKeys.h`
|
||||
- Regenerates all language arrays in `I18nStrings.cpp`
|
||||
|
||||
#### 3. Use in code
|
||||
|
||||
```cpp
|
||||
#include <I18n.h>
|
||||
|
||||
// Using the tr() macro (recommended)
|
||||
renderer.drawText(font, x, y, tr(STR_MY_NEW_STRING));
|
||||
|
||||
// Using I18N.get() directly
|
||||
const char* text = I18N.get(StrId::STR_MY_NEW_STRING);
|
||||
```
|
||||
|
||||
**That's it!** No manual array synchronization needed.
|
||||
|
||||
---
|
||||
|
||||
### Adding a New Language
|
||||
|
||||
To add support for a new language (e.g., Italian):
|
||||
|
||||
#### 1. Create a new YAML file
|
||||
|
||||
Create `lib/I18n/translations/italian.yaml`:
|
||||
|
||||
```yaml
|
||||
_language_name: "Italiano"
|
||||
_language_code: "IT"
|
||||
_order: "7"
|
||||
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "AVVIO"
|
||||
```
|
||||
|
||||
You only need to include the strings you have translations for. Missing
|
||||
keys will fall back to English automatically.
|
||||
|
||||
#### 2. Run the generator
|
||||
|
||||
```bash
|
||||
python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/
|
||||
```
|
||||
|
||||
This automatically updates all necessary code.
|
||||
|
||||
---
|
||||
|
||||
### Modifying Existing Translations
|
||||
|
||||
Simply edit the relevant YAML file and regenerate:
|
||||
|
||||
```bash
|
||||
python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### UTF-8 Encoding
|
||||
|
||||
The YAML files use UTF-8 encoding. Special characters are automatically converted to C++ UTF-8 hex sequences by the generator.
|
||||
|
||||
---
|
||||
|
||||
### I18N API Reference
|
||||
|
||||
```cpp
|
||||
// === Convenience Macros (Recommended) ===
|
||||
|
||||
// tr(id) - Get translated string without StrId:: prefix
|
||||
const char* text = tr(STR_SETTINGS_TITLE);
|
||||
renderer.drawText(font, x, y, tr(STR_BROWSE_FILES));
|
||||
Serial.printf("Status: %s\n", tr(STR_CONNECTED));
|
||||
|
||||
// I18N - Shorthand for I18n::getInstance()
|
||||
I18N.setLanguage(Language::ES);
|
||||
Language lang = I18N.getLanguage();
|
||||
|
||||
// === Full API ===
|
||||
|
||||
// Get the singleton instance
|
||||
I18n& instance = I18n::getInstance();
|
||||
|
||||
// Get translated string (three equivalent ways)
|
||||
const char* text = tr(STR_SETTINGS_TITLE); // Macro (recommended)
|
||||
const char* text = I18N.get(StrId::STR_SETTINGS_TITLE); // Direct call
|
||||
const char* text = I18N[StrId::STR_SETTINGS_TITLE]; // Operator overload
|
||||
|
||||
// Set language
|
||||
I18N.setLanguage(Language::ES);
|
||||
|
||||
// Get current language
|
||||
Language lang = I18N.getLanguage();
|
||||
|
||||
// Save language setting to file
|
||||
I18N.saveSettings();
|
||||
|
||||
// Load language setting from file
|
||||
I18N.loadSettings();
|
||||
|
||||
// Get character set for font subsetting (static method)
|
||||
const char* chars = I18n::getCharacterSet(Language::FR);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Storage
|
||||
|
||||
Language settings are stored in:
|
||||
```
|
||||
/.crosspoint/language.bin
|
||||
```
|
||||
|
||||
This file contains:
|
||||
- Version byte
|
||||
- Current language selection (1 byte)
|
||||
|
||||
---
|
||||
|
||||
## Translation Workflow
|
||||
|
||||
### For Developers (Adding Features)
|
||||
|
||||
1. Add new strings to `lib/I18n/translations/english.yaml`
|
||||
2. Run `python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/`
|
||||
3. Use the new `StrId` in your code
|
||||
4. Request translations from translators
|
||||
|
||||
### For Translators
|
||||
|
||||
1. Open the YAML file for your language in `lib/I18n/translations/`
|
||||
2. Add or update translations using the format `STR_KEY: "translated text"`
|
||||
3. Keep translations concise (E-ink space constraints)
|
||||
4. Make sure the file is in UTF-8 encoding
|
||||
5. Run `python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/` to verify
|
||||
6. Test on device or submit for review
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user