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
@@ -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
|
||||
@@ -156,8 +154,6 @@ 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.
|
||||
|
||||
|
||||
@@ -25,8 +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.
|
||||
|
||||
### Out-of-Scope
|
||||
|
||||
@@ -36,16 +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.*
|
||||
|
||||
* **Clock Display:** The ESP32-C3's RTC drifts significantly during deep sleep; making the clock untrustworthy after any sleep cycle. NTP sync could help, but CrossPoint doesn't connect to the internet on every boot.
|
||||
|
||||
* **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
|
||||
|
||||
|
||||
+68
-230
@@ -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,199 +120,56 @@ 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:
|
||||
@@ -350,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.
|
||||
|
||||
@@ -358,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,154 +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'
|
||||
)
|
||||
|
||||
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