mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-08-13 14:21:57 -07:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
896b9de9b6 | ||
|
|
7e3110d296 |
No files matched your search
@@ -1,33 +0,0 @@
|
|||||||
# CrossPoint Reader: Claude Code skills
|
|
||||||
|
|
||||||
Project skills for Claude Code. Claude auto-discovers them and loads one when the
|
|
||||||
task matches its `description`; you do not invoke them by hand. They encode how
|
|
||||||
this project wants C/C++ written: the judgment calls and self-review gates that
|
|
||||||
keep the firmware small, stable, and reviewable.
|
|
||||||
|
|
||||||
These are written for capable agents, not beginners. They are principle- and
|
|
||||||
decision-focused on purpose. They deliberately avoid line-number citations,
|
|
||||||
which drift; they anchor on durable names (APIs, types, macros, files).
|
|
||||||
|
|
||||||
This is separate from `.skills/SKILL.md`, the GitHub coding-agent guide that
|
|
||||||
mirrors CLAUDE.md. CLAUDE.md stays the always-loaded rule set; these skills are
|
|
||||||
the applied decision procedures that load on demand and add the judgment layer
|
|
||||||
CLAUDE.md does not carry.
|
|
||||||
|
|
||||||
| Skill | Loads when you are... |
|
|
||||||
|---|---|
|
|
||||||
| `heap-discipline` | allocating memory: new/malloc/vector/string, buffers, caches |
|
|
||||||
| `control-flow-clarity` | writing branching logic, state flags, modes, if/else ladders |
|
|
||||||
| `hal-and-abstractions` | touching storage, input, display, settings, i18n, rendering |
|
|
||||||
| `scope-discipline` | adding a feature, activity, lib, setting, or dependency |
|
|
||||||
| `refactor-for-review` | refactoring, cleaning up, or preparing a change for PR |
|
|
||||||
|
|
||||||
Each skill ends with a self-review checklist Claude runs against its own diff
|
|
||||||
before handing it back. Reviewing a PR? Those checklists double as a fast rubric.
|
|
||||||
|
|
||||||
## Maintaining these
|
|
||||||
|
|
||||||
Edit the `SKILL.md` under each directory. Keep them tight. Do not restate
|
|
||||||
CLAUDE.md; add the judgment CLAUDE.md cannot afford to carry. Trigger quality
|
|
||||||
lives in the `description` field: it must name the situations that should pull
|
|
||||||
the skill in, in the words a contributor's task would use.
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
---
|
|
||||||
name: control-flow-clarity
|
|
||||||
description: Branching and state-modeling clarity in C/C++. Use when writing or refactoring logic that branches on discrete values: if/else-if ladders, status flags, mode or state ints, or anything that should be an enum plus an exhaustive switch. Covers enum class over magic ints, exhaustive switch over nested if, early-return guard clauses, table dispatch, and when each is the right call.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Control-Flow Clarity
|
|
||||||
|
|
||||||
The goal: a reviewer verifies correctness by reading, not by tracing. Branching
|
|
||||||
that mirrors the problem's shape is self-evident; branching that encodes it in
|
|
||||||
ad-hoc ints and nesting forces the reader to reconstruct intent.
|
|
||||||
|
|
||||||
## Core moves
|
|
||||||
|
|
||||||
- **Model a closed set of states/modes as an `enum class`, not ints or bools.**
|
|
||||||
A variable kept honest by a comment ("0 = hidden, 1 = showing, 2 = confirm")
|
|
||||||
is a latent bug. Make it an enum and the comment becomes the type.
|
|
||||||
- **Dispatch on an enum with an exhaustive `switch`, no `default`.** This
|
|
||||||
codebase relies on it: omitting `default` lets the compiler flag the
|
|
||||||
unhandled case when someone adds an enum value. A `default:` that swallows the
|
|
||||||
unknown case throws that safety away. Add `default` only when "every other
|
|
||||||
value does nothing" is a deliberate, documented decision.
|
|
||||||
- **Replace nested `if/else-if` ladders that branch on one discriminant with a
|
|
||||||
`switch`.** If each branch only maps input to a value, prefer a lookup table
|
|
||||||
(`static constexpr` array) over both.
|
|
||||||
- **Prefer early-return guard clauses over nested success bodies.** Handle the
|
|
||||||
error/empty/skip cases first and return; keep the main path at the left
|
|
||||||
margin.
|
|
||||||
|
|
||||||
## When NOT to switch
|
|
||||||
|
|
||||||
- Branches test unrelated conditions, not one discriminant: a guarded `if`
|
|
||||||
sequence is honest; a switch would be forced.
|
|
||||||
- Two outcomes on a genuine boolean: keep the `if`.
|
|
||||||
- The discriminant is an open or unbounded set (arbitrary ints, strings): table
|
|
||||||
or map, not a switch.
|
|
||||||
|
|
||||||
## Enum hygiene
|
|
||||||
|
|
||||||
- `enum class` by default for type safety. Plain `enum` only when values must
|
|
||||||
implicitly convert (e.g. a value that doubles as a UI dropdown index), and
|
|
||||||
then give it a trailing `_COUNT` sentinel for safe bounds/iteration, matching
|
|
||||||
the existing settings enums.
|
|
||||||
- Name the discriminant after what it selects, not its storage:
|
|
||||||
`Orientation orientation`, not `uint8_t mode`.
|
|
||||||
- No magic numeric codes for states. If you write a comment mapping numbers to
|
|
||||||
meanings, you owe an enum.
|
|
||||||
|
|
||||||
## Self-review
|
|
||||||
|
|
||||||
- [ ] No int/bool standing in for a closed set of modes; it is an `enum class`.
|
|
||||||
- [ ] Enum dispatch is an exhaustive `switch` with no catch-all `default` (or
|
|
||||||
the `default` is a documented deliberate choice).
|
|
||||||
- [ ] No nested if/else-if ladder on a single discriminant that should be a
|
|
||||||
switch or a table.
|
|
||||||
- [ ] Error/skip cases are early-return guards; the happy path is not buried.
|
|
||||||
- [ ] No magic numbers where a named enum or `constexpr` would state the intent.
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
---
|
|
||||||
name: hal-and-abstractions
|
|
||||||
description: Layering and abstraction discipline for the firmware. Use when touching storage, input, display, settings, i18n, or rendering, or any code that could reach into the SDK. Covers routing through the HAL (HalStorage / HalGPIO / HalDisplay) instead of raw SDK classes, MappedInputManager logical buttons instead of raw GPIO indices, UITheme/GUI for all rendering, the singleton macros, tr() for user-facing text, and where a new abstraction boundary belongs.
|
|
||||||
---
|
|
||||||
|
|
||||||
# HAL and Abstractions
|
|
||||||
|
|
||||||
CLAUDE.md lists the HAL classes and the SdFat-concurrency reason they exist.
|
|
||||||
This is when and how to route through them, and where to draw a new boundary.
|
|
||||||
|
|
||||||
## Route through the layer, always
|
|
||||||
|
|
||||||
- **SD card I/O:** `Storage` (HalStorage) and `HalFile`. Never `SdFat`,
|
|
||||||
`FsFile`, `SdSpiCard`, `FsBaseFile`, or `SDCardManager` directly. The HAL
|
|
||||||
serializes every SD access through one mutex; bypassing it races the SPI state
|
|
||||||
machine and panics FreeRTOS (CLAUDE.md has the failure mode). This is a
|
|
||||||
correctness boundary, not a style preference.
|
|
||||||
- **Display:** `HalDisplay` over `EInkDisplay`. **Input:** `HalGPIO` over
|
|
||||||
`InputManager`.
|
|
||||||
- **Rendering:** everything through the `GUI` macro (UITheme) and the renderer's
|
|
||||||
oriented metrics. No hardcoded fonts, colors, coordinates, or 800/480
|
|
||||||
literals; ask the renderer for width/height and use the oriented viewable
|
|
||||||
area.
|
|
||||||
- **Input in activities:** `MappedInputManager::Button` logical enums
|
|
||||||
(`Button::Confirm`, `Button::PageForward`, ...). Never raw `HalGPIO::BTN_*`
|
|
||||||
indices outside `ButtonRemapActivity`. Logical buttons survive user remapping
|
|
||||||
and orientation; raw indices do not.
|
|
||||||
- **Shared state:** the singleton macros (`SETTINGS`, `APP_STATE`, `GUI`,
|
|
||||||
`Storage`, `I18N`), not threaded pointers.
|
|
||||||
|
|
||||||
## User-facing text
|
|
||||||
|
|
||||||
Every string a user reads goes through `tr(STR_*)`. Add the key to the English
|
|
||||||
YAML, regenerate with `scripts/gen_i18n.py`, then use the `StrId`. Log lines
|
|
||||||
(`LOG_*`) stay hardcoded.
|
|
||||||
|
|
||||||
## Drawing a new boundary
|
|
||||||
|
|
||||||
When you need an SDK capability the HAL does not expose yet, **add the method to
|
|
||||||
the HAL; do not reach around it.** The new method inherits the mutex, logging,
|
|
||||||
and error contract the rest of the HAL carries. A one-off direct SDK call in an
|
|
||||||
activity is exactly the layering violation the mutex discipline cannot tolerate.
|
|
||||||
|
|
||||||
Keep abstractions thin. A wrapper that only renames an SDK call without adding
|
|
||||||
the mutex, logging, or an error contract is dead weight. Add a layer only when
|
|
||||||
it carries one of those contracts or hides a real implementation choice.
|
|
||||||
|
|
||||||
## Self-review
|
|
||||||
|
|
||||||
- [ ] No direct SdFat / FsFile / SDCardManager / EInkDisplay / InputManager use
|
|
||||||
outside `lib/hal`.
|
|
||||||
- [ ] File access uses `HalFile`; no `.close()` on a local handle
|
|
||||||
(DESTRUCTOR_CLOSES_FILE); members closed in `onExit`.
|
|
||||||
- [ ] Input uses `MappedInputManager::Button`, not raw `BTN_*` indices.
|
|
||||||
- [ ] Rendering goes through GUI/UITheme and oriented metrics; no 800/480 or
|
|
||||||
hardcoded fonts/coords.
|
|
||||||
- [ ] User-facing strings use `tr(STR_*)`; new keys added to YAML and
|
|
||||||
regenerated.
|
|
||||||
- [ ] Any new SDK capability is exposed as a HAL method, not called inline.
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
---
|
|
||||||
name: heap-discipline
|
|
||||||
description: Memory allocation discipline for the ESP32-C3 (~380KB RAM, no PSRAM, single 48KB framebuffer). Use whenever writing or reviewing code that allocates: new / malloc / std::vector / std::string, buffers, caches, or anything held across a loop or an activity lifecycle. Covers makeUniqueNoThrow vs raw new/malloc, fragmentation avoidance, reserve-before-push_back, alloc-once-reuse, stack vs heap sizing, and the chunked grayscale buffer pattern.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Heap Discipline (ESP32-C3)
|
|
||||||
|
|
||||||
CLAUDE.md states the allocation rules. This is the procedure you run while
|
|
||||||
writing the code and the gate you run before handing it back.
|
|
||||||
|
|
||||||
The constraint that makes every call matter: ~380KB RAM, no PSRAM, one 48KB
|
|
||||||
framebuffer. **Fragmentation, not total usage, is what kills this device.**
|
|
||||||
Free-heap can read fine while the largest free block is too small for the next
|
|
||||||
allocation. Optimize for not leaving holes, not just for using fewer bytes.
|
|
||||||
|
|
||||||
## Allocation decision procedure
|
|
||||||
|
|
||||||
Ask in order; stop at the first yes.
|
|
||||||
|
|
||||||
1. **Stack?** Local, bounded, under ~256 bytes total: plain array/struct. No
|
|
||||||
heap, no fragmentation. Keep frames lean; the task stack is small.
|
|
||||||
2. **Compile-time constant?** `static constexpr` lives in flash, costs zero DRAM.
|
|
||||||
3. **Allocated once and reused for an activity's lifetime?** Allocate in
|
|
||||||
`onEnter`, hold in a member, release in `onExit`. Never per-frame, never
|
|
||||||
per-iteration.
|
|
||||||
4. **Dynamic and fallible?** `makeUniqueNoThrow<T>(...)` /
|
|
||||||
`makeUniqueNoThrow<T[]>(n)` from `lib/Memory/Memory.h`. Null-check, `LOG_ERR`
|
|
||||||
with the size, return false. It frees on every exit path.
|
|
||||||
5. **A C/SDK API takes ownership and frees it itself?** Only then raw
|
|
||||||
`new (std::nothrow)` / `malloc`, with a comment naming who frees it.
|
|
||||||
|
|
||||||
Bare `new` / `new[]` is never correct here: under `-fno-exceptions` it calls
|
|
||||||
`abort()` on OOM instead of returning null.
|
|
||||||
|
|
||||||
## Fragmentation rules
|
|
||||||
|
|
||||||
- `std::vector`: `reserve(n)` before any `push_back` loop. Each growth is
|
|
||||||
alloc-copy-free (three heap ops) and leaves a hole. Unknown n: estimate high.
|
|
||||||
- No repeated `new`/`delete` or growing containers inside a loop or render path.
|
|
||||||
Hoist the allocation out of the loop.
|
|
||||||
- Large contiguous blocks fragment worst. Full-screen-class buffers use the
|
|
||||||
chunked `storeBwBuffer` / `restoreBwBuffer` path in `GfxRenderer` so they
|
|
||||||
never demand one contiguous 48KB block. Reuse that path. Do not malloc a
|
|
||||||
second full-screen buffer.
|
|
||||||
- `std::string` / Arduino `String`: acceptable on cold paths (file I/O, one-shot
|
|
||||||
setup). Banned on hot/render paths. Build text with a stack `char[]` +
|
|
||||||
`snprintf`; if a `std::string` is unavoidable, `reserve` it first.
|
|
||||||
|
|
||||||
## Justify every allocation
|
|
||||||
|
|
||||||
Per CLAUDE.md's evidence rule: when you add a heap allocation, state in one line
|
|
||||||
why stack/static/reuse was rejected and the worst-case size. If you cannot name
|
|
||||||
the size, you cannot budget it, and you should not allocate it.
|
|
||||||
|
|
||||||
## Self-review before handoff
|
|
||||||
|
|
||||||
- [ ] No bare `new`/`new[]`. Every fallible alloc is `makeUniqueNoThrow`, or a
|
|
||||||
raw alloc with an explicit owner comment.
|
|
||||||
- [ ] Every allocation is null-checked with `LOG_ERR` before the error return.
|
|
||||||
- [ ] No allocation inside a loop or render path that could be hoisted.
|
|
||||||
- [ ] Every `push_back` loop has a preceding `reserve`.
|
|
||||||
- [ ] Anything allocated in `onEnter` is released in `onExit`; member `HalFile`
|
|
||||||
closed there too.
|
|
||||||
- [ ] No second full-screen buffer; grayscale uses store/restoreBwBuffer.
|
|
||||||
- [ ] Each new allocation carries a one-line size + why-not-stack/static note.
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
---
|
|
||||||
name: refactor-for-review
|
|
||||||
description: Producing small, single-concern, reviewable changes. Use when refactoring, cleaning up, restructuring, decomposing, or preparing a change for PR, especially in this multi-contributor AI-assisted codebase that is prone to sprawl diffs. Covers one-concern-per-commit, extracting helpers without widening scope, not bundling unrelated edits, decomposing oversized activities, comment hygiene, and a pre-handoff self-review checklist.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Refactor for Review
|
|
||||||
|
|
||||||
This is a multi-contributor, AI-assisted codebase, and the dominant failure mode
|
|
||||||
is the sprawl diff: a one-line intent that touches thirty files. The goal is a
|
|
||||||
change a reviewer can verify in one sitting. Cleaner structure that makes the
|
|
||||||
next change easier is the win, not lines added.
|
|
||||||
|
|
||||||
## One concern per change
|
|
||||||
|
|
||||||
- A commit/PR does one thing. A bug fix is not also a rename is not also a
|
|
||||||
reformat. If you spot an unrelated improvement mid-change, leave it or capture
|
|
||||||
it separately; do not fold it in.
|
|
||||||
- When the working tree has bundled two changes, separate them with the
|
|
||||||
copy-affected-files-aside, reset, re-apply one concern, restore the rest
|
|
||||||
pattern, not by committing the tangle.
|
|
||||||
- Refactor and behavior change do not ride together. A pure refactor must not
|
|
||||||
alter behavior; a behavior change should not drag a refactor along. If both
|
|
||||||
are needed: two commits, refactor first.
|
|
||||||
|
|
||||||
## Keep the diff narrow
|
|
||||||
|
|
||||||
- Extract a helper to remove real duplication or to name a concept, not to chase
|
|
||||||
abstraction. Three-plus copies, or a block that needs a name to be understood:
|
|
||||||
extract. Two similar lines: leave them.
|
|
||||||
- No "while I'm here" scope creep. A signature or type change that ripples to
|
|
||||||
many call sites is its own PR: map every caller first, update them in one
|
|
||||||
topological pass, and land it separately, not as a rider on a feature.
|
|
||||||
- Match the surrounding code: comment density, naming, idiom. The diff should
|
|
||||||
read like the file, not like a different author.
|
|
||||||
|
|
||||||
## Decompose oversized units
|
|
||||||
|
|
||||||
An activity or function that has outgrown one screen of responsibility (multiple
|
|
||||||
unrelated state machines, or a file far larger than its siblings) is a
|
|
||||||
decomposition candidate. Extract a cohesive sub-responsibility into its own
|
|
||||||
unit, as a standalone behavior-preserving refactor, verified on its own, never
|
|
||||||
mixed into a feature change.
|
|
||||||
|
|
||||||
## Comments earn their place
|
|
||||||
|
|
||||||
Comments explain why: an invariant, a defense, a past incident, a non-obvious
|
|
||||||
constraint. Never what the next line already says. Delete narration, phase-marker
|
|
||||||
comments ("now we loop over..."), and restated function names. If a comment and
|
|
||||||
the code it sits on say the same thing, the comment is the thing to cut.
|
|
||||||
|
|
||||||
## Self-review before handoff
|
|
||||||
|
|
||||||
- [ ] The change does exactly one thing; nothing unrelated rode along.
|
|
||||||
- [ ] Refactor and behavior change are not mixed in one commit.
|
|
||||||
- [ ] No "while I'm here" creep; rename/signature ripples are split out.
|
|
||||||
- [ ] Extractions remove real duplication or name a real concept, not
|
|
||||||
speculative abstraction.
|
|
||||||
- [ ] New comments say why, not what; no narration or phase markers.
|
|
||||||
- [ ] A reviewer can understand the diff without running it.
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
---
|
|
||||||
name: scope-discipline
|
|
||||||
description: Feature-scope discipline for a dedicated e-reader (not a Swiss Army knife). Use when adding a feature, a new activity, a new lib, a setting, or a dependency, or when a request would grow the firmware's surface. Covers the SCOPE.md test, the RAM-cost vs reading-benefit gate, preferring no-code or existing-mechanism solutions, awareness of the existing activity surface, and how to push back on out-of-scope asks.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Scope Discipline
|
|
||||||
|
|
||||||
The mission: do one thing exceptionally well, focused reading on constrained
|
|
||||||
hardware. `SCOPE.md` is the source of truth for what is in and out. Read it
|
|
||||||
before adding surface. This is the gate to run before writing a new feature.
|
|
||||||
|
|
||||||
## The gate
|
|
||||||
|
|
||||||
Before adding a feature, activity, lib, setting, or dependency, answer in order:
|
|
||||||
|
|
||||||
1. **Is it in `SCOPE.md`?** Explicitly out: interactive apps (notepad,
|
|
||||||
calculator, games), active connectivity (RSS, news, browser), media/audio
|
|
||||||
playback. If it is out, say so and stop.
|
|
||||||
2. **Does it materially improve focused reading?** If the benefit is
|
|
||||||
"nice to have" or serves a different use case, it is out. This is not a PDA.
|
|
||||||
3. **What does it cost in RAM and in the largest-free-block budget?** A feature
|
|
||||||
that adds steady-state RAM or a large transient allocation needs a reading
|
|
||||||
benefit that clearly outweighs it. Quantify with `firmware_size_history.py`
|
|
||||||
and `script_profile_mem.sh` rather than guessing.
|
|
||||||
4. **Can it be done with no new code?** Prefer an existing activity, an existing
|
|
||||||
setting, or a doc over a new code path. The cheapest feature is the one
|
|
||||||
already built.
|
|
||||||
|
|
||||||
If a request fails the gate, push back with the specific reason and the
|
|
||||||
`SCOPE.md` basis, and offer the in-scope alternative. Make the call and say why;
|
|
||||||
do not just hand over a menu.
|
|
||||||
|
|
||||||
## Surface awareness
|
|
||||||
|
|
||||||
The firmware already carries dozens of activities. Each new one is permanent
|
|
||||||
RAM, permanent maintenance, and another thing every future refactor must not
|
|
||||||
break. Default to extending an existing activity or setting before adding a new
|
|
||||||
screen. New top-level surface needs a real justification, not "it would be
|
|
||||||
convenient."
|
|
||||||
|
|
||||||
## Settings are not free
|
|
||||||
|
|
||||||
A new setting is a field to persist, migrate, validate, translate, and render,
|
|
||||||
plus combinatorial test burden. Add one only when users genuinely need the
|
|
||||||
choice; otherwise pick a sensible fixed default.
|
|
||||||
|
|
||||||
## Self-review
|
|
||||||
|
|
||||||
- [ ] Checked against `SCOPE.md`; not on the out-of-scope list.
|
|
||||||
- [ ] Stated the concrete reading benefit, not a generic "useful."
|
|
||||||
- [ ] Named the RAM/size cost (measured, not guessed) and why the benefit wins.
|
|
||||||
- [ ] Checked whether an existing activity/setting/doc already covers it.
|
|
||||||
- [ ] New setting (if any) is justified by a real user need, not added
|
|
||||||
"just in case."
|
|
||||||
@@ -53,13 +53,6 @@ jobs:
|
|||||||
- name: Install PlatformIO Core
|
- name: Install PlatformIO Core
|
||||||
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
|
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
|
||||||
|
|
||||||
- name: Cache PlatformIO packages
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/.platformio
|
|
||||||
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
|
|
||||||
restore-keys: pio-${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Run cppcheck
|
- name: Run cppcheck
|
||||||
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
|
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
|
||||||
|
|
||||||
@@ -83,13 +76,6 @@ jobs:
|
|||||||
- name: Install PlatformIO Core
|
- name: Install PlatformIO Core
|
||||||
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
|
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
|
||||||
|
|
||||||
- name: Cache PlatformIO packages
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/.platformio
|
|
||||||
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
|
|
||||||
restore-keys: pio-${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Build CrossPoint
|
- name: Build CrossPoint
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -116,33 +102,6 @@ jobs:
|
|||||||
path: .pio/build/default/firmware.bin
|
path: .pio/build/default/firmware.bin
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
unit-tests:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- name: Install build tools
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y cmake ninja-build
|
|
||||||
|
|
||||||
- name: Cache googletest source
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: build/test/_deps/googletest-src
|
|
||||||
key: ${{ runner.os }}-googletest-${{ hashFiles('test/CMakeLists.txt') }}
|
|
||||||
|
|
||||||
- name: Configure
|
|
||||||
run: cmake -S test -B build/test -G Ninja -DCMAKE_BUILD_TYPE=Release
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: cmake --build build/test
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: ctest --test-dir build/test --output-on-failure -j
|
|
||||||
|
|
||||||
# This job is used as the PR required actions check, allows for changes to other steps in the future without breaking
|
# This job is used as the PR required actions check, allows for changes to other steps in the future without breaking
|
||||||
# PR requirements.
|
# PR requirements.
|
||||||
test-status:
|
test-status:
|
||||||
@@ -151,7 +110,6 @@ jobs:
|
|||||||
- build
|
- build
|
||||||
- clang-format
|
- clang-format
|
||||||
- cppcheck
|
- cppcheck
|
||||||
- unit-tests
|
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -25,13 +25,6 @@ jobs:
|
|||||||
- name: Install PlatformIO Core
|
- name: Install PlatformIO Core
|
||||||
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
|
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
|
||||||
|
|
||||||
- name: Cache PlatformIO packages
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/.platformio
|
|
||||||
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
|
|
||||||
restore-keys: pio-${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Build CrossPoint
|
- name: Build CrossPoint
|
||||||
run: pio run -e gh_release
|
run: pio run -e gh_release
|
||||||
|
|
||||||
|
|||||||
@@ -25,13 +25,6 @@ jobs:
|
|||||||
- name: Install PlatformIO Core
|
- name: Install PlatformIO Core
|
||||||
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
|
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
|
||||||
|
|
||||||
- name: Cache PlatformIO packages
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/.platformio
|
|
||||||
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
|
|
||||||
restore-keys: pio-${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Extract env
|
- name: Extract env
|
||||||
run: |
|
run: |
|
||||||
echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||||
|
|||||||
@@ -19,7 +19,3 @@ build
|
|||||||
lib/EpdFont/scripts/downloaded_fonts/
|
lib/EpdFont/scripts/downloaded_fonts/
|
||||||
lib/EpdFont/scripts/instanced_fonts/
|
lib/EpdFont/scripts/instanced_fonts/
|
||||||
lib/EpdFont/scripts/output/
|
lib/EpdFont/scripts/output/
|
||||||
# Claude Code: share project skills with the team, keep local agent state
|
|
||||||
# (worktrees, scheduled-task locks, settings.local, scout CLEANUP.md) out.
|
|
||||||
.claude/*
|
|
||||||
!.claude/skills/
|
|
||||||
+3
-4
@@ -1,4 +1,3 @@
|
|||||||
[submodule "freeink-sdk"]
|
[submodule "open-x4-sdk"]
|
||||||
path = freeink-sdk
|
path = open-x4-sdk
|
||||||
url = https://github.com/Free-Ink/freeink-sdk.git
|
url = https://github.com/crosspoint-reader/community-sdk.git
|
||||||
branch = main
|
|
||||||
+12
-10
@@ -7,7 +7,7 @@ Mission: Provide a lightweight, high-performance reading experience focused on E
|
|||||||
* Role: Senior Embedded Systems Engineer (ESP-IDF/Arduino-ESP32 specialized).
|
* Role: Senior Embedded Systems Engineer (ESP-IDF/Arduino-ESP32 specialized).
|
||||||
* Primary Constraint: 380KB RAM is the hard ceiling. Stability is non-negotiable.
|
* Primary Constraint: 380KB RAM is the hard ceiling. Stability is non-negotiable.
|
||||||
* Evidence-Based Reasoning: Before proposing a change, you MUST cite the specific file path and line numbers that justify the modification.
|
* Evidence-Based Reasoning: Before proposing a change, you MUST cite the specific file path and line numbers that justify the modification.
|
||||||
* Anti-Hallucination: Do not assume the existence of libraries or ESP-IDF functions. If you are unsure of an API's availability for the ESP32-C3 RISC-V target, check the freeink-sdk source or the FreeInk SDK docs (https://freeink.org/llms.txt for an LLM-readable index) first.
|
* Anti-Hallucination: Do not assume the existence of libraries or ESP-IDF functions. If you are unsure of an API's availability for the ESP32-C3 RISC-V target, check the open-x4-sdk or official docs first.
|
||||||
* No Unfounded Claims: Do not claim performance gains or memory savings without explaining the technical mechanism (e.g., DRAM vs IRAM usage).
|
* No Unfounded Claims: Do not claim performance gains or memory savings without explaining the technical mechanism (e.g., DRAM vs IRAM usage).
|
||||||
* Resource Justification: You must justify any new heap allocation (new, malloc, std::vector) or explain why a stack/static alternative was rejected.
|
* Resource Justification: You must justify any new heap allocation (new, malloc, std::vector) or explain why a stack/static alternative was rejected.
|
||||||
* Verification: After suggesting a fix, instruct the user on how to verify it (e.g., monitoring heap via Serial or checking a specific cache file).
|
* Verification: After suggesting a fix, instruct the user on how to verify it (e.g., monitoring heap via Serial or checking a specific cache file).
|
||||||
@@ -127,7 +127,7 @@ These flags in `platformio.ini` fundamentally affect firmware behavior:
|
|||||||
* lib/hal/: Hardware Abstraction Layer (HalDisplay, HalGPIO, HalStorage)
|
* lib/hal/: Hardware Abstraction Layer (HalDisplay, HalGPIO, HalStorage)
|
||||||
* lib/I18n/: Internationalization (translations in `translations/*.yaml`, generated string tables)
|
* lib/I18n/: Internationalization (translations in `translations/*.yaml`, generated string tables)
|
||||||
* src/activities/: UI logic using the Activity Lifecycle (onEnter, loop, onExit)
|
* src/activities/: UI logic using the Activity Lifecycle (onEnter, loop, onExit)
|
||||||
* freeink-sdk/: Low-level SDK (EInkDisplay, InputManager, BatteryMonitor, SDCardManager)
|
* open-x4-sdk/: Low-level SDK (EInkDisplay, InputManager, BatteryMonitor, SDCardManager)
|
||||||
* .crosspoint/: SD-based binary cache for EPUB metadata and pre-rendered layout sections
|
* .crosspoint/: SD-based binary cache for EPUB metadata and pre-rendered layout sections
|
||||||
|
|
||||||
### Hardware Abstraction Layer (HAL)
|
### Hardware Abstraction Layer (HAL)
|
||||||
@@ -152,19 +152,20 @@ These flags in `platformio.ini` fundamentally affect firmware behavior:
|
|||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
// Use Storage singleton (defined via macro)
|
// Use Storage singleton (defined via macro)
|
||||||
HalFile file;
|
FsFile file;
|
||||||
if (Storage.openFileForRead("MODULE", "/path/to/file.bin", file)) {
|
if (Storage.openFileForRead("MODULE", "/path/to/file.bin", file)) {
|
||||||
// Read from file
|
// Read from file
|
||||||
// No file.close() needed — DESTRUCTOR_CLOSES_FILE=1 handles it at scope exit
|
// No file.close() needed — DESTRUCTOR_CLOSES_FILE=1 handles it at scope exit
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Usage**: Use `HalFile` (the mutex-wrapping handle), NOT raw SdFat `FsFile` or Arduino `File`. Do NOT add `file.close()` for local variables (see DESTRUCTOR_CLOSES_FILE above).
|
**Usage**: See example above. Uses `FsFile` (SdFat), NOT Arduino `File`. Do NOT add `file.close()` for local variables (see DESTRUCTOR_CLOSES_FILE above).
|
||||||
|
|
||||||
**SdFat is not thread-safe; all SD access MUST go through HalStorage**:
|
**SdFat is not thread-safe; all SD access MUST go through HalStorage**:
|
||||||
- SdFat's `SdSpiCard` tracks SPI bus state with an unsynchronized `m_spiActive` bool. Two tasks calling SdFat concurrently can confuse that state machine and end with one task calling `SPIClass::endTransaction()` against a paramLock the *other* task is holding. That trips FreeRTOS's `xTaskPriorityDisinherit` assert (`tasks.c:5156, pxTCB == pxCurrentTCBs[0]`) and panics the system. See SdFat issue #518.
|
- SdFat's `SdSpiCard` tracks SPI bus state with an unsynchronized `m_spiActive` bool. Two tasks calling SdFat concurrently can confuse that state machine and end with one task calling `SPIClass::endTransaction()` against a paramLock the *other* task is holding. That trips FreeRTOS's `xTaskPriorityDisinherit` assert (`tasks.c:5156, pxTCB == pxCurrentTCBs[0]`) and panics the system. See SdFat issue #518.
|
||||||
- `HalStorage` serializes everything via `storageMutex`. Downstream code uses `HalFile` (declared in `<HalStorage.h>`); every method call (read, write, seek, close) takes the mutex. `HalFile`'s destructor also takes the mutex before letting the underlying SdFat `FsFile` close.
|
- `HalStorage` serializes everything via `storageMutex`. Downstream code includes `<HalStorage.h>`, which transparently `using FsFile = HalFile;`; every method call (read, write, seek, close) takes the mutex. `HalFile`'s destructor also takes the mutex before letting the underlying SdFat `FsFile` close.
|
||||||
- **Never** call into `SdFat` / `SdSpiCard` / `FsBaseFile` / `SDCardManager` / raw `FsFile` directly — that bypasses the mutex.
|
- **Never** call into `SdFat` / `SdSpiCard` / `FsBaseFile` / `SDCardManager` directly. **Never** define `HAL_STORAGE_IMPL` outside `HalStorage.cpp`; that disables the `FsFile -> HalFile` typedef and you'll get a raw SdFat handle that bypasses the mutex.
|
||||||
|
- If you're storing a raw `FsFile` in a place that won't transitively include `<HalStorage.h>` (rare), include the header explicitly so the typedef applies.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -457,6 +458,7 @@ void onExit() { /* free: vTaskDelete, free buffer, close member FsFiles */ Act
|
|||||||
**All fonts are loaded as global static objects** at firmware startup:
|
**All fonts are loaded as global static objects** at firmware startup:
|
||||||
- Noto Serif: 12, 14, 16, 18pt (4 styles each: regular, bold, italic, bold-italic)
|
- Noto Serif: 12, 14, 16, 18pt (4 styles each: regular, bold, italic, bold-italic)
|
||||||
- Noto Sans: 12, 14, 16, 18pt (4 styles each)
|
- Noto Sans: 12, 14, 16, 18pt (4 styles each)
|
||||||
|
- OpenDyslexic: 8, 10, 12, 14pt (4 styles each)
|
||||||
- Ubuntu UI fonts: 10, 12pt (2 styles)
|
- Ubuntu UI fonts: 10, 12pt (2 styles)
|
||||||
|
|
||||||
**Total**: ~80+ global `EpdFont` and `EpdFontFamily` objects
|
**Total**: ~80+ global `EpdFont` and `EpdFontFamily` objects
|
||||||
@@ -896,8 +898,8 @@ rm -rf /path/to/sd/.crosspoint/epub_<hash>/sections/
|
|||||||
**Source**: `lib/Epub/Epub/Section.cpp`, `lib/Epub/Epub/BookMetadataCache.cpp`
|
**Source**: `lib/Epub/Epub/Section.cpp`, `lib/Epub/Epub/BookMetadataCache.cpp`
|
||||||
|
|
||||||
**Current Versions** (as of docs/file-formats.md):
|
**Current Versions** (as of docs/file-formats.md):
|
||||||
- `book.bin`: **Version 7** (metadata structure)
|
- `book.bin`: **Version 5** (metadata structure)
|
||||||
- `section.bin`: **Version 25** (layout structure)
|
- `section.bin`: **Version 12** (layout structure)
|
||||||
|
|
||||||
**Version Increment Rules**:
|
**Version Increment Rules**:
|
||||||
1. **ALWAYS increment version** BEFORE changing binary structure
|
1. **ALWAYS increment version** BEFORE changing binary structure
|
||||||
@@ -907,7 +909,7 @@ rm -rf /path/to/sd/.crosspoint/epub_<hash>/sections/
|
|||||||
**Example** (incrementing section format version):
|
**Example** (incrementing section format version):
|
||||||
```cpp
|
```cpp
|
||||||
// lib/Epub/Epub/Section.cpp
|
// lib/Epub/Epub/Section.cpp
|
||||||
static constexpr uint8_t SECTION_FILE_VERSION = 26; // Was 25, now 26
|
static constexpr uint8_t SECTION_FILE_VERSION = 13; // Was 12, now 13
|
||||||
|
|
||||||
// Add new field to structure
|
// Add new field to structure
|
||||||
struct PageLine {
|
struct PageLine {
|
||||||
@@ -918,4 +920,4 @@ struct PageLine {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Philosophy: We are building a dedicated e-reader, not a Swiss Army knife. If a feature adds RAM pressure without significantly improving the reading experience, it is Out of Scope.
|
Philosophy: We are building a dedicated e-reader, not a Swiss Army knife. If a feature adds RAM pressure without significantly improving the reading experience, it is Out of Scope.
|
||||||
@@ -10,7 +10,7 @@ CrossPoint is open-source e-reader firmware - community-built, fully hackable, f
|
|||||||
|
|
||||||
## What can CrossPoint do?
|
## What can CrossPoint do?
|
||||||
|
|
||||||
- **Reader engine**: EPUB 2/3 rendering with embedded-style option, image handling, hyphenation, kerning, chapter navigation, footnotes, bookmarks, go-to-percent, auto page turn, orientation control, focus reading, KOReader progress sync and more.
|
- **Reader engine**: EPUB 2/3 rendering with embedded-style option, image handling, hyphenation, kerning, chapter navigation, footnotes, go-to-percent, auto page turn, orientation control, focus reading, KOReader progress sync and more.
|
||||||
|
|
||||||
- **Various formats**: native handling for `.epub`, `.xtc/.xtch`, `.txt`, and `.bmp`.
|
- **Various formats**: native handling for `.epub`, `.xtc/.xtch`, `.txt`, and `.bmp`.
|
||||||
|
|
||||||
@@ -29,17 +29,21 @@ CrossPoint is open-source e-reader firmware - community-built, fully hackable, f
|
|||||||
- Web settings UI/API (edit many device settings from browser)
|
- Web settings UI/API (edit many device settings from browser)
|
||||||
- WebSocket fast uploads
|
- WebSocket fast uploads
|
||||||
- WebDAV handler
|
- WebDAV handler
|
||||||
- AP mode (hotspot) and STA mode (join existing Wi-Fi), both with QR helpers
|
- AP mode (hotspot) and STA mode (join existing WiFi), both with QR helpers
|
||||||
- Calibre wireless connect flow
|
- Calibre wireless connect flow
|
||||||
- OPDS browser with saved servers (up to 8), search, pagination, and direct download
|
- OPDS browser with saved servers (up to 8), search, pagination, and direct download
|
||||||
- OTA update checks and installs from GitHub releases
|
- OTA update checks and installs from GitHub releases
|
||||||
|
|
||||||
- **Customization**: multiple themes (Classic, Lyra, Lyra Extended, RoundedRaff), sleep screen modes, front/side button remapping, status bar controls, power-button behavior, refresh cadence, and more.
|
- **Customization**: multiple themes (Classic, Lyra, Lyra Extended, RoundedRaff), sleep screen modes, front/side button remapping, status bar controls, power-button behavior, refresh cadence, and more.
|
||||||
|
|
||||||
- **Localization**: 24 UI languages and counting. RTL support.
|
- **Localization**: 22 UI languages and counting.
|
||||||
|
|
||||||
### Coming soon:
|
### Coming soon:
|
||||||
|
|
||||||
|
- RTL support — Arabic, Hebrew, and Farsi.
|
||||||
|
|
||||||
|
- Bookmarks.
|
||||||
|
|
||||||
- Dictionary lookup — inline word lookup without leaving the reader.
|
- Dictionary lookup — inline word lookup without leaving the reader.
|
||||||
|
|
||||||
- More themes.
|
- More themes.
|
||||||
@@ -131,23 +135,6 @@ Convert your own TTF/OTF files into `.cpfont` files that load from the SD card.
|
|||||||
|
|
||||||
Conversion runs the firmware repo's `lib/EpdFont/scripts/fontconvert_sdcard.py` script unmodified, so output matches a local host build.
|
Conversion runs the firmware repo's `lib/EpdFont/scripts/fontconvert_sdcard.py` script unmodified, so output matches a local host build.
|
||||||
|
|
||||||
## Custom SD-card themes
|
|
||||||
|
|
||||||
Downloadable themes are packaged in the tools repo under `../crosspoint-tools/public/themes/<theme-id>/`. Each theme folder must contain a `theme.json`; optional assets such as generated BMP icons live beside it, usually under `icons/`.
|
|
||||||
|
|
||||||
See [SD-card theme creation](./docs/theme-creation.md) for the full JSON format, device-specific overrides, icon generation, CrossInk extension fields, and packaging rules.
|
|
||||||
|
|
||||||
After adding or changing a hosted theme, regenerate the download manifest:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 scripts/generate-theme-manifest.py \
|
|
||||||
--root ../crosspoint-tools/public/themes \
|
|
||||||
--base-url http://crosspointreader.com/themes \
|
|
||||||
--output ../crosspoint-tools/public/themes/themes.json
|
|
||||||
```
|
|
||||||
|
|
||||||
The script scans every theme folder, includes every file in each package, and writes size and CRC32 values used by the device downloader. Commit changed theme package files and the regenerated `themes.json` in `crosspoint-tools`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
@@ -155,7 +142,6 @@ The script scans every theme folder, includes every file in each package, and wr
|
|||||||
- [User Guide](./USER_GUIDE.md)
|
- [User Guide](./USER_GUIDE.md)
|
||||||
- [Web server usage](./docs/webserver.md)
|
- [Web server usage](./docs/webserver.md)
|
||||||
- [Web server endpoints](./docs/webserver-endpoints.md)
|
- [Web server endpoints](./docs/webserver-endpoints.md)
|
||||||
- [SD-card theme creation](./docs/theme-creation.md)
|
|
||||||
- [Project scope](./SCOPE.md)
|
- [Project scope](./SCOPE.md)
|
||||||
- [Contributing docs](./docs/contributing/README.md)
|
- [Contributing docs](./docs/contributing/README.md)
|
||||||
|
|
||||||
@@ -225,7 +211,7 @@ CrossPoint Reader is pretty aggressive about caching data down to the SD card to
|
|||||||
|
|
||||||
### Data caching
|
### Data caching
|
||||||
|
|
||||||
The first time chapters of a book are loaded, they are cached to the SD card. Subsequent loads are served from the
|
The first time chapters of a book are loaded, they are cached to the SD card. Subsequent loads are served from the
|
||||||
cache. This cache directory exists at `.crosspoint` on the SD card. The structure is as follows:
|
cache. This cache directory exists at `.crosspoint` on the SD card. The structure is as follows:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -234,18 +220,13 @@ cache. This cache directory exists at `.crosspoint` on the SD card. The structur
|
|||||||
│ ├── progress.bin # reading position (chapter, page, etc.)
|
│ ├── progress.bin # reading position (chapter, page, etc.)
|
||||||
│ ├── cover.bmp # generated cover image
|
│ ├── cover.bmp # generated cover image
|
||||||
│ ├── book.bin # metadata: title, author, spine, TOC
|
│ ├── book.bin # metadata: title, author, spine, TOC
|
||||||
│ ├── css_rules.cache # parsed CSS rule cache
|
|
||||||
│ ├── img_* # rendered image cache files
|
|
||||||
│ └── sections/ # per-chapter layout cache
|
│ └── sections/ # per-chapter layout cache
|
||||||
│ ├── 0.bin
|
│ ├── 0.bin
|
||||||
│ ├── 1.bin
|
│ ├── 1.bin
|
||||||
│ └── ...
|
│ └── ...
|
||||||
├── settings.json # device settings
|
|
||||||
├── state.json # resume/runtime state
|
|
||||||
└── recent.json # recent books list
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Removing `/.crosspoint` clears all cached metadata and forces a full regeneration on next open. Book deletes, overwrites, and moves done through the firmware or web UI clear or re-key matching caches; manual SD-card edits may leave stale cache directories behind.
|
Removing `/.crosspoint` clears all cached metadata and forces a full regeneration on next open. Note: the cache isn't cleared automatically when you delete a book, and moving a file to a new path resets its reading progress.
|
||||||
|
|
||||||
For more details on the internal file structures, see the [file formats document](./docs/file-formats.md).
|
For more details on the internal file structures, see the [file formats document](./docs/file-formats.md).
|
||||||
|
|
||||||
@@ -269,6 +250,8 @@ One of the best things about open source is that anyone can take the code in a d
|
|||||||
|
|
||||||
- [crosspet](https://github.com/trilwu/crosspet) — A Vietnamese fork that adds a Tamagotchi-style virtual chicken that grows based on your reading milestones (pages read, streaks, care). Also: Flashcards, Weather, Pomodoro timer, and mini-games.
|
- [crosspet](https://github.com/trilwu/crosspet) — A Vietnamese fork that adds a Tamagotchi-style virtual chicken that grows based on your reading milestones (pages read, streaks, care). Also: Flashcards, Weather, Pomodoro timer, and mini-games.
|
||||||
|
|
||||||
|
- [crosspoint-reader (jpirnay)](https://github.com/jpirnay/crosspoint-reader) — Faster integration of functionality. Tracks upstream PRs and integrates the good ones ahead of the official merge.
|
||||||
|
|
||||||
- [crosspoint-reader-cjk](https://github.com/aBER0724/crosspoint-reader-cjk) — Purpose-built for Chinese, Japanese, and Korean reading.
|
- [crosspoint-reader-cjk](https://github.com/aBER0724/crosspoint-reader-cjk) — Purpose-built for Chinese, Japanese, and Korean reading.
|
||||||
|
|
||||||
- [inx](https://github.com/obijuankenobiii/inx) — Completely reimagines the user interface with tabbed navigation.
|
- [inx](https://github.com/obijuankenobiii/inx) — Completely reimagines the user interface with tabbed navigation.
|
||||||
@@ -277,8 +260,6 @@ One of the best things about open source is that anyone can take the code in a d
|
|||||||
|
|
||||||
- [crosspoint-reader-papers3](https://github.com/juicecultus/crosspoint-reader-papers3) — Crosspoint port for M5Stack Paper S3.
|
- [crosspoint-reader-papers3](https://github.com/juicecultus/crosspoint-reader-papers3) — Crosspoint port for M5Stack Paper S3.
|
||||||
|
|
||||||
- [t5s3-reader](https://github.com/ShallowGreen123/t5s3-reader) — Crosspoint port for LilyGo T5 ePaper S3 / T5S3 4.7-inch e-paper device.
|
|
||||||
|
|
||||||
**Note:** Many of these features will make their way into CrossPoint over time. We maintain a slower pace to ensure rock-solid stability and squash bugs before they reach your device.
|
**Note:** Many of these features will make their way into CrossPoint over time. We maintain a slower pace to ensure rock-solid stability and squash bugs before they reach your device.
|
||||||
|
|
||||||
Want to build your own device? Be sure to check out the [de-link](https://github.com/iandchasse/de-link) project.
|
Want to build your own device? Be sure to check out the [de-link](https://github.com/iandchasse/de-link) project.
|
||||||
|
|||||||
+62
-287
File diff suppressed because it is too large.
Load diff
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Formats all C/C++ source and header files in the repository, excluding
|
Formats all C/C++ source and header files in the repository, excluding
|
||||||
generated, vendored, and build directories (freeink-sdk, builtinFonts,
|
generated, vendored, and build directories (open-x4-sdk, builtinFonts,
|
||||||
hyphenation tries, uzlib, .pio, *.generated.h).
|
hyphenation tries, uzlib, .pio, *.generated.h).
|
||||||
|
|
||||||
The clang-format binary path is resolved once and cached in
|
The clang-format binary path is resolved once and cached in
|
||||||
@@ -92,7 +92,7 @@ function Resolve-ClangFormat {
|
|||||||
$clangFormat = Resolve-ClangFormat
|
$clangFormat = Resolve-ClangFormat
|
||||||
|
|
||||||
$exclude = @(
|
$exclude = @(
|
||||||
'freeink-sdk'
|
'open-x4-sdk'
|
||||||
'lib\EpdFont\builtinFonts'
|
'lib\EpdFont\builtinFonts'
|
||||||
'lib\Epub\Epub\hyphenation\generated'
|
'lib\Epub\Epub\hyphenation\generated'
|
||||||
'lib\uzlib'
|
'lib\uzlib'
|
||||||
|
|||||||
@@ -8,18 +8,17 @@ At a high level, it is firmware that uses an activity-driven application archite
|
|||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
graph TD
|
||||||
A[Hardware: ESP32-C3 + SD + E-ink + Buttons] --> B[freeink-sdk]
|
A[Hardware: ESP32-C3 + SD + E-ink + Buttons] --> B[open-x4-sdk HAL]
|
||||||
B --> C[lib/hal wrappers]
|
B --> C[src/main.cpp runtime loop]
|
||||||
C --> D[src/main.cpp runtime loop]
|
C --> D[Activities layer]
|
||||||
D --> E[Activities layer]
|
C --> E[State and settings]
|
||||||
D --> F[State and settings]
|
D --> F[Reader flows]
|
||||||
E --> G[Reader flows]
|
D --> G[Home/Library/Settings flows]
|
||||||
E --> H[Home/Library/Settings flows]
|
D --> H[Network/Web server flows]
|
||||||
E --> I[Network/Web server flows]
|
F --> I[lib/Epub parsing + layout + hyphenation]
|
||||||
G --> J[lib/Epub parsing + layout + hyphenation]
|
I --> J[SD cache in .crosspoint]
|
||||||
J --> K[SD cache in .crosspoint]
|
D --> K[GfxRenderer]
|
||||||
E --> L[GfxRenderer]
|
K --> L[E-ink display buffer]
|
||||||
L --> M[E-ink display buffer]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Runtime lifecycle
|
## Runtime lifecycle
|
||||||
@@ -59,7 +58,7 @@ Top-level activity groups:
|
|||||||
- `src/activities/home/`: home and library navigation
|
- `src/activities/home/`: home and library navigation
|
||||||
- `src/activities/reader/`: EPUB/XTC/TXT reading flows
|
- `src/activities/reader/`: EPUB/XTC/TXT reading flows
|
||||||
- `src/activities/settings/`: settings menus and configuration
|
- `src/activities/settings/`: settings menus and configuration
|
||||||
- `src/activities/network/`: Wi-Fi selection, AP/STA mode, file transfer server
|
- `src/activities/network/`: WiFi selection, AP/STA mode, file transfer server
|
||||||
- `src/activities/boot_sleep/`: boot and sleep transitions
|
- `src/activities/boot_sleep/`: boot and sleep transitions
|
||||||
|
|
||||||
## Reader and content pipeline
|
## Reader and content pipeline
|
||||||
@@ -74,11 +73,10 @@ flowchart LR
|
|||||||
C -->|EPUB| D[lib/Epub/Epub]
|
C -->|EPUB| D[lib/Epub/Epub]
|
||||||
C -->|XTC| E[lib/Xtc reader]
|
C -->|XTC| E[lib/Xtc reader]
|
||||||
C -->|TXT| F[lib/Txt reader]
|
C -->|TXT| F[lib/Txt reader]
|
||||||
D --> G[Parse OPF/TOC and collect CSS refs]
|
D --> G[Parse OPF/TOC/CSS]
|
||||||
G --> H[Build/load book.bin and css_rules.cache]
|
G --> H[Layout pages/sections]
|
||||||
H --> I[Layout pages/sections]
|
H --> I[Write section and metadata caches]
|
||||||
I --> J[Write section cache]
|
I --> J[Render current page via GfxRenderer]
|
||||||
J --> K[Render current page via GfxRenderer]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Why caching matters:
|
Why caching matters:
|
||||||
@@ -100,7 +98,7 @@ flowchart TD
|
|||||||
D --> E[Locate container and OPF]
|
D --> E[Locate container and OPF]
|
||||||
E --> F[Build or load BookMetadataCache]
|
E --> F[Build or load BookMetadataCache]
|
||||||
F --> G[Load TOC and spine]
|
F --> G[Load TOC and spine]
|
||||||
G --> H[Load CSS cache or parse manifest/base-dir CSS]
|
G --> H[Load or parse CSS rules]
|
||||||
|
|
||||||
H --> I[EpubReaderActivity]
|
H --> I[EpubReaderActivity]
|
||||||
I --> J{Section cache exists for current settings?}
|
I --> J{Section cache exists for current settings?}
|
||||||
@@ -123,12 +121,7 @@ flowchart TD
|
|||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
- CSS files are collected from the OPF manifest and, when needed, discovered by
|
- "section cache exists" depends on cache-busting parameters such as font and layout-related settings
|
||||||
streaming ZIP paths under the OPF content base directory; the firmware avoids
|
|
||||||
preloading the full ZIP central directory for large books.
|
|
||||||
- "section cache exists" depends on cache-busting parameters such as font,
|
|
||||||
viewport size, paragraph alignment, hyphenation, embedded CSS, image rendering,
|
|
||||||
and Focus Reading settings
|
|
||||||
- rendering favors reusing precomputed layout data to keep page turns responsive on constrained hardware
|
- 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
|
- progress/session state is persisted so the reader can reopen at the last position after reboot/sleep
|
||||||
|
|
||||||
@@ -145,18 +138,14 @@ Typical persisted areas on SD:
|
|||||||
/.crosspoint/
|
/.crosspoint/
|
||||||
epub_<hash>/
|
epub_<hash>/
|
||||||
book.bin
|
book.bin
|
||||||
css_rules.cache
|
|
||||||
progress.bin
|
progress.bin
|
||||||
cover.bmp
|
cover.bmp
|
||||||
sections/*.bin
|
sections/*.bin
|
||||||
img_* cache files
|
settings.bin
|
||||||
settings.json
|
state.bin
|
||||||
state.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`sections/*.bin` contains rendered pages plus anchor, paragraph, and list-item
|
For binary cache formats, see `docs/file-formats.md`.
|
||||||
lookup tables used for TOC/footnote jumps and KOReader sync refinement. For
|
|
||||||
binary cache formats, see `docs/file-formats.md`.
|
|
||||||
|
|
||||||
## Networking architecture
|
## Networking architecture
|
||||||
|
|
||||||
@@ -164,18 +153,14 @@ Network file transfer is controlled by `src/activities/network/CrossPointWebServ
|
|||||||
|
|
||||||
Modes:
|
Modes:
|
||||||
|
|
||||||
- STA: join existing Wi-Fi network
|
- STA: join existing WiFi network
|
||||||
- AP: create hotspot
|
- AP: create hotspot
|
||||||
- Calibre Wireless: STA flow specialized for Calibre plugin uploads
|
|
||||||
|
|
||||||
Server behavior:
|
Server behavior:
|
||||||
|
|
||||||
- HTTP server on port 80
|
- HTTP server on port 80
|
||||||
- WebSocket upload server on port 81
|
- WebSocket upload server on port 81
|
||||||
- WebDAV handler on the HTTP server
|
|
||||||
- UDP discovery listener for upload clients
|
|
||||||
- file operations backed by SD storage
|
- file operations backed by SD storage
|
||||||
- browser APIs for file management, settings, fonts, OPDS servers, and saved Wi-Fi networks
|
|
||||||
- activity requests faster loop responsiveness while server is running
|
- activity requests faster loop responsiveness while server is running
|
||||||
|
|
||||||
Endpoint reference: `docs/webserver-endpoints.md`.
|
Endpoint reference: `docs/webserver-endpoints.md`.
|
||||||
@@ -185,7 +170,6 @@ Endpoint reference: `docs/webserver-endpoints.md`.
|
|||||||
Some sources are generated and should not be edited manually.
|
Some sources are generated and should not be edited manually.
|
||||||
|
|
||||||
- `scripts/build_html.py` generates `src/network/html/*.generated.h` from HTML files
|
- `scripts/build_html.py` generates `src/network/html/*.generated.h` from HTML files
|
||||||
- `scripts/gen_i18n.py` generates `lib/I18n/I18nKeys.h`, `I18nStrings.h`, and `I18nStrings.cpp`
|
|
||||||
- `scripts/generate_hyphenation_trie.py` generates hyphenation headers under `lib/Epub/Epub/hyphenation/generated/`
|
- `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.
|
When editing related source assets, regenerate via normal build steps/scripts.
|
||||||
@@ -195,10 +179,9 @@ When editing related source assets, regenerate via normal build steps/scripts.
|
|||||||
- `src/`: app orchestration, settings/state, and activity implementations
|
- `src/`: app orchestration, settings/state, and activity implementations
|
||||||
- `src/network/`: web server and OTA/update networking
|
- `src/network/`: web server and OTA/update networking
|
||||||
- `src/components/`: theming and shared UI components
|
- `src/components/`: theming and shared UI components
|
||||||
- `lib/hal/`: hardware abstraction wrappers around freeink-sdk
|
|
||||||
- `lib/Epub/`: EPUB parser, layout, CSS handling, and hyphenation
|
- `lib/Epub/`: EPUB parser, layout, CSS handling, and hyphenation
|
||||||
- `lib/`: supporting libraries (fonts, text, filesystem helpers, etc.)
|
- `lib/`: supporting libraries (fonts, text, filesystem helpers, etc.)
|
||||||
- `freeink-sdk/`: hardware SDK submodule (display, input, storage, battery). Docs: https://freeink.org/docs
|
- `open-x4-sdk/`: hardware SDK submodule (display, input, storage, battery)
|
||||||
- `docs/`: user and technical documentation
|
- `docs/`: user and technical documentation
|
||||||
|
|
||||||
## Embedded constraints that shape design
|
## Embedded constraints that shape design
|
||||||
|
|||||||
+87
-153
@@ -1,26 +1,22 @@
|
|||||||
# File Formats
|
# File Formats
|
||||||
|
|
||||||
These formats describe the SD-card cache files under `/.crosspoint/epub_<hash>/`.
|
|
||||||
All POD fields are written in the ESP32 little-endian representation used by
|
|
||||||
`Serialization.h`; strings are length-prefixed UTF-8.
|
|
||||||
|
|
||||||
## `book.bin`
|
## `book.bin`
|
||||||
|
|
||||||
### Version 7
|
### Version 3
|
||||||
|
|
||||||
`book.bin` stores EPUB metadata plus lookup tables for spine and TOC entries.
|
ImHex Pattern:
|
||||||
The current firmware writes this version from `BookMetadataCache`.
|
|
||||||
|
|
||||||
ImHex pattern:
|
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
import std.mem;
|
import std.mem;
|
||||||
import std.string;
|
import std.string;
|
||||||
import std.core;
|
import std.core;
|
||||||
|
|
||||||
#define EXPECTED_VERSION 7
|
// === Configuration ===
|
||||||
|
#define EXPECTED_VERSION 3
|
||||||
#define MAX_STRING_LENGTH 65535
|
#define MAX_STRING_LENGTH 65535
|
||||||
|
|
||||||
|
// === String Structure ===
|
||||||
|
|
||||||
struct String {
|
struct String {
|
||||||
u32 length [[hidden, comment("String byte length")]];
|
u32 length [[hidden, comment("String byte length")]];
|
||||||
if (length > MAX_STRING_LENGTH) {
|
if (length > MAX_STRING_LENGTH) {
|
||||||
@@ -33,56 +29,74 @@ fn format_string(String s) {
|
|||||||
return s.data;
|
return s.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// === Metadata Structure ===
|
||||||
|
|
||||||
struct Metadata {
|
struct Metadata {
|
||||||
String title [[comment("Book title")]];
|
String title [[comment("Book title")]];
|
||||||
String author [[comment("Book author")]];
|
String author [[comment("Book author")]];
|
||||||
String language [[comment("Book language code")]];
|
|
||||||
String coverItemHref [[comment("Path to cover image")]];
|
String coverItemHref [[comment("Path to cover image")]];
|
||||||
String textReferenceHref [[comment("Path to guided first text reference")]];
|
String textReferenceHref [[comment("Path to guided first text reference")]];
|
||||||
};
|
} [[comment("Book metadata information")]];
|
||||||
|
|
||||||
|
// === Spine Entry Structure ===
|
||||||
|
|
||||||
struct SpineEntry {
|
struct SpineEntry {
|
||||||
String href [[comment("Resource path")]];
|
String href [[comment("Resource path")]];
|
||||||
u32 cumulativeSize [[comment("Cumulative uncompressed spine size through this entry")]];
|
u32 cumulativeSize [[comment("Cumulative size in bytes"), color("FF6B6B")]];
|
||||||
s16 tocIndex [[comment("Index into TOC, or inherited/previous TOC index when no direct entry exists")]];
|
s16 tocIndex [[comment("Index into TOC (-1 if none)"), color("4ECDC4")]];
|
||||||
};
|
} [[comment("Spine entry defining reading order")]];
|
||||||
|
|
||||||
|
// === TOC Entry Structure ===
|
||||||
|
|
||||||
struct TocEntry {
|
struct TocEntry {
|
||||||
String title [[comment("Chapter/section title")]];
|
String title [[comment("Chapter/section title")]];
|
||||||
String href [[comment("Resource path")]];
|
String href [[comment("Resource path")]];
|
||||||
String anchor [[comment("Fragment identifier")]];
|
String anchor [[comment("Fragment identifier")]];
|
||||||
u8 level [[comment("Nesting level")]];
|
u8 level [[comment("Nesting level (0-255)"), color("95E1D3")]];
|
||||||
s16 spineIndex [[comment("Index into spine (-1 if none)")]];
|
s16 spineIndex [[comment("Index into spine (-1 if none)"), color("F38181")]];
|
||||||
};
|
} [[comment("Table of contents entry")]];
|
||||||
|
|
||||||
|
// === Book Bin Structure ===
|
||||||
|
|
||||||
struct BookBin {
|
struct BookBin {
|
||||||
u8 version;
|
// Header
|
||||||
|
u8 version [[comment("Format version"), color("FFD93D")]];
|
||||||
|
|
||||||
|
// Version validation
|
||||||
if (version != EXPECTED_VERSION) {
|
if (version != EXPECTED_VERSION) {
|
||||||
std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION));
|
std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION));
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 lutOffset [[comment("Offset to lookup tables")]];
|
u32 lutOffset [[comment("Offset to lookup tables"), color("6BCB77")]];
|
||||||
u16 spineCount;
|
u16 spineCount [[comment("Number of spine entries"), color("4D96FF")]];
|
||||||
u16 tocCount;
|
u16 tocCount [[comment("Number of TOC entries"), color("FF6B9D")]];
|
||||||
|
|
||||||
Metadata metadata;
|
// Metadata section
|
||||||
|
Metadata metadata [[comment("Book metadata")]];
|
||||||
|
|
||||||
|
// Validate LUT offset alignment
|
||||||
u32 currentOffset = $;
|
u32 currentOffset = $;
|
||||||
if (currentOffset != lutOffset) {
|
if (currentOffset != lutOffset) {
|
||||||
std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset));
|
std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset));
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 spineLut[spineCount] [[comment("Spine entry offsets")]];
|
// Lookup Tables
|
||||||
u32 tocLut[tocCount] [[comment("TOC entry offsets")]];
|
u32 spineLut[spineCount] [[comment("Spine entry offsets"), color("4D96FF")]];
|
||||||
|
u32 tocLut[tocCount] [[comment("TOC entry offsets"), color("FF6B9D")]];
|
||||||
|
|
||||||
SpineEntry spines[spineCount];
|
// Data Entries
|
||||||
TocEntry toc[tocCount];
|
SpineEntry spines[spineCount] [[comment("Spine entries (reading order)")]];
|
||||||
|
TocEntry toc[tocCount] [[comment("Table of contents entries")]];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// === File Parsing ===
|
||||||
|
|
||||||
BookBin book @ 0x00;
|
BookBin book @ 0x00;
|
||||||
|
|
||||||
|
// Validate we've consumed the entire file
|
||||||
u32 fileSize = std::mem::size();
|
u32 fileSize = std::mem::size();
|
||||||
u32 parsedSize = $;
|
u32 parsedSize = $;
|
||||||
|
|
||||||
if (parsedSize != fileSize) {
|
if (parsedSize != fileSize) {
|
||||||
std::warning(std::format("Unparsed data detected: {} bytes remaining at offset 0x{:X}", fileSize - parsedSize, parsedSize));
|
std::warning(std::format("Unparsed data detected: {} bytes remaining at offset 0x{:X}", fileSize - parsedSize, parsedSize));
|
||||||
}
|
}
|
||||||
@@ -90,33 +104,20 @@ if (parsedSize != fileSize) {
|
|||||||
|
|
||||||
## `section.bin`
|
## `section.bin`
|
||||||
|
|
||||||
### Version 25
|
### Version 24
|
||||||
|
|
||||||
Each file in `sections/*.bin` stores one laid-out spine section. The header is
|
ImHex Pattern:
|
||||||
also the cache-busting key: if any layout-affecting setting differs from the
|
|
||||||
current reader settings, the section is discarded and rebuilt.
|
|
||||||
|
|
||||||
Version 25 includes:
|
|
||||||
|
|
||||||
- cache-busting fields for paragraph alignment, hyphenation, embedded CSS,
|
|
||||||
image rendering mode, and Focus Reading
|
|
||||||
- page offset LUT
|
|
||||||
- anchor-to-page map for fragment and footnote navigation
|
|
||||||
- paragraph and list-item LUTs used by KOReader sync page refinement
|
|
||||||
- optional per-word Focus Reading split metadata
|
|
||||||
- per-page footnote entries
|
|
||||||
|
|
||||||
ImHex pattern:
|
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
import std.mem;
|
import std.mem;
|
||||||
import std.string;
|
import std.string;
|
||||||
import std.core;
|
import std.core;
|
||||||
|
|
||||||
#define EXPECTED_VERSION 25
|
// === Configuration ===
|
||||||
|
#define EXPECTED_VERSION 24
|
||||||
#define MAX_STRING_LENGTH 65535
|
#define MAX_STRING_LENGTH 65535
|
||||||
#define FOOTNOTE_NUMBER_LEN 32
|
|
||||||
#define FOOTNOTE_HREF_LEN 96
|
// === String Structure ===
|
||||||
|
|
||||||
struct String {
|
struct String {
|
||||||
u32 length [[hidden, comment("String byte length")]];
|
u32 length [[hidden, comment("String byte length")]];
|
||||||
@@ -130,79 +131,44 @@ fn format_string(String s) {
|
|||||||
return s.data;
|
return s.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// === Page Structure ===
|
||||||
|
|
||||||
enum PageElementTag : u8 {
|
enum PageElementTag : u8 {
|
||||||
TAG_PageLine = 1,
|
PageLine = 1,
|
||||||
TAG_PageImage = 2,
|
PageImage = 2,
|
||||||
TAG_PageHorizontalRule = 3
|
PageHorizontalRule = 3
|
||||||
};
|
};
|
||||||
|
|
||||||
enum WordStyle : u8 {
|
enum WordStyle : u8 {
|
||||||
REGULAR = 0,
|
REGULAR = 0,
|
||||||
BOLD = 1,
|
BOLD = 1,
|
||||||
ITALIC = 2,
|
ITALIC = 2,
|
||||||
BOLD_ITALIC = 3,
|
BOLD_ITALIC = 3
|
||||||
UNDERLINE = 4,
|
|
||||||
STRIKETHROUGH = 8,
|
|
||||||
SUP = 16,
|
|
||||||
SUB = 32
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum TextAlign : u8 {
|
enum BlockStyle : u8 {
|
||||||
JUSTIFIED = 0,
|
JUSTIFIED = 0,
|
||||||
LEFT_ALIGN = 1,
|
LEFT_ALIGN = 1,
|
||||||
CENTER_ALIGN = 2,
|
CENTER_ALIGN = 2,
|
||||||
RIGHT_ALIGN = 3,
|
RIGHT_ALIGN = 3,
|
||||||
NONE = 4
|
|
||||||
};
|
|
||||||
|
|
||||||
struct BlockStyle {
|
|
||||||
TextAlign alignment;
|
|
||||||
bool textAlignDefined;
|
|
||||||
s16 marginTop;
|
|
||||||
s16 marginBottom;
|
|
||||||
s16 marginLeft;
|
|
||||||
s16 marginRight;
|
|
||||||
s16 paddingTop;
|
|
||||||
s16 paddingBottom;
|
|
||||||
s16 paddingLeft;
|
|
||||||
s16 paddingRight;
|
|
||||||
s16 textIndent;
|
|
||||||
bool textIndentDefined;
|
|
||||||
bool isRtl;
|
|
||||||
bool directionDefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct TextBlock {
|
|
||||||
u16 wordCount;
|
|
||||||
String words[wordCount];
|
|
||||||
s16 wordXPos[wordCount];
|
|
||||||
WordStyle wordStyle[wordCount];
|
|
||||||
|
|
||||||
u8 hasFocus;
|
|
||||||
if (hasFocus != 0) {
|
|
||||||
u8 wordFocusBoundary[wordCount] [[comment("UTF-8 byte boundary between bold prefix and suffix")]];
|
|
||||||
u16 wordFocusSuffixX[wordCount] [[comment("Suffix x offset from word start")]];
|
|
||||||
}
|
|
||||||
|
|
||||||
BlockStyle blockStyle;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ImageBlock {
|
|
||||||
String imagePath;
|
|
||||||
s16 width;
|
|
||||||
s16 height;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PageLine {
|
struct PageLine {
|
||||||
s16 xPos;
|
s16 xPos;
|
||||||
s16 yPos;
|
s16 yPos;
|
||||||
TextBlock block;
|
u16 wordCount;
|
||||||
|
String words[wordCount];
|
||||||
|
u16 wordXPos[wordCount];
|
||||||
|
WordStyle wordStyle[wordCount];
|
||||||
|
BlockStyle blockStyle;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PageImage {
|
struct PageImage {
|
||||||
s16 xPos;
|
s16 xPos;
|
||||||
s16 yPos;
|
s16 yPos;
|
||||||
ImageBlock image;
|
String imagePath;
|
||||||
|
s16 width;
|
||||||
|
s16 height;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PageHorizontalRule {
|
struct PageHorizontalRule {
|
||||||
@@ -213,95 +179,63 @@ struct PageHorizontalRule {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct PageElement {
|
struct PageElement {
|
||||||
PageElementTag pageElementType;
|
u8 pageElementType;
|
||||||
if (pageElementType == TAG_PageLine) {
|
if (pageElementType == 1) {
|
||||||
PageLine pageLine [[inline]];
|
PageLine pageLine [[inline]];
|
||||||
} else if (pageElementType == TAG_PageImage) {
|
} else if (pageElementType == 2) {
|
||||||
PageImage pageImage [[inline]];
|
PageImage pageImage [[inline]];
|
||||||
} else if (pageElementType == TAG_PageHorizontalRule) {
|
} else if (pageElementType == 3) {
|
||||||
PageHorizontalRule horizontalRule [[inline]];
|
PageHorizontalRule horizontalRule [[inline]];
|
||||||
} else {
|
} else {
|
||||||
std::error(std::format("Unknown page element type: {}", pageElementType));
|
std::error(std::format("Unknown page element type: {}", pageElementType));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FootnoteEntry {
|
|
||||||
char number[FOOTNOTE_NUMBER_LEN];
|
|
||||||
char href[FOOTNOTE_HREF_LEN];
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Page {
|
struct Page {
|
||||||
u16 elementCount;
|
u16 elementCount;
|
||||||
PageElement elements[elementCount] [[inline]];
|
PageElement elements[elementCount] [[inline]];
|
||||||
|
|
||||||
u16 footnoteCount;
|
|
||||||
FootnoteEntry footnotes[footnoteCount];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct AnchorEntry {
|
// === Section Bin Structure ===
|
||||||
String anchor;
|
|
||||||
u16 page;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct AnchorMap {
|
|
||||||
u16 count;
|
|
||||||
AnchorEntry entries[count];
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ParagraphLut {
|
|
||||||
u16 count;
|
|
||||||
u16 paragraphIndex[count];
|
|
||||||
};
|
|
||||||
|
|
||||||
struct SectionBin {
|
struct SectionBin {
|
||||||
u8 version;
|
// Header
|
||||||
|
u8 version [[comment("Format version"), color("FFD93D")]];
|
||||||
|
|
||||||
|
// Version validation
|
||||||
if (version != EXPECTED_VERSION) {
|
if (version != EXPECTED_VERSION) {
|
||||||
std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION));
|
std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache busting parameters
|
||||||
s32 fontId;
|
s32 fontId;
|
||||||
float lineCompression;
|
float lineCompression;
|
||||||
bool extraParagraphSpacing;
|
bool extraParagraphSpacing;
|
||||||
u8 paragraphAlignment;
|
|
||||||
u16 viewportWidth;
|
u16 viewportWidth;
|
||||||
u16 viewportHeight;
|
u16 vieportHeight;
|
||||||
bool hyphenationEnabled;
|
|
||||||
bool embeddedStyle;
|
|
||||||
u8 imageRendering;
|
|
||||||
bool focusReadingEnabled;
|
|
||||||
|
|
||||||
u16 pageCount;
|
u16 pageCount;
|
||||||
u32 pageLutOffset;
|
u32 lutOffset;
|
||||||
u32 anchorMapOffset;
|
|
||||||
u32 paragraphLutOffset;
|
|
||||||
u32 listItemLutOffset;
|
|
||||||
|
|
||||||
Page pages[pageCount];
|
Page page[pageCount];
|
||||||
|
|
||||||
|
// Validate LUT offset alignment
|
||||||
u32 currentOffset = $;
|
u32 currentOffset = $;
|
||||||
if (currentOffset != pageLutOffset) {
|
if (currentOffset != lutOffset) {
|
||||||
std::warning(std::format("Page LUT offset mismatch: expected 0x{:X}, got 0x{:X}", pageLutOffset, currentOffset));
|
std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset));
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 pageLut[pageCount] [[comment("Page data offsets")]];
|
// Lookup Tables
|
||||||
|
u32 lut[pageCount];
|
||||||
if (anchorMapOffset != 0) {
|
|
||||||
AnchorMap anchorMap @ anchorMapOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (paragraphLutOffset != 0) {
|
|
||||||
ParagraphLut paragraphLut @ paragraphLutOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (listItemLutOffset != 0 && paragraphLutOffset != 0) {
|
|
||||||
u16 listItemIndex[paragraphLut.count] @ listItemLutOffset;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
SectionBin section @ 0x00;
|
// === File Parsing ===
|
||||||
|
|
||||||
|
SectionBin book @ 0x00;
|
||||||
|
|
||||||
|
// Validate we've consumed the entire file
|
||||||
u32 fileSize = std::mem::size();
|
u32 fileSize = std::mem::size();
|
||||||
u32 parsedSize = $;
|
u32 parsedSize = $;
|
||||||
|
|
||||||
if (parsedSize != fileSize) {
|
if (parsedSize != fileSize) {
|
||||||
std::warning(std::format("Unparsed data detected: {} bytes remaining at offset 0x{:X}", fileSize - parsedSize, parsedSize));
|
std::warning(std::format("Unparsed data detected: {} bytes remaining at offset 0x{:X}", fileSize - parsedSize, parsedSize));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Focus Reading is a reading aid that bolds the first portion of each word, guidin
|
|||||||
1. Open **Settings > Reader**
|
1. Open **Settings > Reader**
|
||||||
2. Toggle **Focus Reading** on
|
2. Toggle **Focus Reading** on
|
||||||
|
|
||||||
Toggling the setting invalidates affected EPUB section caches for the current layout, the same as changing font settings. Sections are rebuilt on demand, then page turns proceed as normal. No changes are made to your EPUB files.
|
Toggling the setting will trigger a re-index of your current book, the same as when changing font settings. Once indexing is complete, page turns proceed as normal. No changes are made to your EPUB files.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
|||||||
+23
-42
@@ -5,29 +5,17 @@ This guide explains the multi-language support system in CrossPoint Reader.
|
|||||||
## Supported Languages
|
## Supported Languages
|
||||||
|
|
||||||
- English
|
- English
|
||||||
- Español
|
- French
|
||||||
- Français
|
- German
|
||||||
- Deutsch
|
- Portuguese
|
||||||
- Čeština
|
- Spanish
|
||||||
- Português (Brasil)
|
- Swedish
|
||||||
- Русский
|
- Czech
|
||||||
- Svenska
|
- Russian
|
||||||
- Română
|
- Ukrainian
|
||||||
- Català
|
- Polish
|
||||||
- Українська
|
- Danish
|
||||||
- Беларуская
|
- Turkish
|
||||||
- Italiano
|
|
||||||
- Polski
|
|
||||||
- Suomi
|
|
||||||
- Dansk
|
|
||||||
- Nederlands
|
|
||||||
- Türkçe
|
|
||||||
- Қазақша
|
|
||||||
- Magyar
|
|
||||||
- Lietuvių
|
|
||||||
- Slovenščina
|
|
||||||
- Valencià
|
|
||||||
- עברית
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -120,9 +108,7 @@ This automatically:
|
|||||||
#### 3. Use in code
|
#### 3. Use in code
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
#include <CrossPointSettings.h>
|
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
#include <Logging.h>
|
|
||||||
|
|
||||||
// Using the tr() macro (recommended)
|
// Using the tr() macro (recommended)
|
||||||
renderer.drawText(font, x, y, tr(STR_MY_NEW_STRING));
|
renderer.drawText(font, x, y, tr(STR_MY_NEW_STRING));
|
||||||
@@ -189,7 +175,7 @@ The YAML files use UTF-8 encoding. Special characters are automatically converte
|
|||||||
// tr(id) - Get translated string without StrId:: prefix
|
// tr(id) - Get translated string without StrId:: prefix
|
||||||
const char* text = tr(STR_SETTINGS_TITLE);
|
const char* text = tr(STR_SETTINGS_TITLE);
|
||||||
renderer.drawText(font, x, y, tr(STR_BROWSE_FILES));
|
renderer.drawText(font, x, y, tr(STR_BROWSE_FILES));
|
||||||
LOG_INF("I18N", "Status: %s", tr(STR_CONNECTED));
|
Serial.printf("Status: %s\n", tr(STR_CONNECTED));
|
||||||
|
|
||||||
// I18N - Shorthand for I18n::getInstance()
|
// I18N - Shorthand for I18n::getInstance()
|
||||||
I18N.setLanguage(Language::ES);
|
I18N.setLanguage(Language::ES);
|
||||||
@@ -205,39 +191,34 @@ const char* text = tr(STR_SETTINGS_TITLE); // Macro (recommended)
|
|||||||
const char* text = I18N.get(StrId::STR_SETTINGS_TITLE); // Direct call
|
const char* text = I18N.get(StrId::STR_SETTINGS_TITLE); // Direct call
|
||||||
const char* text = I18N[StrId::STR_SETTINGS_TITLE]; // Operator overload
|
const char* text = I18N[StrId::STR_SETTINGS_TITLE]; // Operator overload
|
||||||
|
|
||||||
// Set runtime language
|
// Set language
|
||||||
I18N.setLanguage(Language::ES);
|
I18N.setLanguage(Language::ES);
|
||||||
|
|
||||||
// Get current language
|
// Get current language
|
||||||
Language lang = I18N.getLanguage();
|
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)
|
// Get character set for font subsetting (static method)
|
||||||
const char* chars = I18n::getCharacterSet(Language::FR);
|
const char* chars = I18n::getCharacterSet(Language::FR);
|
||||||
|
|
||||||
// Persist a user language choice
|
|
||||||
SETTINGS.language = static_cast<uint8_t>(Language::ES);
|
|
||||||
SETTINGS.saveToFile();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## File Storage
|
## File Storage
|
||||||
|
|
||||||
The selected language is stored with the rest of the device settings in:
|
Language settings are stored in:
|
||||||
```text
|
|
||||||
/.crosspoint/settings.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The JSON field is `language`, stored as a stable language code string such as
|
|
||||||
`"EN"`, `"DE"`, or `"HE"` rather than a raw enum value.
|
|
||||||
|
|
||||||
Older firmware versions used:
|
|
||||||
```text
|
|
||||||
/.crosspoint/language.bin
|
/.crosspoint/language.bin
|
||||||
```
|
```
|
||||||
|
|
||||||
On load, current firmware migrates that legacy file into `settings.json` and
|
This file contains:
|
||||||
renames it to `language.bin.bak`.
|
- Version byte
|
||||||
|
- Current language selection (1 byte)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 233 KiB |
Loaded 20 of 281 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user