Compare commits

..

1 Commits

Author SHA1 Message Date
Dave Allie 567fa6e1e2 feat: Overhaul font format into CrossPoint font 2026-01-28 23:59:22 +11:00
441 changed files with 285047 additions and 281415 deletions
-1
View File
@@ -1 +0,0 @@
../../.skills/SKILL.md
+18 -102
View File
@@ -1,61 +1,10 @@
name: CI (build)
on:
name: CI
'on':
push:
branches: [master]
pull_request:
permissions:
contents: read
jobs:
clang-format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Install clang-format-21
run: |
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
- name: Run clang-format
run: |
PATH="/usr/lib/llvm-21/bin:$PATH" ./bin/clang-format-fix
git diff --exit-code || (echo "Please run 'bin/clang-format-fix' to fix formatting issues" && exit 1)
cppcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- 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
- name: Run cppcheck
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
build:
runs-on: ubuntu-latest
steps:
@@ -67,55 +16,22 @@ 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: Install clang-format-21
run: |
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
- name: Run cppcheck
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
- name: Run clang-format
run: PATH="/usr/lib/llvm-21/bin:$PATH" ./bin/clang-format-fix && git diff --exit-code || (echo "Please run 'bin/clang-format-fix' to fix formatting issues" && exit 1)
- name: Build CrossPoint
run: |
set -euo pipefail
pio run | tee pio.log
- name: Extract firmware stats
run: |
set -euo pipefail
ram_line="$(grep -E "RAM:\\s" -m1 pio.log || true)"
flash_line="$(grep -E "Flash:\\s" -m1 pio.log || true)"
echo "ram_line=${ram_line}" >> "$GITHUB_OUTPUT"
echo "flash_line=${flash_line}" >> "$GITHUB_OUTPUT"
{
echo "## Firmware build stats"
if [ -n "$ram_line" ]; then echo "- ${ram_line}"; else echo "- RAM: not found"; fi
if [ -n "$flash_line" ]; then echo "- ${flash_line}"; else echo "- Flash: not found"; fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload firmware.bin artifact
uses: actions/upload-artifact@v6
with:
name: firmware.bin
path: .pio/build/default/firmware.bin
if-no-files-found: error
# This job is used as the PR required actions check, allows for changes to other steps in the future without breaking
# PR requirements.
test-status:
name: Test Status
needs:
- build
- clang-format
- cppcheck
if: always()
runs-on: ubuntu-latest
steps:
- name: Fail because needed jobs failed
# Fail if any job failed or was cancelled (skipped jobs are ok)
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
run: exit 1
- name: Success
run: exit 0
run: pio run
@@ -1,7 +1,6 @@
name: "PR Formatting"
on:
pull_request:
pull_request_target:
types:
- opened
+8 -7
View File
@@ -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
-47
View File
@@ -1,47 +0,0 @@
name: Compile Release Candidate
on:
workflow_dispatch:
jobs:
build-release-candidate:
if: startsWith(github.ref_name, 'release/')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- 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
- name: Extract env
run: |
echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
echo "BRANCH_SUFFIX=${GITHUB_REF_NAME#release/}" >> $GITHUB_ENV
- name: Build CrossPoint Release Candidate
env:
CROSSPOINT_RC_HASH: ${{ env.SHORT_SHA }}
run: pio run -e gh_release_rc
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: CrossPoint-RC-${{ env.BRANCH_SUFFIX }}
path: |
.pio/build/gh_release_rc/bootloader.bin
.pio/build/gh_release_rc/firmware.bin
.pio/build/gh_release_rc/firmware.elf
.pio/build/gh_release_rc/firmware.map
.pio/build/gh_release_rc/partitions.bin
-6
View File
@@ -3,15 +3,9 @@
.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*
-872
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -1 +0,0 @@
.skills/SKILL.md
-38
View File
@@ -1,38 +0,0 @@
# Project Governance & Community Principles
CrossPoint Reader is a community-driven, open-source project. Our goal is to provide a high-quality, open-source
firmware alternative for the Xteink X4 hardware. To keep this project productive and welcoming as we grow, we ask all
contributors to follow these principles.
### 1. The "Human First" Rule
Technical discussions can get heated, but they should never be personal.
- **Assume good intent:** We are all volunteers working on this in our free time. If a comment seems abrasive, assume
its a language barrier or a misunderstanding before taking offense.
- **Focus on the code, not the person:** Critique the implementation, the performance, or the UX. Never the intelligence
or character of the contributor.
- **Inflammatory language:** Personal attacks, trolling, or exclusionary language (based on race, gender, background,
etc.) are not welcome here and will be moderated.
### 2. A "Do-ocracy" with Guidance
CrossPoint thrives because people step up to build what they want to see.
- If you want a feature, the best way to get it is to start an
[Idea Discussion](https://github.com/crosspoint-reader/crosspoint-reader/discussions/categories/ideas) or open a PR.
- If you want to report a bug, check for duplicates and create an
[Issue](https://github.com/crosspoint-reader/crosspoint-reader/issues).
- While we encourage experimentation, the maintainers reserve the right to guide the projects technical direction to
ensure stability on the ESP32-C3s constrained hardware.
- For more guidance on the scope of the project, see the [SCOPE.md](SCOPE.md) document.
### 3. Transparent Communication
To keep the project healthy, we keep our "work" in the open.
- **Public by Default:** All technical decisions and project management discussions happen in GitHub Issues, Pull
Requests, or the public Discussions tab.
- **Clarity in Writing:** Because we have a global community with different levels of English proficiency, please be as
explicit and clear as possible in your PR descriptions and bug reports.
### 4. Moderation & Safety
The maintainers are responsible for keeping the community a safe place to contribute.
- We reserve the right to hide comments, lock threads, or block users who repeatedly violate these principles or engage
in harassment.
- **Reporting:** If you feel you are being harassed or see behavior that is damaging the community, please reach out
privately to @daveallie.
+6 -34
View File
@@ -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,16 +43,13 @@ 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).
For more details about the scope of the project, see the [SCOPE.md](SCOPE.md) document.
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint.
## Installing
### 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
@@ -62,7 +58,7 @@ back to the other partition using the "Swap boot partition" button here https://
### Web (specific firmware version)
1. Connect your Xteink X4 to your computer via USB-C
2. Download the `firmware.bin` file from the release of your choice via the [releases page](https://github.com/crosspoint-reader/crosspoint-reader/releases)
2. Download the `firmware.bin` file from the release of your choice via the [releases page](https://github.com/daveallie/crosspoint-reader/releases)
3. Go to https://xteink.dve.al/ and flash the firmware file using the "OTA fast flash controls" section
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
@@ -86,7 +82,7 @@ See [Development](#development) below.
CrossPoint uses PlatformIO for building and flashing the firmware. To get started, clone the repository:
```
git clone --recursive https://github.com/crosspoint-reader/crosspoint-reader
git clone --recursive https://github.com/daveallie/crosspoint-reader
# Or, if you've already cloned without --recursive:
git submodule update --init --recursive
@@ -99,25 +95,6 @@ Connect your Xteink X4 to your computer via USB-C and run the following command.
```sh
pio run --target upload
```
### Debugging
After flashing the new features, its recommended to capture detailed logs from the serial port.
First, make sure all required Python packages are installed:
```python
python3 -m pip install pyserial colorama matplotlib
```
after that run the script:
```sh
# For Linux
# This was tested on Debian and should work on most Linux systems.
python3 scripts/debugging_monitor.py
# For macOS
python3 scripts/debugging_monitor.py /dev/cu.usbmodem2101
```
Minor adjustments may be required for Windows.
## Internals
@@ -156,14 +133,9 @@ 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 you're looking for a way to help out, take a look at the [ideas discussion board](https://github.com/daveallie/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 goverance and community
principles, please see [GOVERNANCE.md](GOVERNANCE.md).
### To submit a contribution:
1. Fork the repo
-58
View File
@@ -1,58 +0,0 @@
# Project Vision & Scope: CrossPoint Reader
The goal of CrossPoint Reader is to create an efficient, open-source reading experience for the Xteink X4. We believe a
dedicated e-reader should do one thing exceptionally well: **facilitate focused reading.**
## 1. Core Mission
To provide a lightweight, high-performance firmware that maximizes the potential of the X4, prioritizing legibility and
usability over "swiss-army-knife" functionality.
## 2. Scope
### In-Scope
*These are features that directly improve the primary purpose of the device.*
* **User Experience:** E.g. User-friendly interfaces, and interactions, both inside the reader and navigating the
firmware. This includes things like button mapping, book loading, and book navigation like bookmarks.
* **Document Rendering:** E.g. Support for rendering documents (primarily EPUB) and improvements to the rendering
engine.
* **Format Optimization:** E.g. Efficiently parsing EPUB (CSS/Images) and other documents within the device's
capabilities.
* **Typography & Legibility:** E.g. Custom font support, hyphenation engines, and adjustable line spacing.
* **E-Ink Driver Refinement:** E.g. Reducing full-screen flashes (ghosting management) and improving general rendering.
* **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
*These items are rejected because they compromise the device's stability or mission.*
* **Interactive Apps:** No Notepads, Calculators, or Games. This is a reader, not a PDA.
* **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.
## 3. Idea Evaluation
While I appreciate the desire to add new and exciting features to CrossPoint Reader, CrossPoint Reader is designed to be
a lightweight, reliable, and performant e-reader. Things which distract or compromise the device's core mission will not
be accepted. As a guiding question, consider if your idea improve the "core reading experience" for the average user,
and, critically, not distract from that reading experience.
> **Note to Contributors:** If you are unsure if your idea fits the scope, please open a **Discussion** before you start
> coding!
+65 -252
View File
@@ -10,31 +10,22 @@ 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.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)
- [System Navigation](#system-navigation)
- [Supported Languages](#supported-languages)
- [5. Chapter Selection Screen](#5-chapter-selection-screen)
- [6. Current Limitations \& Roadmap](#6-current-limitations--roadmap)
- [7. Troubleshooting Issues \& Escaping Bootloop](#7-troubleshooting-issues--escaping-bootloop)
## 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 +33,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 +42,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 +59,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,226 +93,73 @@ 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
- **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
- "Full w/ Percentage" - Show status bar with book progress (as percentage)
- "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
- "Full" - Show status bar with reading progress
- **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.
- **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 +178,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 +186,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
@@ -373,7 +201,7 @@ CrossPoint renders text using the following Unicode character blocks, enabling s
* **Latin Script (Basic, Supplement, Extended-A):** Covers English, German, French, Spanish, Portuguese, Italian, Dutch, Swedish, Norwegian, Danish, Finnish, Polish, Czech, Hungarian, Romanian, Slovak, Slovenian, Turkish, and others.
* **Cyrillic Script (Standard and Extended):** Covers Russian, Ukrainian, Belarusian, Bulgarian, Serbian, Macedonian, Kazakh, Kyrgyz, Mongolian, and others.
What is not supported: Chinese, Japanese, Korean, Vietnamese, Hebrew, Arabic, Greek and Farsi.
What is not supported: Chinese, Japanese, Korean, Vietnamese, Hebrew, Arabic and Farsi.
---
@@ -392,18 +220,3 @@ Accessible by pressing **Confirm** while inside a book.
Please note that this firmware is currently in active development. The following features are **not yet supported** but are planned for future updates:
* **Images:** Embedded images in e-books will not render.
* **Cover Images:** Large cover images embedded into EPUB require several seconds (~10s for ~2000 pixel tall image) to convert for sleep screen and home screen thumbnail. Consider optimizing the EPUB with e.g. https://github.com/bigbag/epub-to-xtc-converter to speed this up.
---
## 7. Troubleshooting Issues & Escaping Bootloop
If an issue or crash is encountered while using Crosspoint, feel free to raise an issue ticket and attach the serial monitor logs. The logs can be obtained by connecting the device to a computer and starting a serial monitor. Either [Serial Monitor](https://www.serialmonitor.org/) or the following command can be used:
```
pio device monitor
```
If the device is stuck in a bootloop, press and release the Reset button. Then, press and hold on to the configured Back button and the Power Button to boot to the Home Screen.
There can be issues with broken cache or config. In this case, delete the `.crosspoint` directory on your SD card (or consider deleting only `settings.bin`, `state.bin`, or `epub_*` cache directories in the `.crosspoint/` folder).
+5 -30
View File
@@ -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)
@@ -36,10 +13,8 @@ fi
# --modified: files tracked by git that have been modified (staged or unstaged)
# --exclude-standard: ignores files in .gitignore
# Additionally exclude files in 'lib/EpdFont/builtinFonts/' as they are script-generated.
# Also exclude files in 'lib/Epub/Epub/hyphenation/generated/' as they are script-generated.
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
| grep -v -E '^lib/CrossPointFont/builtinFonts/' \
| grep -v -E '^lib/CrossPointFont/Group5' \
| xargs -r clang-format -style=file -i
-154
View File
@@ -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 }
-472
View File
@@ -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();
}
```
-11
View File
@@ -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).
-199
View File
@@ -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)
-43
View File
@@ -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).
-80
View File
@@ -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)
-48
View File
@@ -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)

Some files were not shown because too many files have changed in this diff Show More