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
|
||||
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
|
||||
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
|
||||
|
||||
@@ -83,13 +76,6 @@ jobs:
|
||||
- name: Install PlatformIO Core
|
||||
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
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -116,33 +102,6 @@ jobs:
|
||||
path: .pio/build/default/firmware.bin
|
||||
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
|
||||
# PR requirements.
|
||||
test-status:
|
||||
@@ -151,7 +110,6 @@ jobs:
|
||||
- build
|
||||
- clang-format
|
||||
- cppcheck
|
||||
- unit-tests
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -25,13 +25,6 @@ jobs:
|
||||
- name: Install PlatformIO Core
|
||||
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
|
||||
run: pio run -e gh_release
|
||||
|
||||
|
||||
@@ -25,13 +25,6 @@ jobs:
|
||||
- name: Install PlatformIO Core
|
||||
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
|
||||
run: |
|
||||
echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
|
||||
@@ -19,7 +19,3 @@ build
|
||||
lib/EpdFont/scripts/downloaded_fonts/
|
||||
lib/EpdFont/scripts/instanced_fonts/
|
||||
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"]
|
||||
path = freeink-sdk
|
||||
url = https://github.com/Free-Ink/freeink-sdk.git
|
||||
branch = main
|
||||
[submodule "open-x4-sdk"]
|
||||
path = open-x4-sdk
|
||||
url = https://github.com/crosspoint-reader/community-sdk.git
|
||||
+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).
|
||||
* 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.
|
||||
* 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).
|
||||
* 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).
|
||||
@@ -127,7 +127,7 @@ These flags in `platformio.ini` fundamentally affect firmware behavior:
|
||||
* lib/hal/: Hardware Abstraction Layer (HalDisplay, HalGPIO, HalStorage)
|
||||
* lib/I18n/: Internationalization (translations in `translations/*.yaml`, generated string tables)
|
||||
* 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
|
||||
|
||||
### Hardware Abstraction Layer (HAL)
|
||||
@@ -152,19 +152,20 @@ These flags in `platformio.ini` fundamentally affect firmware behavior:
|
||||
#include <HalStorage.h>
|
||||
|
||||
// Use Storage singleton (defined via macro)
|
||||
HalFile file;
|
||||
FsFile file;
|
||||
if (Storage.openFileForRead("MODULE", "/path/to/file.bin", file)) {
|
||||
// Read from file
|
||||
// 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'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.
|
||||
- **Never** call into `SdFat` / `SdSpiCard` / `FsBaseFile` / `SDCardManager` / raw `FsFile` directly — that bypasses the mutex.
|
||||
- `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` 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:
|
||||
- Noto Serif: 12, 14, 16, 18pt (4 styles each: regular, bold, italic, bold-italic)
|
||||
- Noto Sans: 12, 14, 16, 18pt (4 styles each)
|
||||
- OpenDyslexic: 8, 10, 12, 14pt (4 styles each)
|
||||
- Ubuntu UI fonts: 10, 12pt (2 styles)
|
||||
|
||||
**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`
|
||||
|
||||
**Current Versions** (as of docs/file-formats.md):
|
||||
- `book.bin`: **Version 7** (metadata structure)
|
||||
- `section.bin`: **Version 25** (layout structure)
|
||||
- `book.bin`: **Version 5** (metadata structure)
|
||||
- `section.bin`: **Version 12** (layout structure)
|
||||
|
||||
**Version Increment Rules**:
|
||||
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):
|
||||
```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
|
||||
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?
|
||||
|
||||
- **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`.
|
||||
|
||||
@@ -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)
|
||||
- WebSocket fast uploads
|
||||
- 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
|
||||
- OPDS browser with saved servers (up to 8), search, pagination, and direct download
|
||||
- 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.
|
||||
|
||||
- **Localization**: 24 UI languages and counting. RTL support.
|
||||
- **Localization**: 22 UI languages and counting.
|
||||
|
||||
### Coming soon:
|
||||
|
||||
- RTL support — Arabic, Hebrew, and Farsi.
|
||||
|
||||
- Bookmarks.
|
||||
|
||||
- Dictionary lookup — inline word lookup without leaving the reader.
|
||||
|
||||
- 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.
|
||||
|
||||
## 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
|
||||
@@ -155,7 +142,6 @@ The script scans every theme folder, includes every file in each package, and wr
|
||||
- [User Guide](./USER_GUIDE.md)
|
||||
- [Web server usage](./docs/webserver.md)
|
||||
- [Web server endpoints](./docs/webserver-endpoints.md)
|
||||
- [SD-card theme creation](./docs/theme-creation.md)
|
||||
- [Project scope](./SCOPE.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
|
||||
|
||||
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:
|
||||
|
||||
```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.)
|
||||
│ ├── cover.bmp # generated cover image
|
||||
│ ├── book.bin # metadata: title, author, spine, TOC
|
||||
│ ├── css_rules.cache # parsed CSS rule cache
|
||||
│ ├── img_* # rendered image cache files
|
||||
│ └── sections/ # per-chapter layout cache
|
||||
│ ├── 0.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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
- [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.
|
||||
|
||||
- [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.
|
||||
|
||||
- [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.
|
||||
|
||||
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
|
||||
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).
|
||||
|
||||
The clang-format binary path is resolved once and cached in
|
||||
@@ -92,7 +92,7 @@ function Resolve-ClangFormat {
|
||||
$clangFormat = Resolve-ClangFormat
|
||||
|
||||
$exclude = @(
|
||||
'freeink-sdk'
|
||||
'open-x4-sdk'
|
||||
'lib\EpdFont\builtinFonts'
|
||||
'lib\Epub\Epub\hyphenation\generated'
|
||||
'lib\uzlib'
|
||||
|
||||
@@ -8,18 +8,17 @@ At a high level, it is firmware that uses an activity-driven application archite
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Hardware: ESP32-C3 + SD + E-ink + Buttons] --> B[freeink-sdk]
|
||||
B --> C[lib/hal wrappers]
|
||||
C --> D[src/main.cpp runtime loop]
|
||||
D --> E[Activities layer]
|
||||
D --> F[State and settings]
|
||||
E --> G[Reader flows]
|
||||
E --> H[Home/Library/Settings flows]
|
||||
E --> I[Network/Web server flows]
|
||||
G --> J[lib/Epub parsing + layout + hyphenation]
|
||||
J --> K[SD cache in .crosspoint]
|
||||
E --> L[GfxRenderer]
|
||||
L --> M[E-ink display buffer]
|
||||
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
|
||||
@@ -59,7 +58,7 @@ 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/`: 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
|
||||
|
||||
## Reader and content pipeline
|
||||
@@ -74,11 +73,10 @@ flowchart LR
|
||||
C -->|EPUB| D[lib/Epub/Epub]
|
||||
C -->|XTC| E[lib/Xtc reader]
|
||||
C -->|TXT| F[lib/Txt reader]
|
||||
D --> G[Parse OPF/TOC and collect CSS refs]
|
||||
G --> H[Build/load book.bin and css_rules.cache]
|
||||
H --> I[Layout pages/sections]
|
||||
I --> J[Write section cache]
|
||||
J --> K[Render current page via GfxRenderer]
|
||||
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:
|
||||
@@ -100,7 +98,7 @@ flowchart TD
|
||||
D --> E[Locate container and OPF]
|
||||
E --> F[Build or load BookMetadataCache]
|
||||
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]
|
||||
I --> J{Section cache exists for current settings?}
|
||||
@@ -123,12 +121,7 @@ flowchart TD
|
||||
|
||||
Notes:
|
||||
|
||||
- CSS files are collected from the OPF manifest and, when needed, discovered by
|
||||
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
|
||||
- "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
|
||||
|
||||
@@ -145,18 +138,14 @@ Typical persisted areas on SD:
|
||||
/.crosspoint/
|
||||
epub_<hash>/
|
||||
book.bin
|
||||
css_rules.cache
|
||||
progress.bin
|
||||
cover.bmp
|
||||
sections/*.bin
|
||||
img_* cache files
|
||||
settings.json
|
||||
state.json
|
||||
settings.bin
|
||||
state.bin
|
||||
```
|
||||
|
||||
`sections/*.bin` contains rendered pages plus anchor, paragraph, and list-item
|
||||
lookup tables used for TOC/footnote jumps and KOReader sync refinement. For
|
||||
binary cache formats, see `docs/file-formats.md`.
|
||||
For binary cache formats, see `docs/file-formats.md`.
|
||||
|
||||
## Networking architecture
|
||||
|
||||
@@ -164,18 +153,14 @@ Network file transfer is controlled by `src/activities/network/CrossPointWebServ
|
||||
|
||||
Modes:
|
||||
|
||||
- STA: join existing Wi-Fi network
|
||||
- STA: join existing WiFi network
|
||||
- AP: create hotspot
|
||||
- Calibre Wireless: STA flow specialized for Calibre plugin uploads
|
||||
|
||||
Server behavior:
|
||||
|
||||
- HTTP server on port 80
|
||||
- WebSocket upload server on port 81
|
||||
- WebDAV handler on the HTTP server
|
||||
- UDP discovery listener for upload clients
|
||||
- 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
|
||||
|
||||
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.
|
||||
|
||||
- `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/`
|
||||
|
||||
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/network/`: web server and OTA/update networking
|
||||
- `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/`: 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
|
||||
|
||||
## Embedded constraints that shape design
|
||||
|
||||
+87
-153
@@ -1,26 +1,22 @@
|
||||
# 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`
|
||||
|
||||
### Version 7
|
||||
### Version 3
|
||||
|
||||
`book.bin` stores EPUB metadata plus lookup tables for spine and TOC entries.
|
||||
The current firmware writes this version from `BookMetadataCache`.
|
||||
|
||||
ImHex pattern:
|
||||
ImHex Pattern:
|
||||
|
||||
```c++
|
||||
import std.mem;
|
||||
import std.string;
|
||||
import std.core;
|
||||
|
||||
#define EXPECTED_VERSION 7
|
||||
// === Configuration ===
|
||||
#define EXPECTED_VERSION 3
|
||||
#define MAX_STRING_LENGTH 65535
|
||||
|
||||
// === String Structure ===
|
||||
|
||||
struct String {
|
||||
u32 length [[hidden, comment("String byte length")]];
|
||||
if (length > MAX_STRING_LENGTH) {
|
||||
@@ -33,56 +29,74 @@ fn format_string(String s) {
|
||||
return s.data;
|
||||
};
|
||||
|
||||
// === Metadata Structure ===
|
||||
|
||||
struct Metadata {
|
||||
String title [[comment("Book title")]];
|
||||
String author [[comment("Book author")]];
|
||||
String language [[comment("Book language code")]];
|
||||
String coverItemHref [[comment("Path to cover image")]];
|
||||
String textReferenceHref [[comment("Path to guided first text reference")]];
|
||||
};
|
||||
} [[comment("Book metadata information")]];
|
||||
|
||||
// === Spine Entry Structure ===
|
||||
|
||||
struct SpineEntry {
|
||||
String href [[comment("Resource path")]];
|
||||
u32 cumulativeSize [[comment("Cumulative uncompressed spine size through this entry")]];
|
||||
s16 tocIndex [[comment("Index into TOC, or inherited/previous TOC index when no direct entry exists")]];
|
||||
};
|
||||
u32 cumulativeSize [[comment("Cumulative size in bytes"), color("FF6B6B")]];
|
||||
s16 tocIndex [[comment("Index into TOC (-1 if none)"), color("4ECDC4")]];
|
||||
} [[comment("Spine entry defining reading order")]];
|
||||
|
||||
// === TOC Entry Structure ===
|
||||
|
||||
struct TocEntry {
|
||||
String title [[comment("Chapter/section title")]];
|
||||
String href [[comment("Resource path")]];
|
||||
String anchor [[comment("Fragment identifier")]];
|
||||
u8 level [[comment("Nesting level")]];
|
||||
s16 spineIndex [[comment("Index into spine (-1 if none)")]];
|
||||
};
|
||||
u8 level [[comment("Nesting level (0-255)"), color("95E1D3")]];
|
||||
s16 spineIndex [[comment("Index into spine (-1 if none)"), color("F38181")]];
|
||||
} [[comment("Table of contents entry")]];
|
||||
|
||||
// === Book Bin Structure ===
|
||||
|
||||
struct BookBin {
|
||||
u8 version;
|
||||
// Header
|
||||
u8 version [[comment("Format version"), color("FFD93D")]];
|
||||
|
||||
// Version validation
|
||||
if (version != EXPECTED_VERSION) {
|
||||
std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION));
|
||||
}
|
||||
|
||||
u32 lutOffset [[comment("Offset to lookup tables")]];
|
||||
u16 spineCount;
|
||||
u16 tocCount;
|
||||
u32 lutOffset [[comment("Offset to lookup tables"), color("6BCB77")]];
|
||||
u16 spineCount [[comment("Number of spine entries"), color("4D96FF")]];
|
||||
u16 tocCount [[comment("Number of TOC entries"), color("FF6B9D")]];
|
||||
|
||||
Metadata metadata;
|
||||
// Metadata section
|
||||
Metadata metadata [[comment("Book metadata")]];
|
||||
|
||||
// Validate LUT offset alignment
|
||||
u32 currentOffset = $;
|
||||
if (currentOffset != lutOffset) {
|
||||
std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset));
|
||||
}
|
||||
|
||||
u32 spineLut[spineCount] [[comment("Spine entry offsets")]];
|
||||
u32 tocLut[tocCount] [[comment("TOC entry offsets")]];
|
||||
// Lookup Tables
|
||||
u32 spineLut[spineCount] [[comment("Spine entry offsets"), color("4D96FF")]];
|
||||
u32 tocLut[tocCount] [[comment("TOC entry offsets"), color("FF6B9D")]];
|
||||
|
||||
SpineEntry spines[spineCount];
|
||||
TocEntry toc[tocCount];
|
||||
// Data Entries
|
||||
SpineEntry spines[spineCount] [[comment("Spine entries (reading order)")]];
|
||||
TocEntry toc[tocCount] [[comment("Table of contents entries")]];
|
||||
};
|
||||
|
||||
// === File Parsing ===
|
||||
|
||||
BookBin book @ 0x00;
|
||||
|
||||
// Validate we've consumed the entire file
|
||||
u32 fileSize = std::mem::size();
|
||||
u32 parsedSize = $;
|
||||
|
||||
if (parsedSize != fileSize) {
|
||||
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`
|
||||
|
||||
### Version 25
|
||||
### Version 24
|
||||
|
||||
Each file in `sections/*.bin` stores one laid-out spine section. The header is
|
||||
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:
|
||||
ImHex Pattern:
|
||||
|
||||
```c++
|
||||
import std.mem;
|
||||
import std.string;
|
||||
import std.core;
|
||||
|
||||
#define EXPECTED_VERSION 25
|
||||
// === Configuration ===
|
||||
#define EXPECTED_VERSION 24
|
||||
#define MAX_STRING_LENGTH 65535
|
||||
#define FOOTNOTE_NUMBER_LEN 32
|
||||
#define FOOTNOTE_HREF_LEN 96
|
||||
|
||||
// === String Structure ===
|
||||
|
||||
struct String {
|
||||
u32 length [[hidden, comment("String byte length")]];
|
||||
@@ -130,79 +131,44 @@ fn format_string(String s) {
|
||||
return s.data;
|
||||
};
|
||||
|
||||
// === Page Structure ===
|
||||
|
||||
enum PageElementTag : u8 {
|
||||
TAG_PageLine = 1,
|
||||
TAG_PageImage = 2,
|
||||
TAG_PageHorizontalRule = 3
|
||||
PageLine = 1,
|
||||
PageImage = 2,
|
||||
PageHorizontalRule = 3
|
||||
};
|
||||
|
||||
enum WordStyle : u8 {
|
||||
REGULAR = 0,
|
||||
BOLD = 1,
|
||||
ITALIC = 2,
|
||||
BOLD_ITALIC = 3,
|
||||
UNDERLINE = 4,
|
||||
STRIKETHROUGH = 8,
|
||||
SUP = 16,
|
||||
SUB = 32
|
||||
BOLD_ITALIC = 3
|
||||
};
|
||||
|
||||
enum TextAlign : u8 {
|
||||
enum BlockStyle : u8 {
|
||||
JUSTIFIED = 0,
|
||||
LEFT_ALIGN = 1,
|
||||
CENTER_ALIGN = 2,
|
||||
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 {
|
||||
s16 xPos;
|
||||
s16 yPos;
|
||||
TextBlock block;
|
||||
s16 xPos;
|
||||
s16 yPos;
|
||||
u16 wordCount;
|
||||
String words[wordCount];
|
||||
u16 wordXPos[wordCount];
|
||||
WordStyle wordStyle[wordCount];
|
||||
BlockStyle blockStyle;
|
||||
};
|
||||
|
||||
struct PageImage {
|
||||
s16 xPos;
|
||||
s16 yPos;
|
||||
ImageBlock image;
|
||||
String imagePath;
|
||||
s16 width;
|
||||
s16 height;
|
||||
};
|
||||
|
||||
struct PageHorizontalRule {
|
||||
@@ -213,95 +179,63 @@ struct PageHorizontalRule {
|
||||
};
|
||||
|
||||
struct PageElement {
|
||||
PageElementTag pageElementType;
|
||||
if (pageElementType == TAG_PageLine) {
|
||||
u8 pageElementType;
|
||||
if (pageElementType == 1) {
|
||||
PageLine pageLine [[inline]];
|
||||
} else if (pageElementType == TAG_PageImage) {
|
||||
} else if (pageElementType == 2) {
|
||||
PageImage pageImage [[inline]];
|
||||
} else if (pageElementType == TAG_PageHorizontalRule) {
|
||||
} else if (pageElementType == 3) {
|
||||
PageHorizontalRule horizontalRule [[inline]];
|
||||
} else {
|
||||
std::error(std::format("Unknown page element type: {}", pageElementType));
|
||||
}
|
||||
};
|
||||
|
||||
struct FootnoteEntry {
|
||||
char number[FOOTNOTE_NUMBER_LEN];
|
||||
char href[FOOTNOTE_HREF_LEN];
|
||||
};
|
||||
|
||||
struct Page {
|
||||
u16 elementCount;
|
||||
PageElement elements[elementCount] [[inline]];
|
||||
|
||||
u16 footnoteCount;
|
||||
FootnoteEntry footnotes[footnoteCount];
|
||||
};
|
||||
|
||||
struct AnchorEntry {
|
||||
String anchor;
|
||||
u16 page;
|
||||
};
|
||||
|
||||
struct AnchorMap {
|
||||
u16 count;
|
||||
AnchorEntry entries[count];
|
||||
};
|
||||
|
||||
struct ParagraphLut {
|
||||
u16 count;
|
||||
u16 paragraphIndex[count];
|
||||
};
|
||||
// === Section Bin Structure ===
|
||||
|
||||
struct SectionBin {
|
||||
u8 version;
|
||||
// Header
|
||||
u8 version [[comment("Format version"), color("FFD93D")]];
|
||||
|
||||
// Version validation
|
||||
if (version != EXPECTED_VERSION) {
|
||||
std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION));
|
||||
}
|
||||
|
||||
// Cache busting parameters
|
||||
s32 fontId;
|
||||
float lineCompression;
|
||||
bool extraParagraphSpacing;
|
||||
u8 paragraphAlignment;
|
||||
u16 viewportWidth;
|
||||
u16 viewportHeight;
|
||||
bool hyphenationEnabled;
|
||||
bool embeddedStyle;
|
||||
u8 imageRendering;
|
||||
bool focusReadingEnabled;
|
||||
|
||||
u16 vieportHeight;
|
||||
u16 pageCount;
|
||||
u32 pageLutOffset;
|
||||
u32 anchorMapOffset;
|
||||
u32 paragraphLutOffset;
|
||||
u32 listItemLutOffset;
|
||||
u32 lutOffset;
|
||||
|
||||
Page pages[pageCount];
|
||||
Page page[pageCount];
|
||||
|
||||
// Validate LUT offset alignment
|
||||
u32 currentOffset = $;
|
||||
if (currentOffset != pageLutOffset) {
|
||||
std::warning(std::format("Page LUT offset mismatch: expected 0x{:X}, got 0x{:X}", pageLutOffset, currentOffset));
|
||||
if (currentOffset != lutOffset) {
|
||||
std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset));
|
||||
}
|
||||
|
||||
u32 pageLut[pageCount] [[comment("Page data offsets")]];
|
||||
|
||||
if (anchorMapOffset != 0) {
|
||||
AnchorMap anchorMap @ anchorMapOffset;
|
||||
}
|
||||
|
||||
if (paragraphLutOffset != 0) {
|
||||
ParagraphLut paragraphLut @ paragraphLutOffset;
|
||||
}
|
||||
|
||||
if (listItemLutOffset != 0 && paragraphLutOffset != 0) {
|
||||
u16 listItemIndex[paragraphLut.count] @ listItemLutOffset;
|
||||
}
|
||||
// Lookup Tables
|
||||
u32 lut[pageCount];
|
||||
};
|
||||
|
||||
SectionBin section @ 0x00;
|
||||
// === File Parsing ===
|
||||
|
||||
SectionBin book @ 0x00;
|
||||
|
||||
// Validate we've consumed the entire file
|
||||
u32 fileSize = std::mem::size();
|
||||
u32 parsedSize = $;
|
||||
|
||||
if (parsedSize != fileSize) {
|
||||
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**
|
||||
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
|
||||
|
||||
|
||||
+23
-42
@@ -5,29 +5,17 @@ This guide explains the multi-language support system in CrossPoint Reader.
|
||||
## Supported Languages
|
||||
|
||||
- English
|
||||
- Español
|
||||
- Français
|
||||
- Deutsch
|
||||
- Čeština
|
||||
- Português (Brasil)
|
||||
- Русский
|
||||
- Svenska
|
||||
- Română
|
||||
- Català
|
||||
- Українська
|
||||
- Беларуская
|
||||
- Italiano
|
||||
- Polski
|
||||
- Suomi
|
||||
- Dansk
|
||||
- Nederlands
|
||||
- Türkçe
|
||||
- Қазақша
|
||||
- Magyar
|
||||
- Lietuvių
|
||||
- Slovenščina
|
||||
- Valencià
|
||||
- עברית
|
||||
- French
|
||||
- German
|
||||
- Portuguese
|
||||
- Spanish
|
||||
- Swedish
|
||||
- Czech
|
||||
- Russian
|
||||
- Ukrainian
|
||||
- Polish
|
||||
- Danish
|
||||
- Turkish
|
||||
|
||||
---
|
||||
|
||||
@@ -120,9 +108,7 @@ This automatically:
|
||||
#### 3. Use in code
|
||||
|
||||
```cpp
|
||||
#include <CrossPointSettings.h>
|
||||
#include <I18n.h>
|
||||
#include <Logging.h>
|
||||
|
||||
// Using the tr() macro (recommended)
|
||||
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
|
||||
const char* text = tr(STR_SETTINGS_TITLE);
|
||||
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.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[StrId::STR_SETTINGS_TITLE]; // Operator overload
|
||||
|
||||
// Set runtime language
|
||||
// Set language
|
||||
I18N.setLanguage(Language::ES);
|
||||
|
||||
// Get current language
|
||||
Language lang = I18N.getLanguage();
|
||||
|
||||
// Save language setting to file
|
||||
I18N.saveSettings();
|
||||
|
||||
// Load language setting from file
|
||||
I18N.loadSettings();
|
||||
|
||||
// Get character set for font subsetting (static method)
|
||||
const char* chars = I18n::getCharacterSet(Language::FR);
|
||||
|
||||
// Persist a user language choice
|
||||
SETTINGS.language = static_cast<uint8_t>(Language::ES);
|
||||
SETTINGS.saveToFile();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Storage
|
||||
|
||||
The selected language is stored with the rest of the device settings in:
|
||||
```text
|
||||
/.crosspoint/settings.json
|
||||
Language settings are stored in:
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
On load, current firmware migrates that legacy file into `settings.json` and
|
||||
renames it to `language.bin.bak`.
|
||||
This file contains:
|
||||
- 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