Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
973a902332 | ||
|
|
cb9b971c26 | ||
|
|
f42fab1c66 | ||
|
|
c9188a7347 | ||
|
|
e03aa16330 | ||
|
|
3319aa1720 | ||
|
|
3dfbc0383f | ||
|
|
64364bec5d | ||
|
|
556b8aee5b | ||
|
|
b1d1d757ba | ||
|
|
5ba1d5747f | ||
|
|
9fe4dc5e38 | ||
|
|
b1d037569b | ||
|
|
9737cb335c | ||
|
|
fdffc2e5d9 | ||
|
|
a2db43d235 | ||
|
|
63093e606c | ||
|
|
a4ac3b2788 | ||
|
|
35a45f9c1e | ||
|
|
f3cad2f9e1 | ||
|
|
95d8cb712b | ||
|
|
b9867b0d09 | ||
|
|
3dac4446b7 | ||
|
|
f180069643 | ||
|
|
a0ec5812b1 | ||
|
|
e84840b473 | ||
|
|
94ae750e89 | ||
|
|
8c5cd67f38 | ||
|
|
c5787d1cc1 | ||
|
|
2a19eb9e0d | ||
|
|
fae3423dec | ||
|
|
932a472835 | ||
|
|
552b2683e6 | ||
|
|
bfa1d706b1 | ||
|
|
73ab9ab20d | ||
|
|
3a97f3e63e | ||
|
|
39ea4b045f | ||
|
|
444d87de82 | ||
|
|
859f6cb0d5 | ||
|
|
d8139e9605 | ||
|
|
b596f21e61 | ||
|
|
5fbc657aeb | ||
|
|
bb06219c9c | ||
|
|
5202bbf911 | ||
|
|
e0253a8664 | ||
|
|
e142e88ce7 | ||
|
|
6add0b91bc | ||
|
|
287457f7dd | ||
|
|
3e627112f6 | ||
|
|
1f5669a08a | ||
|
|
0dc92c142c | ||
|
|
4b34a576eb | ||
|
|
4fb843ef92 | ||
|
|
a43061305a | ||
|
|
7ec07b4d9d | ||
|
|
c2c1badc7e | ||
|
|
5776911af0 | ||
|
|
3c5c5e8aa7 | ||
|
|
6f5c5a0900 | ||
|
|
f2b00f6b93 | ||
|
|
44ff313740 | ||
|
|
ca1b833126 | ||
|
|
3e1dd31e53 | ||
|
|
685d4e88f9 | ||
|
|
79b4d63ee2 | ||
|
|
57447a56a6 | ||
|
|
5c86bfe1fd | ||
|
|
4cdefb137c | ||
|
|
e7f3406704 | ||
|
|
6f08a88b12 | ||
|
|
e4d3981469 | ||
|
|
c9d27826f9 | ||
|
|
13bd00bf9d | ||
|
|
fef39da23c | ||
|
|
fd43ca2fe1 | ||
|
|
28255061fb | ||
|
|
43ff1b6644 | ||
|
|
2754a5ff01 | ||
|
|
6edeed73c5 | ||
|
|
8ac302018c | ||
|
|
a11517a62d | ||
|
|
cbaa498ccc | ||
|
|
ebebc6f202 | ||
|
|
970b2c6ca1 | ||
|
|
b6ce599b20 | ||
|
|
f54eab2725 | ||
|
|
0d4c9ab91b | ||
|
|
0daa9db243 | ||
|
|
a2f2eea79e | ||
|
|
86a9b9c4a2 | ||
|
|
ba44978ac4 | ||
|
|
555f76da88 | ||
|
|
0a57c0a5a7 | ||
|
|
a09aef0889 | ||
|
|
8626d69f46 | ||
|
|
fc89e57e69 | ||
|
|
487613b082 | ||
|
|
8d5b119644 | ||
|
|
7271c00d35 | ||
|
|
6e8dbd7f23 | ||
|
|
c4b1d9644a | ||
|
|
f6e59aab72 | ||
|
|
9ad2da0950 | ||
|
|
1db1442319 | ||
|
|
362dcb2a65 | ||
|
|
d1abcc00a2 | ||
|
|
0ce874f8d4 | ||
|
|
282514f755 | ||
|
|
d1e4650e19 | ||
|
|
370f87ea01 | ||
|
|
8b9a73a735 | ||
|
|
29c69b3d8f | ||
|
|
4b5a84dd81 | ||
|
|
b9874c8114 | ||
|
|
b1131795d8 | ||
|
|
5e990b3991 | ||
|
|
ff1951c715 | ||
|
|
121c0690b0 | ||
|
|
16b0853654 | ||
|
|
7d639cf880 | ||
|
|
22f3575064 | ||
|
|
d4069aeae5 | ||
|
|
54d5a788a5 | ||
|
|
f66220a245 | ||
|
|
2266060e84 | ||
|
|
90039d7f4d | ||
|
|
16eb66d7cf | ||
|
|
02bab00be6 | ||
|
|
55a56914d7 | ||
|
|
8634cb8ad3 | ||
|
|
21caebd6e4 | ||
|
|
6f05e401a1 | ||
|
|
f2e3d117dc | ||
|
|
9202522a39 | ||
|
|
301f1d1a38 | ||
|
|
1f83087aa5 | ||
|
|
68138fffdb | ||
|
|
eb98735115 | ||
|
|
b94b58756f | ||
|
|
ca6ebc56fb | ||
|
|
2ea042a68a | ||
|
|
9c12b90737 | ||
|
|
7f01eab62e | ||
|
|
3a1e9f3023 | ||
|
|
fad1a801a4 | ||
|
|
67936cb3af | ||
|
|
fd5b8078c6 | ||
|
|
bd101b2af8 | ||
|
|
19d51ec08c | ||
|
|
ea231ffd18 | ||
|
|
a2d4c6a139 | ||
|
|
b5b1f650e2 | ||
|
|
f04b8aa9a9 | ||
|
|
d9bcef7a58 | ||
|
|
a60f31cdd4 | ||
|
|
f055fdd774 | ||
|
|
bb078bae09 | ||
|
|
2d65808302 | ||
|
|
6e75e5a6be | ||
|
|
db94a86fba | ||
|
|
b12839d1d4 | ||
|
|
50e4d550fb | ||
|
|
03f73fadc7 | ||
|
|
0d64980d25 | ||
|
|
4fe80ba7a5 | ||
|
|
b229428a17 | ||
|
|
36d0e52e4b | ||
|
|
94c0ba466c | ||
|
|
da2e4eabb0 | ||
|
|
3392b3e35a | ||
|
|
52e7446757 | ||
|
|
d83605e003 | ||
|
|
ebf0413fe1 | ||
|
|
66d9e4403a | ||
|
|
174a24ba42 | ||
|
|
9897025ded | ||
|
|
f5bc554ae7 | ||
|
|
cc2079a578 | ||
|
|
75a35f0578 | ||
|
|
f872f1f549 | ||
|
|
02573dc592 | ||
|
|
dfefbfe9b5 | ||
|
|
36a3a0cc3a | ||
|
|
bbb3e06eb1 | ||
|
|
213972badc | ||
|
|
c5e861d71c | ||
|
|
34e923d722 | ||
|
|
f4e7eaa198 | ||
|
|
94d4b0c7bb | ||
|
|
6dbfc29c7e | ||
|
|
4ee406897b | ||
|
|
67973166e3 | ||
|
|
50bc9325a4 | ||
|
|
d736e52bdd | ||
|
|
91d17d9f48 | ||
|
|
d5c28fa1f1 | ||
|
|
f69650fb86 | ||
|
|
e3298c6e43 | ||
|
|
56ba9e4337 | ||
|
|
b56d3d853d | ||
|
|
e9120888fa | ||
|
|
d7797baff2 | ||
|
|
b53ac3d52c | ||
|
|
38210be820 | ||
|
|
88b10c82a3 | ||
|
|
19954aa1b5 | ||
|
|
2823a4a2cd | ||
|
|
929f290042 | ||
|
|
0f021ea5f1 | ||
|
|
99ab8b2772 | ||
|
|
7accc607af | ||
|
|
f39ba7037f | ||
|
|
2dd491b62e | ||
|
|
dc404b41c7 | ||
|
|
4ffc2a7e7e | ||
|
|
c44555007b | ||
|
|
d9aa5b4de1 | ||
|
|
a7aa4c55d8 | ||
|
|
3800179595 | ||
|
|
0b9d1a7d23 | ||
|
|
252a64fa97 | ||
|
|
75764bc6eb | ||
|
|
0ddefdc0c9 | ||
|
|
41e6e15229 | ||
|
|
082b5f295b | ||
|
|
beb7876ccf | ||
|
|
b69111bea5 | ||
|
|
86a9510957 | ||
|
|
acc1ed4358 | ||
|
|
08461c08e7 | ||
|
|
dac7fef49d | ||
|
|
a14c8e762d | ||
|
|
a525606d7f | ||
|
|
df53faab91 | ||
|
|
f2adefe729 | ||
|
|
6a98c2d865 | ||
|
|
cfd3a381ed | ||
|
|
151bf1dae4 | ||
|
|
06d28d6ffa | ||
|
|
8a11f44571 | ||
|
|
061c7688a4 | ||
|
|
510ee10153 | ||
|
|
c6d116024c | ||
|
|
85e08f9a93 | ||
|
|
0af0ad5a17 | ||
|
|
93e81daf41 | ||
|
|
a3e51f9b1e | ||
|
|
90d4c885e1 | ||
|
|
ee947b06d1 | ||
|
|
28b907321e | ||
|
|
77afea4d95 | ||
|
|
2f342508bc | ||
|
|
7bb1f7ed76 | ||
|
|
7acc31bc34 | ||
|
|
938ca3fac1 | ||
|
|
0fbe6ff469 | ||
|
|
43b20bd8be | ||
|
|
b186529120 | ||
|
|
5fa5a71ba2 | ||
|
|
aa43bd3569 | ||
|
|
8377ac9e31 | ||
|
|
71518b3336 | ||
|
|
30b14f2ecf | ||
|
|
6f7f4c592a | ||
|
|
bc57e5d64b | ||
|
|
db3bb850b2 | ||
|
|
30a209de8b | ||
|
|
3c34a8310e | ||
|
|
bc6e090aa8 | ||
|
|
8d1b86a893 | ||
|
|
63d5094f2e | ||
|
|
24977048c3 | ||
|
|
63e92ec74e | ||
|
|
accd50b593 | ||
|
|
26250c0b80 | ||
|
|
99ac1c5c89 | ||
|
|
90874dae9f | ||
|
|
a0037ced2b | ||
|
|
20fee843c7 | ||
|
|
181ed6c488 | ||
|
|
3ac1ab13a0 | ||
|
|
dd06e71b66 | ||
|
|
4e5e2fe40a | ||
|
|
2ff63884d6 | ||
|
|
74b8cac928 | ||
|
|
aba393f900 | ||
|
|
45441e0789 | ||
|
|
cff54d776c | ||
|
|
5917c561b3 | ||
|
|
5d5533b3d2 | ||
|
|
c5d2dc2e00 | ||
|
|
91de6ac278 | ||
|
|
c7ad14eb36 | ||
|
|
d3e0aeb62b | ||
|
|
3efc863038 | ||
|
|
bf30964982 | ||
|
|
ceb3fed392 | ||
|
|
f03d3a8056 | ||
|
|
bf894fd343 | ||
|
|
628794f8b8 | ||
|
|
e64155ed63 | ||
|
|
7993b2bb97 | ||
|
|
29fd29f537 | ||
|
|
b463966045 | ||
|
|
a48ad3cd61 | ||
|
|
e3fb3bba37 | ||
|
|
e8d7153d7f | ||
|
|
1e2f6e2d67 | ||
|
|
e841c84194 | ||
|
|
83f0cee565 | ||
|
|
c15b5b9bc5 | ||
|
|
dadce519c4 | ||
|
|
11bc36ebb5 | ||
|
|
395e68ea2b | ||
|
|
cfe3a948a0 | ||
|
|
4ea938b709 | ||
|
|
939014d996 | ||
|
|
3e7d63dab9 | ||
|
|
efa4f71a68 | ||
|
|
f44722a0f2 | ||
|
|
40af42683e | ||
|
|
78625afe76 | ||
|
|
adcd7961c9 | ||
|
|
5717374e4b | ||
|
|
b8a6b58b5e | ||
|
|
333286acfc | ||
|
|
e026bcb9dc | ||
|
|
a1007a4660 | ||
|
|
6c4ae7c41a | ||
|
|
a5fac320ab | ||
|
|
2e2ea6a9e8 | ||
|
|
b692bad10d | ||
|
|
aa7a31b3db | ||
|
|
22701ccf18 | ||
|
|
64ecfe2ef3 | ||
|
|
ba4a361d64 | ||
|
|
15ea7027df | ||
|
|
b25389b43e | ||
|
|
ae865f6d08 | ||
|
|
d53c8b0e0e | ||
|
|
2f969a93b4 | ||
|
|
14e1ce2d04 | ||
|
|
5d2e5596b4 | ||
|
|
bc9651b664 | ||
|
|
741dd89ac1 | ||
|
|
ef98d44ef3 | ||
|
|
b8a51522ca | ||
|
|
33386953d9 | ||
|
|
02822e01b3 | ||
|
|
198b9d7786 | ||
|
|
15e0d39d2c | ||
|
|
6c4e946e27 | ||
|
|
907e14da28 | ||
|
|
b9b795bf45 | ||
|
|
b21b10f4b9 | ||
|
|
1cf2239742 | ||
|
|
c5f82709c0 | ||
|
|
867fb7cbe2 | ||
|
|
5e26baef63 | ||
|
|
8154f88dbe | ||
|
|
56d3ab929c | ||
|
|
9dab5f471b | ||
|
|
ce22deab7c | ||
|
|
e28918b24d | ||
|
|
c0ee096841 | ||
|
|
1a145fe085 | ||
|
|
302dea1eea | ||
|
|
e8645ed92e | ||
|
|
64f5ef018a | ||
|
|
3cdfc6c781 | ||
|
|
77b2c31635 | ||
|
|
fedcb2f53d | ||
|
|
a888978f95 | ||
|
|
23f60a3407 | ||
|
|
2c5a47f9d7 | ||
|
|
ce1756e36f | ||
|
|
3b12c083bc | ||
|
|
c4f5c8e931 | ||
|
|
0c5dee3c62 | ||
|
|
81ae9dd779 | ||
|
|
40e4c96906 | ||
|
|
80772ff6b8 | ||
|
|
57fc6555f2 | ||
|
|
ed54f97909 | ||
|
|
45cd00889e | ||
|
|
cced77783f | ||
|
|
1bd7a1de67 | ||
|
|
23aad213fc | ||
|
|
075ad7d021 | ||
|
|
243ae8b408 | ||
|
|
4e9c7a787f | ||
|
|
cc23aaa9c2 | ||
|
|
05f8e6e12d | ||
|
|
405ce0c3c8 | ||
|
|
9bc5111c77 | ||
|
|
9c11f3e4a2 | ||
|
|
fa2a3d2539 | ||
|
|
5c12f2f01e | ||
|
|
8d6b35b8e7 | ||
|
|
83cd96bc2f | ||
|
|
14ec53a335 | ||
|
|
5ba85290ab | ||
|
|
104f391a29 | ||
|
|
b3b43bb373 | ||
|
|
5349e81723 | ||
|
|
825ef56ad8 | ||
|
|
ed0811c898 | ||
|
|
d29b8ee2f9 | ||
|
|
b898d53f7b | ||
|
|
c656673b9a | ||
|
|
1398aeb1ed | ||
|
|
6cd19f5619 | ||
|
|
cff3e12a0a | ||
|
|
fa3c7d96a0 | ||
|
|
f429f9035c | ||
|
|
11984f8fef | ||
|
|
1c13331189 | ||
|
|
9b3885135f |
@@ -0,0 +1,33 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
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."
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Run formatter from repository root regardless of current directory.
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
cd "${REPO_ROOT}"
|
||||
|
||||
# Capture the files already staged for commit so we only re-stage those
|
||||
# paths after formatting.
|
||||
staged_files=()
|
||||
while IFS= read -r -d '' file; do
|
||||
staged_files+=("${file}")
|
||||
done < <(git diff --cached --name-only -z --diff-filter=ACMR)
|
||||
|
||||
# Intentionally format all currently modified tracked C/C++ files.
|
||||
# The helper handles no-op cases and exits 0 when nothing matches.
|
||||
echo "Running clang-format fix before commit..."
|
||||
./bin/clang-format-fix
|
||||
|
||||
# Ensure formatting changes are included in the pending commit without
|
||||
# staging unrelated tracked modifications from other files in the
|
||||
# working tree.
|
||||
if ((${#staged_files[@]})); then
|
||||
git add -- "${staged_files[@]}"
|
||||
fi
|
||||
+1
-2
@@ -1,2 +1 @@
|
||||
github: [daveallie]
|
||||
ko_fi: daveallie
|
||||
custom: ["https://app.royalty.dev/crosspoint-reader/crosspoint-reader"]
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Scope or Roadmap Question (start here if unsure)
|
||||
url: https://github.com/crosspoint-reader/crosspoint-reader/discussions
|
||||
about: |
|
||||
Not sure if your idea fits CrossPoint's scope? Start a Discussion before filing an issue.
|
||||
See SCOPE.md and ROADMAP.md for what is in, out, and currently paused.
|
||||
- name: Read the Scope document
|
||||
url: https://github.com/crosspoint-reader/crosspoint-reader/blob/master/SCOPE.md
|
||||
about: The authoritative list of what CrossPoint will and will not accept.
|
||||
- name: Read the Roadmap
|
||||
url: https://github.com/crosspoint-reader/crosspoint-reader/blob/master/ROADMAP.md
|
||||
about: Current phase, what is being closed out, and what comes next.
|
||||
@@ -0,0 +1,80 @@
|
||||
name: Feature Request
|
||||
description: Propose a new feature, enhancement, or change to CrossPoint
|
||||
title: "Short, descriptive title of the request"
|
||||
labels: ["enhancement", "needs-scope-review"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to propose a change to CrossPoint!
|
||||
|
||||
**Before you continue, please read [SCOPE.md](../blob/master/SCOPE.md) and [ROADMAP.md](../blob/master/ROADMAP.md).**
|
||||
|
||||
CrossPoint is intentionally narrow. Most rejected proposals are rejected for scope reasons that are already
|
||||
documented. The checklist below exists to save both of us time.
|
||||
|
||||
If you are not sure whether your idea fits, open a [Discussion](../../discussions) first instead of filing
|
||||
this issue.
|
||||
|
||||
- type: checkboxes
|
||||
id: scope-check
|
||||
attributes:
|
||||
label: Scope Self-Check (required)
|
||||
description: Please confirm each of the following. If any are unchecked, your issue will likely be closed.
|
||||
options:
|
||||
- label: I have read SCOPE.md and ROADMAP.md.
|
||||
required: true
|
||||
- label: This is **not** a new theme or theming change (themes are temporarily closed pending the move to SD-loaded themes).
|
||||
required: true
|
||||
- label: This is **not** a new external network connector (sync engine, cloud storage, remote file access, OPDS extensions beyond what exists, or any new "talk to a server" feature).
|
||||
required: true
|
||||
- label: This is **not** an interactive app (game, calculator, notepad), writing/authoring tool, RSS/news/browser feature, media playback feature, or PDF rendering.
|
||||
required: true
|
||||
- label: The stock firmware does **not** already handle this well.
|
||||
required: true
|
||||
- label: No other popular CrossPoint fork already handles this well (or, if one does, I explain below why CrossPoint still needs it).
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem this solves
|
||||
description: What user-facing problem or reading-experience gap does this address? Be concrete.
|
||||
placeholder: e.g., "When reading in landscape, paragraph breaks are inconsistent because..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: stock-gap
|
||||
attributes:
|
||||
label: Why the stock firmware (and other forks) do not already solve this
|
||||
description: Explain which existing solutions you checked and why they fall short. This is the core scope filter.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: proposal
|
||||
attributes:
|
||||
label: Proposed change
|
||||
description: A short description of what you would build or change. Focus on user impact; implementation details can come later.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: tradeoffs
|
||||
attributes:
|
||||
label: Memory / flash / complexity cost
|
||||
description: |
|
||||
CrossPoint runs on 380KB of RAM. Roughly how much DRAM, flash, or code complexity does this add?
|
||||
"Don't know" is a valid answer, but please attempt an estimate.
|
||||
placeholder: e.g., "Adds ~2KB flash for the new font tables, no DRAM impact at runtime."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Anything else relevant (links, screenshots, related discussions).
|
||||
validations:
|
||||
required: false
|
||||
@@ -3,16 +3,34 @@
|
||||
* **What is the goal of this PR?** (e.g., Implements the new feature for file uploading.)
|
||||
* **What changes are included?**
|
||||
|
||||
## Scope Check
|
||||
|
||||
CrossPoint is intentionally narrow. See [SCOPE.md](../blob/master/SCOPE.md) and [ROADMAP.md](../blob/master/ROADMAP.md).
|
||||
Please confirm:
|
||||
|
||||
- [ ] I have read SCOPE.md and ROADMAP.md.
|
||||
- [ ] This PR is **not** a new built-in theme (themes are temporarily closed pending the move to SD-loaded themes).
|
||||
- [ ] This PR is **not** a new external network connector (sync engine, cloud storage, remote file access, etc.).
|
||||
- [ ] This PR is **not** an interactive app, writing tool, RSS/news/browser, media playback, or PDF feature.
|
||||
- [ ] The stock firmware does not already handle this well, **and** no other popular CrossPoint fork already does
|
||||
(or, if one does, I explain why CrossPoint still needs it below).
|
||||
- [ ] If this PR touches `freeink-sdk/`, `lib/hal/`, the bootloader, OTA, or recovery code, I have coordinated with
|
||||
the relevant maintainer.
|
||||
|
||||
**If this PR was opened against the previous (broader) scope and was already in flight under Phase 0, link the
|
||||
relevant Discussion or issue so reviewers can see the history.**
|
||||
|
||||
## Additional Context
|
||||
|
||||
* Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks,
|
||||
* Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks,
|
||||
specific areas to focus on).
|
||||
* Memory / flash impact, if known.
|
||||
|
||||
---
|
||||
|
||||
### AI Usage
|
||||
|
||||
While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it
|
||||
While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it
|
||||
helps set the right context for reviewers.
|
||||
|
||||
Did you use AI tools to help write this code? _**< YES | PARTIALLY | NO >**_
|
||||
|
||||
@@ -53,6 +53,13 @@ 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
|
||||
|
||||
@@ -76,10 +83,21 @@ 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
|
||||
# Both envs in ONE pio invocation: a second `pio run` can wipe the whole
|
||||
# .pio/build tree (project-checksum mismatch after the S3 toolchain
|
||||
# installs into the restored cache), deleting the default firmware.bin
|
||||
# before the artifact upload.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pio run | tee pio.log
|
||||
pio run -e default -e sticky | tee pio.log
|
||||
|
||||
- name: Extract firmware stats
|
||||
|
||||
@@ -102,6 +120,33 @@ 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:
|
||||
@@ -110,6 +155,7 @@ jobs:
|
||||
- build
|
||||
- clang-format
|
||||
- cppcheck
|
||||
- unit-tests
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
name: Build & Publish SD Card Fonts
|
||||
|
||||
# Fonts change rarely — run manually when font sources or the conversion
|
||||
# pipeline are updated. Publishes .cpfont files + fonts.json manifest as
|
||||
# GitHub Release assets on the crosspoint-fonts repo so font releases don't
|
||||
# clutter the firmware releases page.
|
||||
#
|
||||
# Requires a repository secret FONTS_REPO_TOKEN — a fine-grained PAT (or
|
||||
# classic PAT) with contents:write permission on the target fonts repo.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
FONTS_REPO: crosspoint-reader/crosspoint-fonts
|
||||
|
||||
jobs:
|
||||
build-fonts:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Install font tools
|
||||
run: pip install freetype-py fonttools pyyaml
|
||||
|
||||
- name: Install system dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y libfreetype6-dev
|
||||
|
||||
- name: Read version constants
|
||||
id: versions
|
||||
run: |
|
||||
cd lib/EpdFont/scripts
|
||||
echo "binary=$(python3 -c 'from cpfont_version import CPFONT_VERSION; print(CPFONT_VERSION)')" >> "$GITHUB_OUTPUT"
|
||||
echo "metadata=$(python3 -c 'from cpfont_version import FONTS_MANIFEST_VERSION; print(FONTS_MANIFEST_VERSION)')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build SD card fonts
|
||||
run: python3 lib/EpdFont/scripts/build-sd-fonts.py --clean --verbose -j 1
|
||||
|
||||
- name: Flatten output for release assets
|
||||
run: |
|
||||
mkdir -p dist
|
||||
find lib/EpdFont/scripts/output -name '*.cpfont' -exec cp {} dist/ \;
|
||||
|
||||
- name: Compute release tags
|
||||
id: tags
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.FONTS_REPO_TOKEN }}
|
||||
run: |
|
||||
BASE="sd-fonts-m${{ steps.versions.outputs.metadata }}-b${{ steps.versions.outputs.binary }}"
|
||||
echo "base=$BASE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Find the highest existing revision for this m/b pair
|
||||
LAST=$(gh release list --repo "${{ env.FONTS_REPO }}" \
|
||||
--json tagName --jq \
|
||||
'[.[] | select(.tagName | startswith("'"${BASE}-r"'")) | .tagName | split("-r")[1] | tonumber] | max // 0')
|
||||
NEXT=$((LAST + 1))
|
||||
echo "revision=$NEXT" >> "$GITHUB_OUTPUT"
|
||||
echo "versioned=${BASE}-r${NEXT}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Generate manifest
|
||||
run: |
|
||||
python3 scripts/generate-font-manifest.py \
|
||||
--input dist \
|
||||
--base-url "https://github.com/${{ env.FONTS_REPO }}/releases/download/${{ steps.tags.outputs.base }}/" \
|
||||
--output dist/fonts.json \
|
||||
--descriptions-from lib/EpdFont/scripts/sd-fonts.yaml
|
||||
|
||||
- name: Publish versioned release to fonts repo
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.FONTS_REPO_TOKEN }}
|
||||
run: |
|
||||
VERSIONED="${{ steps.tags.outputs.versioned }}"
|
||||
TITLE="SD Card Fonts (${VERSIONED#sd-fonts-})"
|
||||
|
||||
gh release create "$VERSIONED" dist/* \
|
||||
--repo "${{ env.FONTS_REPO }}" \
|
||||
--title "$TITLE" \
|
||||
--notes "Pre-built \`.cpfont\` font files for CrossPoint Reader.
|
||||
|
||||
Download individual files or use **Settings > System > Manage Fonts** on the device.
|
||||
|
||||
See [SD Card Fonts documentation](https://github.com/${{ github.repository }}/blob/main/docs/sd-card-fonts.md) for details."
|
||||
|
||||
- name: Update stable tag for device downloads
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.FONTS_REPO_TOKEN }}
|
||||
run: |
|
||||
BASE="${{ steps.tags.outputs.base }}"
|
||||
|
||||
# Delete the old stable release for this m/b pair (the versioned releases are kept)
|
||||
gh release delete "$BASE" --repo "${{ env.FONTS_REPO }}" --yes 2>/dev/null || true
|
||||
|
||||
gh release create "$BASE" dist/* \
|
||||
--repo "${{ env.FONTS_REPO }}" \
|
||||
--title "SD Card Fonts (${BASE#sd-fonts-})" \
|
||||
--notes "Current font build for manifest v${{ steps.versions.outputs.metadata }}, binary format v${{ steps.versions.outputs.binary }}. Devices with this firmware version download from this release.
|
||||
|
||||
This is revision **${{ steps.tags.outputs.revision }}** — see [\`${{ steps.tags.outputs.versioned }}\`](https://github.com/${{ env.FONTS_REPO }}/releases/tag/${{ steps.tags.outputs.versioned }}) for the immutable copy.
|
||||
|
||||
Download individual files or use **Settings > System > Manage fonts** on the device."
|
||||
@@ -25,6 +25,13 @@ 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,6 +25,13 @@ 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
|
||||
|
||||
+10
@@ -2,6 +2,8 @@
|
||||
.idea
|
||||
.DS_Store
|
||||
.vscode
|
||||
open-x4-sdk
|
||||
fs_
|
||||
lib/EpdFont/fontsrc
|
||||
lib/I18n/I18nKeys.h
|
||||
lib/I18n/I18nStrings.h
|
||||
@@ -15,3 +17,11 @@ build
|
||||
.history/
|
||||
/.venv
|
||||
*.local*
|
||||
*.cpfont
|
||||
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/
|
||||
|
||||
+4
-3
@@ -1,3 +1,4 @@
|
||||
[submodule "open-x4-sdk"]
|
||||
path = open-x4-sdk
|
||||
url = https://github.com/open-x4-epaper/community-sdk.git
|
||||
[submodule "freeink-sdk"]
|
||||
path = freeink-sdk
|
||||
url = https://github.com/Free-Ink/freeink-sdk.git
|
||||
branch = main
|
||||
|
||||
+86
-37
@@ -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 open-x4-sdk or official docs 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 freeink-sdk source or the FreeInk SDK docs (https://freeink.org/llms.txt for an LLM-readable index) 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).
|
||||
@@ -58,6 +58,7 @@ find src -name "*.cpp" -o -name "*.h" | xargs clang-format -i
|
||||
6. `constexpr` First: Compile-time constants and lookup tables must be `constexpr`, not just `static const`. This moves computation to compile time, enables dead-branch elimination, and guarantees flash placement. Use `static constexpr` for class-level constants.
|
||||
7. `std::vector` Pre-allocation: Always call `.reserve(N)` before any `push_back()` loop. Each growth event allocates a new block (2×), copies all elements, then frees the old one — three heap operations that fragment DRAM. When the final size is unknown, estimate conservatively.
|
||||
8. SPIFFS Write Throttling: Never write a settings file on every user interaction. Guard all writes with a value-change check (`if (newVal == _current) return;`). Progress saves during reading must be debounced — write on activity exit or every N page turns, not on every turn. SPIFFS sectors have a finite erase cycle limit.
|
||||
9. `new` is not nothrow on ESP32: With `-fno-exceptions`, bare `new` that fails calls `abort()` — it does NOT return `nullptr`. Always use `new (std::nothrow)` and null-check the result, or use `makeUniqueNoThrow<T>()` from `lib/Memory/Memory.h`. Never write bare `new` for any fallible allocation.
|
||||
|
||||
---
|
||||
|
||||
@@ -104,8 +105,17 @@ These flags in `platformio.ini` fundamentally affect firmware behavior:
|
||||
-DUSE_UTF8_LONG_NAMES=1 // SD card long filename support
|
||||
-DMINIZ_NO_ZLIB_COMPATIBLE_NAMES=1 // Avoid zlib name conflicts
|
||||
-DXML_GE=0 // Disable XML general entities (security)
|
||||
-DDESTRUCTOR_CLOSES_FILE=1 // FsFile destructor auto-closes (SdFat)
|
||||
```
|
||||
|
||||
**DESTRUCTOR_CLOSES_FILE implications**:
|
||||
- SdFat's `FsBaseFile` destructor calls `close()` automatically when the object goes out of scope
|
||||
- **Do NOT add explicit `file.close()` calls** for local `FsFile` variables — the destructor handles it
|
||||
- Explicit `close()` is still required in these cases:
|
||||
1. **Close before delete**: Must close before `Storage.remove()` on the same path
|
||||
2. **Close before reopen**: Must close before reopening the same `FsFile` variable (e.g., write then reopen for read, or rewrite the same path)
|
||||
3. **Member variables**: `FsFile` members persist beyond any single function scope, so close at the intended release point (e.g., in `onExit()`)
|
||||
|
||||
**SINGLE_BUFFER_MODE implications**:
|
||||
- Only ONE framebuffer exists (not double-buffered)
|
||||
- Grayscale rendering requires temporary buffer allocation (`renderer.storeBwBuffer()`)
|
||||
@@ -117,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)
|
||||
* open-x4-sdk/: Low-level SDK (EInkDisplay, InputManager, BatteryMonitor, SDCardManager)
|
||||
* freeink-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)
|
||||
@@ -142,14 +152,19 @@ These flags in `platformio.ini` fundamentally affect firmware behavior:
|
||||
#include <HalStorage.h>
|
||||
|
||||
// Use Storage singleton (defined via macro)
|
||||
FsFile file;
|
||||
HalFile file;
|
||||
if (Storage.openFileForRead("MODULE", "/path/to/file.bin", file)) {
|
||||
// Read from file
|
||||
file.close(); // Explicit close required
|
||||
// No file.close() needed — DESTRUCTOR_CLOSES_FILE=1 handles it at scope exit
|
||||
}
|
||||
```
|
||||
|
||||
**Usage**: See example above. Uses `FsFile` (SdFat), NOT Arduino `File`.
|
||||
**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).
|
||||
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
@@ -167,7 +182,7 @@ if (Storage.openFileForRead("MODULE", "/path/to/file.bin", file)) {
|
||||
|
||||
### Memory Safety and RAII
|
||||
* Smart Pointers: Prefer std::unique_ptr. Avoid std::shared_ptr (unnecessary atomic overhead for a single-core RISC-V).
|
||||
* RAII: Use destructors for cleanup, but call file.close() or vTaskDelete() explicitly for deterministic resource release.
|
||||
* RAII: Use destructors for cleanup. Call `vTaskDelete()` explicitly for deterministic task release. Do NOT call `file.close()` on local `FsFile` variables — `DESTRUCTOR_CLOSES_FILE=1` handles it at scope exit (see Critical Build Flags).
|
||||
|
||||
### ESP32-C3 Platform Pitfalls
|
||||
|
||||
@@ -257,43 +272,78 @@ When a template is necessary, limit instantiations: use explicit template instan
|
||||
|
||||
**Rules**: NO exceptions, NO abort(), ALWAYS log before error return
|
||||
|
||||
### Acceptable malloc/free Patterns
|
||||
### Heap Buffer Allocation
|
||||
|
||||
**Source**: [src/activities/home/HomeActivity.cpp:166](../src/activities/home/HomeActivity.cpp), [lib/GfxRenderer/GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp)
|
||||
**Prefer `makeUniqueNoThrow` over `malloc`.** Both are nothrow (return `nullptr` on OOM rather than calling `abort()`), but `malloc` requires a manual `free` on every return path — a common source of leaks. `makeUniqueNoThrow<uint8_t[]>(size)` from `lib/Memory/Memory.h` frees automatically when it goes out of scope.
|
||||
|
||||
Despite "prefer stack allocation," malloc is acceptable for:
|
||||
1. **Large temporary buffers** (> 256 bytes, won't fit on stack)
|
||||
2. **One-time allocations** during activity initialization
|
||||
3. **Bitmap rendering buffers** (variable size, used briefly)
|
||||
|
||||
**Pattern**:
|
||||
**Preferred pattern**:
|
||||
```cpp
|
||||
// Allocate
|
||||
auto* buffer = static_cast<uint8_t*>(malloc(bufferSize));
|
||||
#include <Memory.h>
|
||||
|
||||
auto buffer = makeUniqueNoThrow<uint8_t[]>(bufferSize);
|
||||
if (!buffer) {
|
||||
LOG_ERR("MODULE", "malloc failed: %d bytes", bufferSize);
|
||||
return false; // Handle allocation failure
|
||||
LOG_ERR("MODULE", "OOM: %d bytes", bufferSize);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use buffer
|
||||
processData(buffer, bufferSize);
|
||||
processData(buffer.get(), bufferSize);
|
||||
// freed automatically — no manual free needed, no leak on early return
|
||||
```
|
||||
|
||||
// Free immediately after use
|
||||
free(buffer);
|
||||
buffer = nullptr;
|
||||
**`malloc` or `new (std::nothrow)` are still acceptable** when the buffer must be passed to a C API that takes ownership and frees it itself (e.g., certain SDK callbacks). In that case follow the manual pattern:
|
||||
```cpp
|
||||
auto* buffer = static_cast<uint8_t*>(malloc(bufferSize)); // or new (std::nothrow) uint8_t[bufferSize]
|
||||
if (!buffer) {
|
||||
LOG_ERR("MODULE", "OOM: %d bytes", bufferSize);
|
||||
return false;
|
||||
}
|
||||
sdkApiThatTakesOwnership(buffer, bufferSize); // SDK calls free() / delete[]
|
||||
```
|
||||
|
||||
**Rules**:
|
||||
- **ALWAYS check for nullptr** after malloc
|
||||
- **Free immediately** after use (don't hold across multiple operations)
|
||||
- **Set to nullptr** after free (avoid use-after-free)
|
||||
- **Document size**: Comment why stack allocation was rejected
|
||||
- **Prefer `makeUniqueNoThrow`** — automatic cleanup eliminates leak risk on error paths
|
||||
- **ALWAYS check for nullptr** after any allocation and `LOG_ERR` before returning false
|
||||
- **Raw allocation only** when a C API takes ownership; document why in a comment
|
||||
|
||||
**Examples in codebase**:
|
||||
- Memory utilities: [Memory.h](../lib/Memory/Memory.h) (`makeUniqueNoThrow`)
|
||||
- Cover image buffers: [HomeActivity.cpp:166](../src/activities/home/HomeActivity.cpp)
|
||||
- Text chunk buffers: [TxtReaderActivity.cpp:259](../src/activities/reader/TxtReaderActivity.cpp)
|
||||
- Bitmap rendering: [GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp)
|
||||
- OTA update buffer: [OtaUpdater.cpp:40](../src/network/OtaUpdater.cpp)
|
||||
|
||||
### Heap Allocation with `new`: Always Use `makeUniqueNoThrow`
|
||||
|
||||
**CRITICAL**: With `-fno-exceptions`, bare `new` on OOM calls `abort()` — it does NOT return `nullptr`. Always use `makeUniqueNoThrow` from `lib/Memory/Memory.h`, which wraps `new (std::nothrow)` and returns a `std::unique_ptr` that is null on OOM and automatically frees on scope exit.
|
||||
|
||||
**Preferred pattern**:
|
||||
```cpp
|
||||
#include <Memory.h>
|
||||
|
||||
auto obj = makeUniqueNoThrow<MyClass>(args);
|
||||
if (!obj) { LOG_ERR("MOD", "OOM: MyClass"); return false; }
|
||||
|
||||
auto buf = makeUniqueNoThrow<uint8_t[]>(size);
|
||||
if (!buf) { LOG_ERR("MOD", "OOM: %d bytes", size); return false; }
|
||||
|
||||
// Pass to C APIs via .get(); unique_ptr frees automatically on return
|
||||
someApi(buf.get(), size);
|
||||
```
|
||||
|
||||
**`new (std::nothrow)` directly is acceptable** when the object must be passed to a C API that takes ownership and calls `delete` itself:
|
||||
```cpp
|
||||
auto* obj = new (std::nothrow) MyClass(args);
|
||||
if (!obj) { LOG_ERR("MOD", "OOM: MyClass"); return false; }
|
||||
sdkApiThatTakesOwnership(obj); // SDK calls delete
|
||||
```
|
||||
|
||||
**Rules**:
|
||||
- **Prefer `makeUniqueNoThrow`** — automatic cleanup eliminates leak risk on error paths
|
||||
- **NEVER use bare `new`** — always `makeUniqueNoThrow` or `new (std::nothrow)`
|
||||
- **ALWAYS `LOG_ERR` before returning false** on OOM
|
||||
- **Use `.get()`** to pass the raw pointer to C-style APIs; ownership stays with the `unique_ptr`
|
||||
- **`new (std::nothrow)` directly only** when a C API takes ownership; document why in a comment
|
||||
|
||||
**Examples in codebase**:
|
||||
- Memory utilities: [Memory.h](../lib/Memory/Memory.h) (`makeUniqueNoThrow`)
|
||||
|
||||
---
|
||||
|
||||
@@ -376,13 +426,13 @@ void enterNewActivity(Activity* activity) {
|
||||
- Activity navigation = `delete` old activity + `new` create next activity
|
||||
- Any memory allocated in `onEnter()` MUST be freed in `onExit()`
|
||||
- FreeRTOS tasks MUST be deleted in `onExit()` before activity destruction
|
||||
- File handles MUST be closed in `onExit()`
|
||||
- Member `FsFile` handles MUST be closed in `onExit()` (local `FsFile` variables auto-close via destructor)
|
||||
|
||||
**Activity Pattern**:
|
||||
```cpp
|
||||
void onEnter() { Activity::onEnter(); /* alloc: buffer, tasks */ render(); }
|
||||
void loop() { mappedInput.update(); /* handle input */ }
|
||||
void onExit() { /* free: vTaskDelete, free buffer, close files */ Activity::onExit(); }
|
||||
void onExit() { /* free: vTaskDelete, free buffer, close member FsFiles */ Activity::onExit(); }
|
||||
```
|
||||
|
||||
**Critical**: Free resources in reverse order. Delete tasks BEFORE activity destruction.
|
||||
@@ -405,9 +455,8 @@ void onExit() { /* free: vTaskDelete, free buffer, close files */ Activity::on
|
||||
**Source**: [src/main.cpp:40-115](../src/main.cpp)
|
||||
|
||||
**All fonts are loaded as global static objects** at firmware startup:
|
||||
- Bookerly: 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)
|
||||
- OpenDyslexic: 8, 10, 12, 14pt (4 styles each)
|
||||
- Ubuntu UI fonts: 10, 12pt (2 styles)
|
||||
|
||||
**Total**: ~80+ global `EpdFont` and `EpdFontFamily` objects
|
||||
@@ -847,8 +896,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 5** (metadata structure)
|
||||
- `section.bin`: **Version 12** (layout structure)
|
||||
- `book.bin`: **Version 7** (metadata structure)
|
||||
- `section.bin`: **Version 25** (layout structure)
|
||||
|
||||
**Version Increment Rules**:
|
||||
1. **ALWAYS increment version** BEFORE changing binary structure
|
||||
@@ -858,7 +907,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 = 13; // Was 12, now 13
|
||||
static constexpr uint8_t SECTION_FILE_VERSION = 26; // Was 25, now 26
|
||||
|
||||
// Add new field to structure
|
||||
struct PageLine {
|
||||
@@ -869,4 +918,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.
|
||||
|
||||
@@ -1,104 +1,196 @@
|
||||
# CrossPoint Reader
|
||||
|
||||
Firmware for the **Xteink X4** e-paper display reader (unaffiliated with Xteink).
|
||||
Built using **PlatformIO** and targeting the **ESP32-C3** microcontroller.
|
||||
[](https://app.royalty.dev/crosspoint-reader/crosspoint-reader)
|
||||
|
||||
CrossPoint Reader is a purpose-built firmware designed to be a drop-in, fully open-source replacement for the official
|
||||
Xteink firmware. It aims to match or improve upon the standard EPUB reading experience.
|
||||
CrossPoint is open-source e-reader firmware - community-built, fully hackable, free forever. It's maintained by a growing community of developers and readers who believe your device should do what you want - not what a manufacturer decided for you.
|
||||
|
||||

|
||||
**Now running on:** ESP32C3-based Xteink [X4](https://www.xteink.com/products/xteink-x4) and [X3](https://www.xteink.com/products/xteink-x3).
|
||||
|
||||
## Motivation
|
||||

|
||||
|
||||
E-paper devices are fantastic for reading, but most commercially available readers are closed systems with limited
|
||||
customisation. The **Xteink X4** is an affordable, e-paper device, however the official firmware remains closed.
|
||||
CrossPoint exists partly as a fun side-project and partly to open up the ecosystem and truely unlock the device's
|
||||
potential.
|
||||
> If you're planning to buy an Xteink device, consider purchasing an **X3/X4 Developer Edition** through https://crosspointreader.com. CrossPoint receives a small share of each sale, helping fund development costs.
|
||||
|
||||
CrossPoint Reader aims to:
|
||||
* Provide a **fully open-source alternative** to the official firmware.
|
||||
* Offer a **document reader** capable of handling EPUB content on constrained hardware.
|
||||
* Support **customisable font, layout, and display** options.
|
||||
* Run purely on the **Xteink X4 hardware**.
|
||||
## What can CrossPoint do?
|
||||
|
||||
This project is **not affiliated with Xteink**; it's built as a community project.
|
||||
- **Reader engine**: EPUB 2/3 rendering with embedded-style option, image handling, hyphenation, kerning, chapter navigation, footnotes, bookmarks, dictionary lookups ([StarDict](docs/dictionary.md)), go-to-percent, auto page turn, orientation control, focus reading, KOReader progress sync and more.
|
||||
|
||||
## Features & Usage
|
||||
- **Various formats**: native handling for `.epub`, `.xtc/.xtch`, `.txt`, and `.bmp`.
|
||||
|
||||
- [x] EPUB parsing and rendering (EPUB 2 and EPUB 3)
|
||||
- [x] Image support within EPUB
|
||||
- [x] Saved reading position
|
||||
- [x] File explorer with file picker
|
||||
- [x] Basic EPUB picker from root directory
|
||||
- [x] Support nested folders
|
||||
- [ ] EPUB picker with cover art
|
||||
- [x] Custom sleep screen
|
||||
- [x] Cover sleep screen
|
||||
- [x] Wifi book upload
|
||||
- [x] Wifi OTA updates
|
||||
- [x] KOReader Sync integration for cross-device reading progress
|
||||
- [x] Configurable font, layout, and display options
|
||||
- [ ] User provided fonts
|
||||
- [ ] Full UTF support
|
||||
- [x] Screen rotation
|
||||
- **Screenshots.**
|
||||
|
||||
Multi-language support: Read EPUBs in various languages, including English, Spanish, French, German, Italian, Portuguese, Russian, Ukrainian, Polish, Swedish, Norwegian, [and more](./USER_GUIDE.md#supported-languages).
|
||||
- **Custom fonts**: install your favorite fonts on the SD card.
|
||||
|
||||
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint, including the
|
||||
[KOReader Sync quick setup](./USER_GUIDE.md#365-koreader-sync-quick-setup).
|
||||
- **Tilt page turn (X3 only)**.
|
||||
|
||||
For more details about the scope of the project, see the [SCOPE.md](SCOPE.md) document.
|
||||
- **Library workflow**: folder browser, hidden-file toggle, long-press delete, recent books, SD-cache management.
|
||||
|
||||
## Installing
|
||||
- **Wireless workflows**:
|
||||
|
||||
- File transfer web UI
|
||||
- EPUB Optimizer
|
||||
- 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
|
||||
- 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
|
||||
|
||||
### Web (latest firmware)
|
||||
- **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.
|
||||
|
||||
1. Connect your Xteink X4 to your computer via USB-C and wake/unlock the device
|
||||
2. Go to https://xteink.dve.al/ and click "Flash CrossPoint firmware"
|
||||
- **Localization**: 24 UI languages and counting. RTL support.
|
||||
|
||||
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
|
||||
back to the other partition using the "Swap boot partition" button here https://xteink.dve.al/debug.
|
||||
### Coming soon:
|
||||
|
||||
### Web (specific firmware version)
|
||||
- More themes.
|
||||
|
||||
1. Connect your Xteink X4 to your computer via USB-C
|
||||
2. Download the `firmware.bin` file from the release of your choice via the [releases page](https://github.com/crosspoint-reader/crosspoint-reader/releases)
|
||||
3. Go to https://xteink.dve.al/ and flash the firmware file using the "OTA fast flash controls" section
|
||||
- Much more! stay tuned.
|
||||
|
||||
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
|
||||
back to the other partition using the "Swap boot partition" button here https://xteink.dve.al/debug.
|
||||
---
|
||||
|
||||
## USB-locked devices (Xteink Unlocker)
|
||||
|
||||
Some Xteink units purchased from third-party stores (e.g. AliExpress) ship with USB flashing locked from the factory.
|
||||
If your device is locked, you will need to use the **Xteink Unlocker** tool available at
|
||||
https://crosspointreader.com/#unlock-tool before you can flash CrossPoint.
|
||||
|
||||
**You do not need this tool if you bought your device directly from xteink.com.** Those units are not locked.
|
||||
|
||||
**Not sure if your device is locked?** Power it on, connect the USB-C cable, and try flashing via the web flasher first (see
|
||||
[Install firmware](#install-firmware) below). If the browser's serial device picker does not show your device, try a different
|
||||
USB port or browser before assuming the device is locked. Only reach for the unlocker if the device still doesn't appear.
|
||||
|
||||
> ### ⚠️ WARNING: READ THIS BEFORE USING THE UNLOCKER ⚠️
|
||||
>
|
||||
> **The only officially supported firmwares in the unlock tool are CrossPoint and CrossInk.**
|
||||
>
|
||||
> Flashing any other firmware on a USB-locked device may **permanently brick the device** or leave it **permanently
|
||||
> stuck on that firmware with no recovery path**. Once USB flashing is re-locked, your only way back is via OTA, and if
|
||||
> the firmware you flashed doesn't support OTA, **there is no way out**.
|
||||
|
||||
## Install firmware
|
||||
|
||||
### Web installer (recommended)
|
||||
|
||||
1. Connect your device to your computer via USB-C and wake/unlock the device
|
||||
2. Go to https://crosspointreader.com/#flash-tools, select device (X3 or X4), and choose an official CrossPoint release.
|
||||
|
||||
### Web installer (specific version)
|
||||
|
||||
1. Connect your device to your computer via USB-C and wake/unlock the device
|
||||
2. Download a `firmware.bin` from [Releases](https://github.com/crosspoint-reader/crosspoint-reader/releases), local build, or continuous integration artifact.
|
||||
3. Go to https://crosspointreader.com/#flash-tools, select device (X3 or X4), click "Custom .bin" and upload a `firmware.bin`.
|
||||
|
||||
### Revert to Official Firmware
|
||||
|
||||
To revert to the official firmware, you can also flash the latest official firmware using https://crosspointreader.com/#flash-tools.
|
||||
|
||||
### Command line
|
||||
|
||||
1. Install [`esptool`](https://github.com/espressif/esptool):
|
||||
|
||||
```bash
|
||||
pip install esptool
|
||||
```
|
||||
|
||||
2. Download `firmware.bin` from the [releases page](https://github.com/crosspoint-reader/crosspoint-reader/releases).
|
||||
3. Connect your device via USB-C.
|
||||
4. Find the device port. On Linux, run `dmesg` after connecting. On macOS:
|
||||
|
||||
```bash
|
||||
log stream --predicate 'subsystem == "com.apple.iokit"' --info
|
||||
```
|
||||
|
||||
5. Flash:
|
||||
|
||||
```bash
|
||||
esptool.py --chip esp32c3 --port /dev/ttyACM0 --baud 921600 write_flash 0x10000 /path/to/firmware.bin
|
||||
```
|
||||
|
||||
Adjust `/dev/ttyACM0` to match your system.
|
||||
|
||||
### Manual
|
||||
|
||||
See [Development](#development) below.
|
||||
See [Development quick start](#development-quick-start) below.
|
||||
|
||||
## Development
|
||||
---
|
||||
|
||||
## Custom SD-card fonts
|
||||
|
||||
Convert your own TTF/OTF files into `.cpfont` files that load from the SD card. No firmware reflash is needed.
|
||||
|
||||
1. Go to https://crosspointreader.com/fonts and open the "SD-card font builder" form.
|
||||
2. Upload up to four styles (regular, bold, italic, bold-italic), set the family name, point sizes, and Unicode range.
|
||||
3. Download the generated `.cpfont` files.
|
||||
4. Copy them to your SD card under `/fonts/YourFont/` (or `/.fonts/YourFont/` to hide the folder).
|
||||
5. Select the font on the device from the font settings.
|
||||
|
||||
Conversion runs the firmware repo's `lib/EpdFont/scripts/fontconvert_sdcard.py` script unmodified, so output matches a local host build.
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
- [User Guide](./USER_GUIDE.md)
|
||||
- [Web server usage](./docs/webserver.md)
|
||||
- [Web server endpoints](./docs/webserver-endpoints.md)
|
||||
- [Project scope](./SCOPE.md)
|
||||
- [Contributing docs](./docs/contributing/README.md)
|
||||
- [Touch and UI development](./docs/contributing/touch-and-ui.md) - FreeInkUI components for new screens, the touch bridge for existing ones, and build envs for the non-Xteink touch devices
|
||||
|
||||
---
|
||||
|
||||
## Development quick start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
* **PlatformIO Core** (`pio`) or **VS Code + PlatformIO IDE**
|
||||
* Python 3.8+
|
||||
* USB-C cable for flashing the ESP32-C3
|
||||
* Xteink X4
|
||||
- [pioarduino](https://github.com/pioarduino/pioarduino) or VS Code + pioarduino plugin
|
||||
- Python 3.8+
|
||||
- `clang-format` 21
|
||||
- USB-C cable supporting data transfer
|
||||
|
||||
### Checking out the code
|
||||
### Setup
|
||||
|
||||
CrossPoint uses PlatformIO for building and flashing the firmware. To get started, clone the repository:
|
||||
|
||||
```
|
||||
```bash
|
||||
git clone --recursive https://github.com/crosspoint-reader/crosspoint-reader
|
||||
cd crosspoint-reader
|
||||
|
||||
# Or, if you've already cloned without --recursive:
|
||||
# if cloned without --recursive:
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
### Flashing your device
|
||||
### Nix/NixOS
|
||||
|
||||
Connect your Xteink X4 to your computer via USB-C and run the following command.
|
||||
Nix/NixOS users can enter the development shell with either `nix develop` (flakes) or `nix-shell`:
|
||||
|
||||
```sh
|
||||
```bash
|
||||
nix develop -f nix
|
||||
# or
|
||||
nix-shell nix
|
||||
```
|
||||
|
||||
To flash a connected ESP32-C3 device, enable PlatformIO's udev rules in your NixOS configuration:
|
||||
|
||||
```nix
|
||||
services.udev.packages = with pkgs; [ platformio-core.udev ];
|
||||
```
|
||||
|
||||
After rebuilding the system configuration, reconnect the device or reload udev rules.
|
||||
|
||||
### Build / flash / monitor
|
||||
|
||||
```bash
|
||||
pio run --target upload
|
||||
```
|
||||
|
||||
### Contributor pre-PR checks
|
||||
|
||||
```bash
|
||||
./bin/clang-format-fix
|
||||
pio check -e default
|
||||
pio run -e default
|
||||
```
|
||||
|
||||
### Debugging
|
||||
|
||||
After flashing the new features, it’s recommended to capture detailed logs from the serial port.
|
||||
@@ -108,7 +200,9 @@ First, make sure all required Python packages are installed:
|
||||
```python
|
||||
python3 -m pip install pyserial colorama matplotlib
|
||||
```
|
||||
after that run the script:
|
||||
|
||||
After that run the script:
|
||||
|
||||
```sh
|
||||
# For Linux
|
||||
# This was tested on Debian and should work on most Linux systems.
|
||||
@@ -117,63 +211,77 @@ python3 scripts/debugging_monitor.py
|
||||
# For macOS
|
||||
python3 scripts/debugging_monitor.py /dev/cu.usbmodem2101
|
||||
```
|
||||
|
||||
Minor adjustments may be required for Windows.
|
||||
|
||||
## Internals
|
||||
|
||||
CrossPoint Reader is pretty aggressive about caching data down to the SD card to minimise RAM usage. The ESP32-C3 only
|
||||
has ~380KB of usable RAM, so we have to be careful. A lot of the decisions made in the design of the firmware were based
|
||||
on this constraint.
|
||||
|
||||
### Data caching
|
||||
|
||||
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:
|
||||
|
||||
|
||||
```
|
||||
.crosspoint/
|
||||
├── epub_12471232/ # Each EPUB is cached to a subdirectory named `epub_<hash>`
|
||||
│ ├── progress.bin # Stores reading progress (chapter, page, etc.)
|
||||
│ ├── cover.bmp # Book cover image (once generated)
|
||||
│ ├── book.bin # Book metadata (title, author, spine, table of contents, etc.)
|
||||
│ └── sections/ # All chapter data is stored in the sections subdirectory
|
||||
│ ├── 0.bin # Chapter data (screen count, all text layout info, etc.)
|
||||
│ ├── 1.bin # files are named by their index in the spine
|
||||
│ └── ...
|
||||
│
|
||||
└── epub_189013891/
|
||||
```
|
||||
|
||||
Deleting the `.crosspoint` directory will clear the entire cache.
|
||||
|
||||
Due the way it's currently implemented, the cache is not automatically cleared when a book is deleted and moving a book
|
||||
file will use a new cache directory, resetting the reading progress.
|
||||
|
||||
For more details on the internal file structures, see the [file formats document](./docs/file-formats.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are very welcome!
|
||||
|
||||
If you are new to the codebase, start with the [contributing docs](./docs/contributing/README.md).
|
||||
|
||||
If you're looking for a way to help out, take a look at the [ideas discussion board](https://github.com/crosspoint-reader/crosspoint-reader/discussions/categories/ideas).
|
||||
If there's something there you'd like to work on, leave a comment so that we can avoid duplicated effort.
|
||||
|
||||
Everyone here is a volunteer, so please be respectful and patient. For more details on our goverance and community
|
||||
principles, please see [GOVERNANCE.md](GOVERNANCE.md).
|
||||
|
||||
### To submit a contribution:
|
||||
|
||||
1. Fork the repo
|
||||
2. Create a branch (`feature/dithering-improvement`)
|
||||
3. Make changes
|
||||
4. Submit a PR
|
||||
|
||||
---
|
||||
|
||||
CrossPoint Reader is **not affiliated with Xteink or any manufacturer of the X4 hardware**.
|
||||
## Internals
|
||||
|
||||
Huge shoutout to [**diy-esp32-epub-reader** by atomic14](https://github.com/atomic14/diy-esp32-epub-reader), which was a project I took a lot of inspiration from as I
|
||||
was making CrossPoint.
|
||||
CrossPoint Reader is pretty aggressive about caching data down to the SD card to minimise RAM usage. The ESP32-C3 only has ~380KB of usable RAM, so we have to be careful. A lot of the decisions made in the design of the firmware were based on this constraint.
|
||||
|
||||
### Data caching
|
||||
|
||||
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
|
||||
.crosspoint/
|
||||
├── epub_<hash>/ # one directory per book, named by content hash
|
||||
│ ├── 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.
|
||||
|
||||
For more details on the internal file structures, see the [file formats document](./docs/file-formats.md).
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome. If you're new to the codebase, start with the [contributing docs](./docs/contributing/README.md). For things to work on, check the [ideas discussion board](https://github.com/crosspoint-reader/crosspoint-reader/discussions/categories/ideas) — leave a comment before starting so we don't duplicate effort.
|
||||
|
||||
Everyone here is a volunteer, so please be respectful and patient. For governance and community expectations, see [GOVERNANCE.md](./GOVERNANCE.md).
|
||||
|
||||
---
|
||||
|
||||
## Community forks
|
||||
|
||||
One of the best things about open source is that anyone can take the code in a different direction. If you need something outside CrossPoint's [scope](./SCOPE.md), check out the community forks:
|
||||
|
||||
- [CrossInk](https://github.com/uxjulia/CrossInk) — Typography and reading tracking: Bionic Reading (bolds word stems to create fixation points), guide dots between words, improved paragraph indents, and replaces the default fonts with ChareInk/Lexend/Bitter.
|
||||
|
||||
- [papyrix-reader](https://github.com/bigbag/papyrix-reader) — Adds FB2 and MD format support. Actively maintained with Arabic script support. Custom themes via SD card.
|
||||
|
||||
- ~~[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.~~ (Unmaintained)
|
||||
|
||||
- [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.
|
||||
|
||||
- ~~[PlusPoint](https://github.com/ngxson/pluspoint-reader) — custom JS apps support.~~ (Unmaintained)
|
||||
|
||||
- [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.
|
||||
|
||||
---
|
||||
|
||||
CrossPoint Reader is **not affiliated with Xteink or any device manufacturer**.
|
||||
|
||||
Huge shoutout to [diy-esp32-epub-reader](https://github.com/atomic14/diy-esp32-epub-reader), which inspired this project.
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
# CrossPoint Reader Roadmap
|
||||
|
||||
This roadmap describes how CrossPoint is moving through the tighter scope defined in [SCOPE.md](SCOPE.md). It is
|
||||
intentionally phased: Phase 0 closed out the commitments already in flight before locking down to the stricter
|
||||
"fill gaps the stock firmware leaves" delineator.
|
||||
|
||||
Phases are sequential. We do not start the next phase until the prior one is wrapped or explicitly carried over.
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 - Close Out Legacy Scope Items — **COMPLETE**
|
||||
|
||||
**Goal:** Land the work that was already in motion under the prior, broader scope so contributors are not left
|
||||
hanging, and so we enter the stricter phases with a clean slate.
|
||||
|
||||
**Landed in Phase 0:**
|
||||
|
||||
* **RTL support PRs.** The in-flight right-to-left work was reviewed, iterated, and merged.
|
||||
* **Dictionary PR.** The offline dictionary lookup work was reviewed and merged.
|
||||
* **Bookmarks** feature. First-class navigation markers in EPUBs.
|
||||
* ~~**Transparent sleep screens.**~~ Shelved; not picked back up under the stricter phases.
|
||||
|
||||
Phase 0 is closed. The tighter scope in [SCOPE.md](SCOPE.md) is now fully enforced. "But it was on the old roadmap"
|
||||
is not a valid argument for accepting a PR.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 - Consolidation, Footprint, and Multi-Device Support — **IN PROGRESS**
|
||||
|
||||
**Goal:** Reduce memory and flash usage, clean up the codebase, and land the SDK / HAL generalization work so
|
||||
CrossPoint runs cleanly on ESP32-based e-reader hardware beyond Xteink (X3 / X4), including ESP32-S3 class devices.
|
||||
|
||||
**Focus areas:**
|
||||
|
||||
* DRAM and heap fragmentation reduction across the reader core.
|
||||
* Flash footprint reduction (dead code, redundant strings, oversized tables).
|
||||
* Refactors that tighten the HAL / SDK boundary.
|
||||
* Pluggable per-device SDK layers (display, input, storage, battery) and per-device build configuration without
|
||||
forking the reader core.
|
||||
* Documentation for adding a new ESP32 e-reader target.
|
||||
* E-ink driver refinement (ghosting, partial update behavior).
|
||||
|
||||
**Closed during this phase:** new themes built into firmware, new external network connectors (sync engines, cloud
|
||||
storage, remote file access).
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 - Languages, Fonts, and Themes
|
||||
|
||||
**Goal:** With the codebase smaller and portable, make reading great in every language: multi-language support,
|
||||
better font support with custom fonts, UI translations, and themes loaded from the SD card instead of consuming
|
||||
flash.
|
||||
|
||||
**Focus areas:**
|
||||
|
||||
* Multi-language reading support (underserved languages, complex script support where realistic on ESP32 hardware).
|
||||
* Better font support and custom fonts.
|
||||
* UI languages and localization.
|
||||
* Moving themes off-firmware to SD-loaded assets (see SCOPE.md Section 6).
|
||||
* **Moving hyphenation files off-firmware.** Hyphenation rules vary per language and the files are large (German
|
||||
alone is ~200KB). Today these eat flash budget that should be available for the reader core. The plan is to build
|
||||
a downloader analogous to the existing font downloader and store the dictionaries on SD / SPIFFS, loading on
|
||||
demand. This unlocks better hyphenation for long-word languages (German, Finnish, Norwegian, etc.) without paying
|
||||
the flash cost up front.
|
||||
|
||||
This phase depends on Phase 1 cleanup landing first; otherwise we generalize a moving target.
|
||||
|
||||
---
|
||||
|
||||
## Out of Roadmap
|
||||
|
||||
The following are explicitly *not* on the roadmap. They may live in other CrossPoint forks; they will not be picked
|
||||
up here:
|
||||
|
||||
* Interactive apps (games, calculators, notepads).
|
||||
* Writing / authoring tools.
|
||||
* Active connectivity features (RSS, news, browsers).
|
||||
* PDF rendering as a first-class format.
|
||||
|
||||
See [SCOPE.md](SCOPE.md) for the full rationale.
|
||||
|
||||
---
|
||||
|
||||
## How This Roadmap Changes
|
||||
|
||||
* Phase boundaries are decided by maintainers, not by individual PRs.
|
||||
* If a phase needs to be extended or an item carried over, that is documented here with a short note.
|
||||
* Proposals for new phases or reordering should go through a Discussion first.
|
||||
@@ -1,58 +1,100 @@
|
||||
# Project Vision & Scope: CrossPoint Reader
|
||||
|
||||
The goal of CrossPoint Reader is to create an efficient, open-source reading experience for the Xteink X4. We believe a
|
||||
dedicated e-reader should do one thing exceptionally well: **facilitate focused reading.**
|
||||
The goal of CrossPoint Reader is to create an efficient, open-source reading experience for ESP32-based e-reader devices. Xteink hardware (X3, X4) is where the project started and remains a primary target, but CrossPoint is explicitly broadening to support the wider ecosystem of small ESP32 e-ink readers. We believe a dedicated e-reader should do one thing exceptionally well: **facilitate focused reading.**
|
||||
|
||||
## 1. Core Mission
|
||||
|
||||
To provide a lightweight, high-performance firmware that maximizes the potential of the X4, prioritizing legibility and
|
||||
usability over "swiss-army-knife" functionality.
|
||||
To provide a lightweight, high-performance firmware that maximizes the potential of ESP32-based e-reader hardware, prioritizing legibility, performance, and usability over "swiss-army-knife" functionality.
|
||||
|
||||
## 2. Scope
|
||||
CrossPoint is **not** a kitchen-sink firmware, and it is **not** Xteink-only. We want clean, maintainable code that the community can build on, and that runs across the range of ESP32 e-reader devices (ESP32-C3, ESP32-S3, and adjacent variants). Every accepted change should make that goal easier, not harder. Device-specific code should live behind the HAL / SDK boundary so the reader core stays portable.
|
||||
|
||||
## 2. Guiding Principle: Fill Gaps the Stock Firmware Leaves
|
||||
|
||||
CrossPoint exists to do the things the stock firmware does poorly or not at all. New work is evaluated against that delineator:
|
||||
|
||||
* **Does the stock firmware already do this well?** We should hit that bar or surpass it
|
||||
* **Is another popular CrossPoint fork already solving this well?** If yes, we generally defer to that fork if it's not part of the core reading experience. e.g. stats
|
||||
* **Does this directly improve the reading experience or the firmware's long-term maintainability?** If no, it is out of scope.
|
||||
|
||||
## 3. Current Focus
|
||||
|
||||
We are intentionally narrowing scope to consolidate the codebase as we open it up to more ESP32 e-reader devices.
|
||||
|
||||
During this period, the priorities are:
|
||||
|
||||
* **Memory footprint:** Reducing DRAM usage and heap fragmentation. The ESP32-C3 is the tightest target and sets the ceiling, but the gains benefit every ESP32 variant we run on.
|
||||
* **Flash footprint:** Trimming binary size to leave room for additional device targets and features.
|
||||
* **Code cleanup:** Refactoring, removing dead code, tightening abstractions, and improving readability.
|
||||
* **Reading experience:** EPUB parsing and rendering, typography, hyphenation, line spacing, font handling, and legibility improvements.
|
||||
|
||||
### Temporarily Closed Areas
|
||||
|
||||
PRs in the following areas will be closed until this notice is lifted. Adding these now makes the cleanup and multi-device work materially harder:
|
||||
|
||||
* **New themes.** The existing theming surface is frozen.
|
||||
* **New external network connectors.** This includes sync engines, cloud storage clients, OPDS extensions beyond what exists, remote file access, and any new "talk to a server" feature. We now have our own CrossPoint KOSync server which gives us a way to sync to 3rd party systems like Hardcover at an API level instead of bloating the firmware. If you're interested in helping here, the sync server is also open source.
|
||||
|
||||
If you are unsure whether your idea falls into one of these categories, open a Discussion first.
|
||||
|
||||
## 4. Scope
|
||||
|
||||
### In-Scope
|
||||
|
||||
*These are features that directly improve the primary purpose of the device.*
|
||||
*Features that directly improve the core reading experience or the firmware's maintainability.*
|
||||
|
||||
* **User Experience:** E.g. User-friendly interfaces, and interactions, both inside the reader and navigating the
|
||||
firmware. This includes things like button mapping, book loading, and book navigation like bookmarks.
|
||||
* **Document Rendering:** E.g. Support for rendering documents (primarily EPUB) and improvements to the rendering
|
||||
engine.
|
||||
* **Format Optimization:** E.g. Efficiently parsing EPUB (CSS/Images) and other documents within the device's
|
||||
capabilities.
|
||||
* **Typography & Legibility:** E.g. Custom font support, hyphenation engines, and adjustable line spacing.
|
||||
* **E-Ink Driver Refinement:** E.g. Reducing full-screen flashes (ghosting management) and improving general rendering.
|
||||
* **Library Management:** E.g. Simple, intuitive ways to organize and navigate a collection of books.
|
||||
* **Local Transfer:** E.g. Simple, "pull" based book loading via a basic web-server or public and widely-used standards.
|
||||
* **Language Support:** E.g. Support for multiple languages both in the reader and in the interfaces.
|
||||
* **Reference Tools:** E.g. Local dictionary lookup. Providing quick, offline definitions to enhance comprehension
|
||||
without breaking focus.
|
||||
* **EPUB Rendering & Optimization:** Improvements to the rendering engine, CSS/image handling, and parsing performance.
|
||||
* **Typography & Legibility:** Custom font support, hyphenation, line and paragraph spacing, margins.
|
||||
* **E-Ink Driver Refinement:** Reducing full-screen flashes (ghosting management) and improving general rendering.
|
||||
* **Reading UX:** Bookmarks, progress tracking, button mapping, page navigation, and other in-reader interactions.
|
||||
* **Library Management:** Simple, intuitive ways to organize and navigate a local book collection.
|
||||
* **Memory, Flash, and Code Quality:** Refactors and cleanups that reduce resource use or improve maintainability, even without a user-visible feature.
|
||||
|
||||
### Out-of-Scope
|
||||
|
||||
*These items are rejected because they compromise the device's stability or mission.*
|
||||
*Rejected because they compromise the device's stability, maintainability, or core mission.*
|
||||
|
||||
* **Interactive Apps:** No Notepads, Calculators, or Games. This is a reader, not a PDA.
|
||||
* **Active Connectivity:** No RSS readers, News aggregators, or Web browsers. Background Wi-Fi tasks drain the battery
|
||||
and complicate the single-core CPU's execution.
|
||||
* **Media Playback:** No Audio players or Audio-books.
|
||||
* **Complex Annotation:** No typed out notes. These features are better suited for devices with better input
|
||||
capabilities and more powerful chips.
|
||||
* **Interactive Apps:** No notepads, calculators, or games. These belong in other forks and are not part of CrossPoint's focus.
|
||||
* **Writing / Authoring Tools:** No typed notes, journals, or editors. Input hardware and RAM are wrong for this, and other forks already explore this space.
|
||||
* **Active Connectivity:** No RSS readers, news aggregators, or web browsers. Background Wi-Fi drains the battery and complicates the single-core CPU.
|
||||
* **PDF Rendering:** PDFs are fixed-layout documents, so rendering them requires displaying pages as images rather than reflowable text, resulting in constant panning and zooming that makes for a poor reading experience on e-ink. Out of scope on the current hardware class.
|
||||
|
||||
### In-scope — Technically Unsupported
|
||||
## 5. Calls to Action
|
||||
|
||||
*These features align with CrossPoint's goals but are impractical on the current hardware or produce poor UX.*
|
||||
These are the areas where contributor help is most valuable right now. If you want to take one of these on, open a Discussion or issue first so we can coordinate.
|
||||
|
||||
* **Clock Display:** The ESP32-C3's RTC drifts significantly during deep sleep; making the clock untrustworthy after any sleep cycle. NTP sync could help, but CrossPoint doesn't connect to the internet on every boot.
|
||||
### Theme System: Move Themes Off-Firmware
|
||||
|
||||
* **PDF Rendering:** PDFs are fixed-layout documents, so rendering them requires displaying pages as images rather than reflowable text — resulting in constant panning and zooming that makes for a poor reading experience on e-ink.
|
||||
We want to abstract themes out of the firmware entirely so they no longer consume flash, and instead load from the SD card. This directly supports the current focus on flash footprint and code cleanup.
|
||||
|
||||
## 3. Idea Evaluation
|
||||
* **Status:** [@itsthisjustin](https://github.com/itsthisjustin) plans to take this on eventually but is very open to someone else claiming it sooner.
|
||||
* **Why it matters:** Every built-in theme costs flash that we would rather spend on rendering, fonts, or future device support. SD-loaded themes also let users customize without rebuilding firmware. It also leads to SD font loading for better language support in the UI.
|
||||
* **How to claim:** Comment on the relevant Discussion (or open one) before starting.
|
||||
|
||||
While I appreciate the desire to add new and exciting features to CrossPoint Reader, CrossPoint Reader is designed to be
|
||||
a lightweight, reliable, and performant e-reader. Things which distract or compromise the device's core mission will not
|
||||
be accepted. As a guiding question, consider if your idea improve the "core reading experience" for the average user,
|
||||
and, critically, not distract from that reading experience.
|
||||
### Identifying Other Stock-Firmware Gaps
|
||||
|
||||
> **Note to Contributors:** If you are unsure if your idea fits the scope, please open a **Discussion** before you start
|
||||
> coding!
|
||||
We want help cataloguing things the stock firmware (and other popular CrossPoint forks) handle poorly or not at all, so future work has a clear target list. Particularly interested in:
|
||||
|
||||
* **RTL (right-to-left) text support:** Arabic, Hebrew, Persian, and similar scripts.
|
||||
* **Languages with poor stock and fork coverage:** Especially those that need shaping, complex layout, or non-Latin font work that nobody is handling well today.
|
||||
* **Other gaps:** Rendering edge cases, accessibility issues, input quirks, anything stock does badly and existing forks have not fixed.
|
||||
|
||||
If you can read or use the device in one of these languages, your feedback (even without code) is genuinely useful. Open a Discussion with concrete examples (screenshots, sample EPUBs, expected vs actual behavior) and we will prioritize from there.
|
||||
|
||||
## 6. Funding and Contributor Sustainability
|
||||
|
||||
CrossPoint uses [Royalty.dev](https://royalty.dev) (yes, a product built by [@itsthisjustin](https://github.com/itsthisjustin)) to fund contributors. There has been some tension in the community around this, so the intent is being clarified here directly.
|
||||
|
||||
**Why we do this:**
|
||||
|
||||
* To maintain long-term interest from contributors and maintainers, in direct response to substantial community requests for a way to give back.
|
||||
* To motivate contributors to invest in the *core* project rather than spinning up competing forks.
|
||||
* To help pay for new ESP32 devices so we can port CrossPoint to additional hardware.
|
||||
* To give the project a credible long-term path to sustainability.
|
||||
|
||||
**How it works:**
|
||||
|
||||
* Funds are distributed automatically to contributors based on impact to the codebase and tenure on the project.
|
||||
* Over **$600** was raised in the first few hours after opening up funding, which is a signal the demand is real.
|
||||
* The exact scoring methodology is published at <https://app.royalty.dev/transparency>.
|
||||
|
||||
**This is not fixed in stone.** The weighting, eligibility, and distribution rules can be tweaked as we learn what works for this project. If you have concerns or suggestions about how funds are allocated, open a Discussion. The goal is a system that fairly recognizes the people doing the work, not a perfect one on day one.
|
||||
|
||||
+363
-69
@@ -5,6 +5,7 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
|
||||
- [CrossPoint User Guide](#crosspoint-user-guide)
|
||||
- [1. Hardware Overview](#1-hardware-overview)
|
||||
- [Button Layout](#button-layout)
|
||||
- [Taking a Screenshot](#taking-a-screenshot)
|
||||
- [2. Power \& Startup](#2-power--startup)
|
||||
- [Power On / Off](#power-on--off)
|
||||
- [First Launch](#first-launch)
|
||||
@@ -14,29 +15,47 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
|
||||
- [3.3 Browse Files Screen](#33-browse-files-screen)
|
||||
- [3.4 Recent Books Screen](#34-recent-books-screen)
|
||||
- [3.5 File Transfer Screen](#35-file-transfer-screen)
|
||||
- [3.5.1 Calibre Wireless Transfers](#351-calibre-wireless-transfers)
|
||||
- [3.5.1 Calibre Wireless Transfers](#351-calibre-wireless-transfers)
|
||||
- [Installing the Plugin in Calibre](#installing-the-plugin-in-calibre)
|
||||
- [Configuring the CrossPoint Plugin in Calibre](#configuring-the-crosspoint-plugin-in-calibre)
|
||||
- [Uploading Books](#uploading-books)
|
||||
- [Removing a Book](#removing-a-book)
|
||||
- [3.6 Settings](#36-settings)
|
||||
- [3.6.1 Display](#361-display)
|
||||
- [3.6.2 Reader](#362-reader)
|
||||
- [3.6.3 Controls](#363-controls)
|
||||
- [3.6.4 System](#364-system)
|
||||
- [3.6.5 KOReader Sync Quick Setup](#365-koreader-sync-quick-setup)
|
||||
- [3.6.5 OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries)
|
||||
- [3.6.6 Web Settings (Wi-Fi + OPDS)](#366-web-settings-wi-fi--opds)
|
||||
- [3.6.7 KOReader Sync Quick Setup](#367-koreader-sync-quick-setup)
|
||||
- [Option A: CrossPoint Sync Server (`sync.crosspointreader.com`, default)](#option-a-crosspoint-sync-server-synccrosspointreadercom-default)
|
||||
- [Option B: Legacy Public KOReader Server (`sync.koreader.rocks`)](#option-b-legacy-public-koreader-server-synckoreaderrocks)
|
||||
- [Option C: Self-Hosted Server (Docker Compose)](#option-c-self-hosted-server-docker-compose)
|
||||
- [Syncing While Reading](#syncing-while-reading)
|
||||
- [3.7 Sleep Screen](#37-sleep-screen)
|
||||
- [Cover settings](#cover-settings)
|
||||
- [Custom images](#custom-images)
|
||||
- [3.8 Custom Fonts (SD Card)](#38-custom-fonts-sd-card)
|
||||
- [4. Reading Mode](#4-reading-mode)
|
||||
- [Page Turning](#page-turning)
|
||||
- [Chapter Navigation](#chapter-navigation)
|
||||
- [Auto Page Turn](#auto-page-turn)
|
||||
- [Tilt Page Turn (X3 only)](#tilt-page-turn-x3-only)
|
||||
- [Footnote Navigation](#footnote-navigation)
|
||||
- [System Navigation](#system-navigation)
|
||||
- [Supported Languages](#supported-languages)
|
||||
- [5. Chapter Selection Screen](#5-chapter-selection-screen)
|
||||
- [5. Reader Menu](#5-reader-menu)
|
||||
- [5.1 Chapter Selection](#51-chapter-selection)
|
||||
- [5.2 Bookmarks](#52-bookmarks)
|
||||
- [6. Current Limitations \& Roadmap](#6-current-limitations--roadmap)
|
||||
- [7. Troubleshooting Issues \& Escaping Bootloop](#7-troubleshooting-issues--escaping-bootloop)
|
||||
|
||||
|
||||
## 1. Hardware Overview
|
||||
|
||||
The device utilises the standard buttons on the Xteink X4 (in the same layout as the manufacturer firmware, by default):
|
||||
|
||||
### Button Layout
|
||||
|
||||
| Location | Buttons |
|
||||
| --------------- | ---------------------------------------------------- |
|
||||
| **Bottom Edge** | **Back**, **Confirm**, **Left**, **Right** |
|
||||
@@ -45,6 +64,7 @@ The device utilises the standard buttons on the Xteink X4 (in the same layout as
|
||||
Button layout can be customized in the **[Controls Settings](#363-controls)**.
|
||||
|
||||
### Taking a Screenshot
|
||||
|
||||
When the Power Button and Volume Down button are pressed at the same time, it will take a screenshot and save it in the folder `screenshots/`.
|
||||
|
||||
Alternatively, while reading a book, press the **Confirm** button to open the reader menu and select **Take screenshot**.
|
||||
@@ -81,11 +101,12 @@ See [Reading Mode](#4-reading-mode) below for more information.
|
||||
|
||||
### 3.3 Browse Files Screen
|
||||
|
||||
The Browse Files screen acts as a file and folder browser.
|
||||
The Browse Files screen acts as a file and folder browser. The full path to the current directory is shown at the top of the screen. File extensions are displayed alongside each filename, and directories are shown with brackets (e.g. `[folder-name]`). Hidden directories (those beginning with `.`) are also visible.
|
||||
|
||||
* **Navigate List:** Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to move the selection cursor up and down through folders and books. You can also long-press these buttons to scroll a full page up or down.
|
||||
* **Open Selection:** Press **Confirm** to open a folder or read a selected book.
|
||||
* **Delete Files:** Hold and release **Confirm** to delete the selected file. You will be given an option to either confirm or cancel deletion. Folder deletion is not supported.
|
||||
* **Open Selection:** Press **Confirm** to open a folder or start reading a selected book. Selecting a `.bmp` file will open the image viewer.
|
||||
* **Delete Files or Folders:** Hold and release **Confirm** to delete the selected file or folder. You will be given an option to either confirm or cancel. Multiple files can be selected for deletion in a single operation.
|
||||
* **Rename or Move:** Files can be renamed or moved to a different folder from within the browse screen.
|
||||
|
||||
### 3.4 Recent Books Screen
|
||||
|
||||
@@ -93,24 +114,62 @@ The Recent Books screen lists the most recently opened books in a chronological
|
||||
|
||||
### 3.5 File Transfer Screen
|
||||
|
||||
The File Transfer screen allows you to upload new e-books to the device. When you enter the screen, you'll be prompted with a WiFi selection dialog and then your X4 will start hosting a web server.
|
||||
The File Transfer screen allows you to upload and manage files on the device. When you enter the screen, choose **Join a Network**, **Calibre Wireless**, or **Create Hotspot**. The reader then starts the web server for the selected mode.
|
||||
|
||||
See the [webserver docs](./docs/webserver.md) for more information on how to connect to the web server and upload files.
|
||||
See the [web server docs](./docs/webserver.md) for more information on how to connect to the web server and upload files.
|
||||
|
||||
The web interface also supports **WebDAV**, allowing you to mount the device as a network drive and manage files directly from your computer's file manager.
|
||||
|
||||
Download links for files already on the device are available in the web interface, so you can retrieve books or screenshots over Wi-Fi without connecting a cable.
|
||||
|
||||
A **Wi-Fi signal strength indicator** (dBm) is displayed on-screen during joined-network web server sessions.
|
||||
|
||||
> [!TIP]
|
||||
> Advanced users can also manage files programmatically or via the command line using `curl`. See the [webserver docs](./docs/webserver.md) for details.
|
||||
> Advanced users can also manage files programmatically or via the command line using `curl`. See the [web server docs](./docs/webserver.md) for details.
|
||||
> [!TIP]
|
||||
> If your EPUBs have compatibility issues, you can run the built-in **EPUB Optimizer** directly from the device to clean up and reprocess books for better rendering.
|
||||
|
||||
### 3.5.1 Calibre Wireless Transfers
|
||||
|
||||
CrossPoint supports sending books from Calibre using the CrossPoint Reader device plugin.
|
||||
|
||||
1. Install the plugin in Calibre:
|
||||
- Head to https://github.com/crosspoint-reader/calibre-plugins/releases to download the latest version of the crosspoint_reader plugin.
|
||||
- Download the zip file.
|
||||
- Open Calibre → Preferences → Plugins → Load plugin from file → Select the zip file.
|
||||
2. On the device: File Transfer → Connect to Calibre → Join a network.
|
||||
3. Make sure your computer is on the same WiFi network.
|
||||
4. In Calibre, click "Send to device" to transfer books.
|
||||
#### Installing the Plugin in Calibre
|
||||
|
||||
If you don't already have the plugin installed:
|
||||
|
||||
1. Head to https://github.com/crosspoint-reader/calibre-plugins/releases to download the latest version of the crosspoint_reader plugin.
|
||||
2. Download the zip file.
|
||||
3. Open Calibre → Preferences → Plugins → Load plugin from file → Select the zip file.
|
||||
4. Restart Calibre.
|
||||
|
||||
#### Configuring the CrossPoint Plugin in Calibre
|
||||
1. In Calibre select Preferences.
|
||||
2. In the Preferences dialog select Plugins.
|
||||
3. In Plugins search for "crosspoint".
|
||||
4. Click on "Customize plugin".
|
||||
5. Update the value for "Host" to match the IP for your device.
|
||||
6. Leave the other settings as they are.
|
||||
7. [optional] Modify the "Upload path" to point to a subfolder other than the root "/" folder. Enter this as a path relative to the root folder. Example: `/mybooks`
|
||||
8. Restart Calibre.
|
||||
|
||||
<img width="420" height="385" alt="Image" src="https://github.com/user-attachments/assets/01fc7e33-a9a7-48ba-9e26-2e68d1f9daec" />
|
||||
|
||||
#### Uploading Books
|
||||
|
||||
To upload a book using the CrossPoint plugin in Calibre:
|
||||
|
||||
1. On the device: File Transfer -> Calibre Wireless, then join a network.
|
||||
2. Select one or more books.
|
||||
3. Right-click on that selection.
|
||||
4. Select "Send to Device" > "Send to main memory"
|
||||
|
||||
The CrossPoint plugin will connect to your device, create a folder for the book's author in the root folder (or the folder you configured for the plugin), then copy the book into that folder.
|
||||
|
||||
<img width="783" height="310" alt="Image" src="https://github.com/user-attachments/assets/741b0909-2e1d-4f16-8af0-2c43fbda5ce6" />
|
||||
|
||||
#### Removing a Book
|
||||
|
||||
Books cannot be removed from your device through Calibre. Use the web interface instead.
|
||||
|
||||
### 3.6 Settings
|
||||
|
||||
@@ -119,94 +178,224 @@ The Settings screen allows you to configure the device's behavior. There are a f
|
||||
#### 3.6.1 Display
|
||||
|
||||
- **Sleep Screen**: Which sleep screen to display when the device sleeps:
|
||||
|
||||
- "Dark" (default) - The default dark Crosspoint logo sleep screen
|
||||
- "Light" - The same default sleep screen, on a white background
|
||||
- "Custom" - Custom images from the SD card; see [Sleep Screen](#37-sleep-screen) below for more information
|
||||
- "Cover" - The book cover image (Note: this is experimental and may not work as expected)
|
||||
- "None" - A blank screen
|
||||
- "Cover + Custom" - The book cover image, falls back to "Custom" behavior
|
||||
- "Cover + Custom" - The book cover image while actively reading, falls back to "Custom" behavior otherwise
|
||||
- "Quick resume" - The text of the last page read will be displayed on the sleep screen and a moon icon is shown on the edge of the screen. Waking up the device will return to the same page of the opened book. This is useful for quickly resuming reading without waiting for the device to fully wake up and load the book.
|
||||
|
||||
- **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected:
|
||||
|
||||
- "Fit" (default) - Scale the image down to fit centered on the screen, padding with white borders as necessary
|
||||
- "Crop" - Scale the image down and crop as necessary to try to fill the screen (Note: this is experimental and may not work as expected)
|
||||
|
||||
- **Sleep Screen Cover Filter**: What filter will be applied to the book cover when "Cover" sleep screen is selected:
|
||||
|
||||
- "None" (default) - The cover image will be converted to a grayscale image and displayed as it is
|
||||
- "Contrast" - The image will be displayed as a black & white image without grayscale conversion
|
||||
- "Inverted" - The image will be inverted as in white & black and will be displayed without grayscale conversion
|
||||
|
||||
- **Quick Resume on Timeout**: Whether to enable the "Quick Resume" sleep screen when the device goes to sleep due to inactivity (System > Time to Sleep). This is useful for quickly resuming reading without waiting for the device to fully wake up and load the book. This overwrites the Sleep Screen Cover Mode when enabled.
|
||||
|
||||
- **Status Bar**: Configure the status bar displayed while reading:
|
||||
|
||||
- "None" - No status bar
|
||||
- "No Progress" - Show status bar without reading progress
|
||||
- "Full w/ Percentage" - Show status bar with book progress (as percentage)
|
||||
- "Full w/ Book Bar" - Show status bar with book progress (as bar)
|
||||
- "Book Bar Only" - Show book progress (as bar)
|
||||
- "Full w/ Chapter Bar" - Show status bar with chapter progress (as bar)
|
||||
|
||||
- **Hide Battery %**: Configure where to suppress the battery percentage display in the status bar; the battery icon will still be shown:
|
||||
|
||||
- "Never" (default) - Always show battery percentage
|
||||
- "In Reader" - Show battery percentage everywhere except in reading mode
|
||||
- "Always" - Always hide battery percentage
|
||||
|
||||
- **Refresh Frequency**: Set how often the screen does a full refresh while reading to reduce ghosting; options are every 1, 5, 10, 15, or 30 pages.
|
||||
|
||||
- **UI Theme**: Set which UI theme to use:
|
||||
|
||||
- "Classic" - The original Crosspoint theme
|
||||
- "Lyra" - The new theme for Crosspoint featuring rounded elements and menu icons
|
||||
- "Lyra Extended" - Lyra, but displays 3 books instead of 1 on the **[Home Screen](#31-home-screen)**
|
||||
- "RoundedRaff" - A rounded theme with additional visual styling
|
||||
|
||||
- **Sunlight Fading Fix**: Configure whether to enable a software-fix for the issue where white X4 models may fade when used in direct sunlight:
|
||||
|
||||
- "OFF" (default) - Disable the fix
|
||||
- "ON" - Enable the fix
|
||||
|
||||
> [!NOTE]
|
||||
> A battery charging indicator is shown on the battery icon whenever the device is actively charging.
|
||||
|
||||
#### 3.6.2 Reader
|
||||
|
||||
- **Reader Font Family**: Choose the font used for reading:
|
||||
- "Bookerly" (default) - Amazon's reading font
|
||||
|
||||
- "Noto Serif" (default) - Google's serif font
|
||||
- "Noto Sans" - Google's sans-serif font
|
||||
- "Open Dyslexic" - Font designed for readers with dyslexia
|
||||
|
||||
- **Reader Font Size**: Adjust the text size for reading; options are "Small", "Medium" (default), "Large", or "X Large".
|
||||
|
||||
- **Reader Line Spacing**: Adjust the spacing between lines; options are "Tight", "Normal" (default), or "Wide".
|
||||
|
||||
- **Reader Screen Margin**: Controls the screen margins in Reading Mode between 5 and 40 pixels in 5-pixel increments.
|
||||
|
||||
- **Reader Paragraph Alignment**: Set the alignment of paragraphs; options are "Justified" (default), "Left", "Center", or "Right".
|
||||
|
||||
- **Embedded Style**: Whether to use the EPUB file's embedded HTML and CSS stylisation and formatting; options are "ON" or "OFF".
|
||||
|
||||
- **Hyphenation**: Whether to hyphenate text in Reading Mode; options are "ON" or "OFF".
|
||||
|
||||
- **Reading Orientation**: Set the screen orientation for reading EPUB files:
|
||||
|
||||
- "Portrait" (default) - Standard portrait orientation
|
||||
- "Landscape CW" - Landscape, rotated clockwise
|
||||
- "Inverted" - Portrait, upside down
|
||||
- "Landscape CCW" - Landscape, rotated counter-clockwise
|
||||
|
||||
- **Extra Paragraph Spacing**: Set how to handle paragraph breaks:
|
||||
|
||||
- "ON" - Vertical space will be added between paragraphs in Reading Mode
|
||||
- "OFF" - Paragraphs will not have vertical space added, but will have first-line indentation
|
||||
|
||||
- **Dictionary**: Select the StarDict dictionary used for word lookups while reading, or "None" to disable lookups. *(Only shown when at least one dictionary folder exists under `/dictionaries/` on the SD card — see [docs/dictionary.md](docs/dictionary.md) for setup and usage.)*
|
||||
|
||||
- **Text Anti-Aliasing**: Whether to show smooth grey edges (anti-aliasing) on text in reading mode. Note this slows down page turns slightly.
|
||||
|
||||
- **Images**: Whether to display embedded images (JPG/PNG) found in EPUB files; options are "ON" (default) or "OFF".
|
||||
|
||||
- **Focus Reading**: Bolds the first part of each word to create visual fixation points, similar to Bionic Reading. This can help improve reading speed and focus; options are "ON" or "OFF" (default).
|
||||
|
||||
#### 3.6.3 Controls
|
||||
|
||||
- **Remap Front Buttons**: A menu for customising the function of each bottom edge button.
|
||||
- **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from "Prev/Next" (default) to "Next/Prev". This change is only in effect when reading.
|
||||
|
||||
- **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from "Prev/Next" (default) to "Next/Prev". You can also disable them entirely. This change is only in effect when reading.
|
||||
|
||||
- **Long-press Chapter Skip**: Set whether long-pressing page turn buttons skips to the next/previous chapter:
|
||||
|
||||
- "Chapter Skip" (default) - Long-pressing skips to next/previous chapter
|
||||
- "Page Scroll" - Long-pressing scrolls a page up/down
|
||||
- **Long-press Menu**: Selects the function bound to holding the menu button (Confirm) while reading an EPUB. **Cycles through the available functions** each time the setting is selected — additional functions may be added in future releases, so this is not a binary on/off toggle. A short press of Confirm always opens the reader menu as normal:
|
||||
- "Bookmark" (default) - Hold Confirm (~0.4 second) to drop a bookmark at the current page.
|
||||
- "KOSync" - Hold Confirm (~1 second) to launch KOReader sync directly.
|
||||
- "Dictionary" - Hold Confirm (~0.4 second) to start dictionary word selection on the current page (see [docs/dictionary.md](docs/dictionary.md)).
|
||||
- "Disabled" - Long-press is ignored; only short-press opens the reader menu.
|
||||
|
||||
- **Short Power Button Click**: Controls the effect of a short click of the power button:
|
||||
|
||||
- "Ignore" (default) - Require a long press to turn off the device
|
||||
- "Sleep" - A short press puts the device into sleep mode
|
||||
- "Page Turn" - A short press in reading mode turns to the next page; a long press turns the device off
|
||||
- "Footnotes" - A short press in reading mode opens the footnotes submenu; if only one footnote is present on the page, the referenced page is opened directly. The short press on the power button can be used to select the footnote in the submenu, and to go back to the original page after finish reading the footnote (like the back button).
|
||||
- "Refresh" - A short press triggers a manual full-screen refresh, useful for clearing ghosting
|
||||
- **Quick-return from footnotes**: Toggles on and off the quick return functionality from the footnotes. When the functionality it's active, a short press of the power button will act as the back button from the footnotes page.
|
||||
|
||||
#### 3.6.4 System
|
||||
|
||||
- **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep; options are 1, 5, 10 (default), 15 or 30 minutes.
|
||||
- **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep; options are 1, 3, 5, 10 (default), 15 or 30 minutes.
|
||||
|
||||
- **Wi-Fi Networks**: Connect to Wi-Fi networks for file transfers and firmware updates.
|
||||
|
||||
- **KOReader Sync**: Options for setting up KOReader for syncing book progress. **Smart sync** is the default for new configurations and auto-resolves simple push/pull decisions. Existing credential files retain **Ask every time** when migrated; you can switch Sync Behavior at any time if you prefer manual confirmation.
|
||||
|
||||
- **OPDS Servers**: Manage one or more OPDS [(Open Publication Distribution System)](https://en.wikipedia.org/wiki/Open_Publication_Distribution_System) libraries for browsing and downloading books. See [OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries) below.
|
||||
|
||||
- **WiFi Networks**: Connect to WiFi networks for file transfers and firmware updates.
|
||||
- **KOReader Sync**: Options for setting up KOReader for syncing book progress.
|
||||
- **OPDS Browser**: Configure OPDS server settings for browsing and downloading books. Set the server URL (for Calibre Content Server, add `/opds` to the end), and optionally configure username and password for servers requiring authentication. Note: Only HTTP Basic authentication is supported. If using Calibre Content Server with authentication enabled, you must set it to use Basic authentication instead of the default Digest authentication.
|
||||
- **Clear Reading Cache**: Clear the internal SD card cache.
|
||||
- **Check for updates**: Check for Crosspoint firmware updates over WiFi.
|
||||
- **Language**: Set the system language (see **[Supported Languages](#supported-languages)** for more information).
|
||||
|
||||
#### 3.6.5 KOReader Sync Quick Setup
|
||||
- **Check for updates**: Check for Crosspoint firmware updates over Wi-Fi. Firmware can also be updated without a USB connection by placing a `firmware.bin` file on the SD card.
|
||||
|
||||
- **Language**: Set the UI language. CrossPoint supports 24 languages: English, Spanish, French, German, Czech, Brazilian Portuguese, Russian, Swedish, Romanian, Catalan, Ukrainian, Belarusian, Italian, Polish, Finnish, Danish, Dutch, Turkish, Kazakh, Hungarian, Lithuanian, Slovenian, Valencian, and Hebrew.
|
||||
|
||||
- **Manage Fonts**: Browse, download, and manage custom font families installed from the SD card. See [Custom Fonts (SD Card)](#38-custom-fonts-sd-card) for more information.
|
||||
|
||||
#### 3.6.5 OPDS Servers (Multiple Libraries)
|
||||
|
||||
CrossPoint supports saving multiple OPDS servers and switching between them when browsing catalogs.
|
||||
|
||||
1. Open **Settings -> System -> OPDS Servers**.
|
||||
|
||||
2. Select **Add Server** to create a new entry, or select an existing server to edit it.
|
||||
|
||||
3. Configure these fields:
|
||||
|
||||
- **Server Name**: Optional display name (for example, "Home Calibre" or "Public Catalog").
|
||||
|
||||
- **OPDS Server URL**: Full catalog root URL (for Calibre Content Server, usually ends with `/opds`).
|
||||
|
||||
- **Username / Password**: Optional credentials for authenticated servers.
|
||||
|
||||
4. Use **Delete Server** inside a server entry to remove it.
|
||||
|
||||
Behavior notes:
|
||||
|
||||
- You can store up to 8 OPDS servers.
|
||||
- OPDS authentication supports HTTP Basic auth. If you use Calibre Content Server with authentication enabled, set it to Basic (not Digest).
|
||||
|
||||
You can also manage OPDS servers from the web interface while in File Transfer mode:
|
||||
|
||||
1. Connect to the device web UI.
|
||||
2. Open `http://<device-ip>/settings`.
|
||||
3. Use the **OPDS Servers** card to add, edit, or delete entries.
|
||||
|
||||
For web-based Wi-Fi network management, see [Web Settings (Wi-Fi + OPDS)](#366-web-settings-wi-fi--opds).
|
||||
|
||||
#### 3.6.6 Web Settings (Wi-Fi + OPDS)
|
||||
|
||||
While in **File Transfer** mode, the web settings page includes management cards for both **Wi-Fi Networks** and **OPDS Servers**.
|
||||
|
||||
1. On device: open **File Transfer** and connect through **Join a Network** or **Create Hotspot**.
|
||||
2. In a browser, open `http://<device-ip>/settings` or `http://crosspoint.local`.
|
||||
3. In **Wi-Fi Networks**, add, edit, or delete saved network entries (SSID + optional password).
|
||||
4. In **OPDS Servers**, add, edit, or delete OPDS catalogs.
|
||||
|
||||
Behavior notes:
|
||||
|
||||
- Passwords are never shown back in the web UI after saving.
|
||||
- Leaving Password blank while editing keeps the existing saved password unchanged.
|
||||
- The web UI can save hidden-network SSIDs, but connecting to hidden networks still depends on the device-side Wi-Fi connection flow.
|
||||
|
||||
#### 3.6.7 KOReader Sync Quick Setup
|
||||
|
||||
CrossPoint can sync reading progress with KOReader-compatible sync servers.
|
||||
It also interoperates with KOReader apps/devices when they use the same server and credentials.
|
||||
|
||||
##### Option A: Free Public Server (`sync.koreader.rocks`)
|
||||
##### Option A: CrossPoint Sync Server (`sync.crosspointreader.com`, default)
|
||||
|
||||
1. Register a user once (only if needed):
|
||||
When **Sync Server URL** is left empty, CrossPoint uses the free CrossPoint sync server at `https://sync.crosspointreader.com`. It speaks the standard KOReader sync protocol (so KOReader apps can use it too) and additionally stores an exact spine/page position for lossless CrossPoint-to-CrossPoint sync.
|
||||
|
||||
1. On each CrossPoint device:
|
||||
|
||||
- Go to **Settings -> System -> KOReader Sync**.
|
||||
|
||||
- Set **Username** and **Password** (enter the plain password; CrossPoint computes MD5 internally, and use the same values on all devices).
|
||||
|
||||
- Leave **Sync Server URL** empty (or set it to `https://sync.crosspointreader.com`).
|
||||
|
||||
- On the first device, run **Sign Up** once to create the account directly from the device. On every other device, just run **Authenticate**.
|
||||
|
||||
Accounts are per server. Existing `sync.koreader.rocks` credentials do not exist on the CrossPoint server; either sign up again with the same username/password or use Option B to keep using the legacy server.
|
||||
|
||||
##### Option B: Legacy Public KOReader Server (`sync.koreader.rocks`)
|
||||
|
||||
Use this if you already sync KOReader devices against the official public server.
|
||||
|
||||
1. On each CrossPoint device:
|
||||
|
||||
- Go to **Settings -> System -> KOReader Sync**.
|
||||
|
||||
- Set **Sync Server URL** to `https://sync.koreader.rocks` (required; an empty URL now points at the CrossPoint server instead).
|
||||
|
||||
- Set **Username** and **Password** to your existing KOReader Sync credentials.
|
||||
|
||||
- Run **Authenticate**.
|
||||
|
||||
2. If you do not have an account yet, run **Sign Up** on the device, or register once with curl:
|
||||
|
||||
```bash
|
||||
USERNAME="user"
|
||||
@@ -219,21 +408,9 @@ curl -i "https://sync.koreader.rocks/users/create" \
|
||||
--data "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD_MD5\"}"
|
||||
```
|
||||
|
||||
Already have KOReader Sync credentials? Skip registration; basic sync only requires using the same existing username/password on all devices.
|
||||
|
||||
When this returns `HTTP 402` with `{"code":2002,"message":"Username is already registered."}`, pick a different username or use that existing account.
|
||||
|
||||
2. On each CrossPoint device:
|
||||
- Go to **Settings -> System -> KOReader Sync**.
|
||||
- Set **Username** and **Password** (enter the plain password; CrossPoint computes MD5 internally, and use the same values on all devices).
|
||||
- Set **Sync Server URL** to `https://sync.koreader.rocks`, or leave it empty (both use the same default KOReader sync server).
|
||||
- Run **Authenticate**.
|
||||
|
||||
3. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**.
|
||||
- Choose **Apply Remote** to jump to remote progress.
|
||||
- Choose **Upload Local** to push current progress.
|
||||
|
||||
##### Option B: Self-Hosted Server (Docker Compose)
|
||||
##### Option C: Self-Hosted Server (Docker Compose)
|
||||
|
||||
1. Start a sync server:
|
||||
|
||||
@@ -273,7 +450,7 @@ curl -H "Accept: application/vnd.koreader.v1+json" "http://<server-ip>:17200/hea
|
||||
```
|
||||
|
||||
3. Register a user once.
|
||||
CrossPoint authenticates against KOReader Sync (`koreader/kosync`) using an MD5 key, so register using the MD5 of your password:
|
||||
CrossPoint authenticates against KOReader Sync (`koreader/kosync`) using an MD5 key, so register using the MD5 of your password:
|
||||
|
||||
> [!WARNING]
|
||||
> Sending a reusable MD5-derived password over plain HTTP is insecure.
|
||||
@@ -295,29 +472,36 @@ curl -i "http://<server-ip>:17200/users/create" \
|
||||
If this returns `HTTP 402` with `{"code":2002,"message":"Username is already registered."}`, the account already exists.
|
||||
|
||||
4. On each CrossPoint device:
|
||||
|
||||
- Go to **Settings -> System -> KOReader Sync**.
|
||||
|
||||
- Set **Username** and **Password** (enter the plain password; CrossPoint computes MD5 internally, and use the same values on all devices).
|
||||
|
||||
- Set **Sync Server URL** to `http://<server-ip>:17200`.
|
||||
|
||||
- Run **Authenticate**.
|
||||
|
||||
If you use the HTTPS listener, use `https://<server-ip>:7200` (`curl -k` only for self-signed certificate testing).
|
||||
|
||||
5. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**.
|
||||
- Choose **Apply Remote** to jump to remote progress.
|
||||
- Choose **Upload Local** to push current progress.
|
||||
##### Syncing While Reading
|
||||
|
||||
Once any of the options above is set up, press **Confirm** while reading to open the reader menu, then select **Sync Progress**. Alternatively, set **Settings -> Controls -> Long-press Menu** to **KOSync** and hold Confirm to launch sync directly.
|
||||
|
||||
- With **Sync Behavior** set to **Ask every time**, choose **Apply Remote** to jump to remote progress or **Upload Local** to push current progress.
|
||||
- With **Sync Behavior** set to **Smart sync**, CrossPoint auto-resolves simple cases: upload when no remote progress exists, confirm and leave both unchanged when local and remote progress are already synchronized, upload when local progress is further ahead, or apply remote when remote progress is further ahead.
|
||||
|
||||
### 3.7 Sleep Screen
|
||||
|
||||
The **Sleep Screen** setting controls what is displayed when the device goes to sleep:
|
||||
|
||||
| Mode | Behavior |
|
||||
|------|----------|
|
||||
| **Dark** (default) | The CrossPoint logo on a dark background. |
|
||||
| **Light** | The CrossPoint logo on a white background. |
|
||||
| **Custom** | A custom image from the SD card (see below). Falls back to **Dark** if no custom image is found. |
|
||||
| **Cover** | The cover of the currently open book. Falls back to **Dark** if no book is open. |
|
||||
| **Cover + Custom** | The cover of the currently open book. Falls back to **Custom** behavior if no book is open. |
|
||||
| **None** | A blank screen. |
|
||||
| Mode | Behavior |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Dark** (default) | The CrossPoint logo on a dark background. |
|
||||
| **Light** | The CrossPoint logo on a white background. |
|
||||
| **Custom** | A custom image from the SD card (see below). Falls back to **Dark** if no custom image is found. |
|
||||
| **Cover** | The cover of the currently open book. Falls back to **Dark** if no book is open. |
|
||||
| **Cover + Custom** | The cover of the currently open book, shown only while actively reading. Falls back to **Custom** behavior when not reading. |
|
||||
| **None** | A blank screen. |
|
||||
|
||||
#### Cover settings
|
||||
|
||||
@@ -335,8 +519,29 @@ To use custom sleep images, set the sleep screen mode to **Custom** or **Cover +
|
||||
|
||||
> [!TIP]
|
||||
> For best results:
|
||||
>
|
||||
> - Use uncompressed BMP files with 24-bit color depth
|
||||
> - Use a resolution of 480x800 pixels to match the device's screen resolution.
|
||||
> - X4: Use a resolution of 480x800 pixels to match the device's screen resolution.
|
||||
> - X3: Use a resolution of 528x792 pixels to match the device's screen resolution.
|
||||
|
||||
> [!TIP]
|
||||
> You can set an image as the sleep screen cover directly from the BMP image viewer in the **[Browse Files](#33-browse-files-screen)** screen.
|
||||
|
||||
---
|
||||
|
||||
### 3.8 Custom Fonts (SD Card)
|
||||
|
||||
CrossPoint supports loading additional fonts from the SD card, extending beyond the two built-in families (Noto Serif, Noto Sans). Custom fonts can include extended Unicode coverage, enabling CJK (Chinese, Japanese, Korean) and other scripts.
|
||||
|
||||
There are three ways to install fonts:
|
||||
|
||||
1. **Download from device (recommended):** Go to **Settings -> System -> Manage Fonts**, browse the available font families, and select one to download over Wi-Fi.
|
||||
2. **Upload via web interface:** While in **File Transfer** mode, open the web UI in a browser and navigate to the **Fonts** tab to upload `.cpfont` files.
|
||||
3. **Manual SD card copy:** Download font files from the [crosspoint-fonts repository](https://github.com/crosspoint-reader/crosspoint-fonts) and copy them to `/.fonts/` (preferred) or `/fonts/` on your SD card.
|
||||
|
||||
Once installed, custom fonts appear in **Settings → Reader → Font Family** alongside the built-in fonts.
|
||||
|
||||
See [docs/sd-card-fonts.md](./docs/sd-card-fonts.md) for full installation details and SD card folder structure.
|
||||
|
||||
---
|
||||
|
||||
@@ -345,6 +550,7 @@ To use custom sleep images, set the sleep screen mode to **Custom** or **Cover +
|
||||
Once you have opened a book, the button layout changes to facilitate reading.
|
||||
|
||||
### Page Turning
|
||||
|
||||
| Action | Buttons |
|
||||
| ----------------- | ------------------------------------ |
|
||||
| **Previous Page** | Press **Left** _or_ **Volume Up** |
|
||||
@@ -355,55 +561,143 @@ The role of the volume (side) buttons can be swapped in the **[Controls Settings
|
||||
If the **Short Power Button Click** setting is set to "Page Turn", you can also turn to the next page by briefly pressing the Power button.
|
||||
|
||||
### Chapter Navigation
|
||||
|
||||
* **Next Chapter:** Press and **hold** the **Right** (or **Volume Down**) button briefly, then release.
|
||||
* **Previous Chapter:** Press and **hold** the **Left** (or **Volume Up**) button briefly, then release.
|
||||
|
||||
This feature can be disabled in the **[Controls Settings](#363-controls)** to help avoid changing chapters by mistake.
|
||||
|
||||
### Auto Page Turn
|
||||
|
||||
Auto Page Turn automatically advances pages at a set interval, useful for hands-free reading. This feature can be enabled and configured from the **[Reader Menu](#5-reader-menu)** while reading an EPUB.
|
||||
|
||||
### Tilt Page Turn (X3 only)
|
||||
|
||||
On the **Xteink X3**, the gyroscope can be used to turn pages by tilting the device. This feature is available in the Controls settings.
|
||||
|
||||
### Footnote Navigation
|
||||
|
||||
When reading an EPUB that contains footnotes, you can navigate to the footnote text by selecting the footnote reference in the book. From the footnote, you can return to your original reading position.
|
||||
|
||||
If the device goes to sleep or you close the book while viewing a footnote, the book reopens to your original reading position, not the footnote.
|
||||
|
||||
### System Navigation
|
||||
|
||||
* **Return to Home:** Press the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.
|
||||
* **Return to Browse Files:** Press and hold the **Back** button to close the book and return to the **[Browse Files](#33-browse-files-screen)** screen.
|
||||
* **Chapter Menu:** Press **Confirm** to open the **[Table of Contents/Chapter Selection](#5-chapter-selection-screen)** screen.
|
||||
* **Reader Menu:** Press **Confirm** to open the **[Reader Menu](#5-reader-menu)**, which includes chapter navigation, reading options, and more.
|
||||
* **Long-press Confirm (configurable):** Holding **Confirm** runs the function chosen by the **Long-press Menu** setting in **[Controls Settings](#363-controls)** — "Bookmark" (default) drops a bookmark, "KOSync" launches KOReader Sync, "Dictionary" starts a word lookup, "Disabled" does nothing. A short press always opens the Reader Menu.
|
||||
|
||||
### Supported Languages
|
||||
|
||||
CrossPoint renders text using the following Unicode character blocks, enabling support for a wide range of languages:
|
||||
|
||||
* **Latin Script (Basic, Supplement, Extended-A):** Covers English, German, French, Spanish, Portuguese, Italian, Dutch, Swedish, Norwegian, Danish, Finnish, Polish, Czech, Hungarian, Romanian, Slovak, Slovenian, Turkish, and others.
|
||||
* **Cyrillic Script (Standard and Extended):** Covers Russian, Ukrainian, Belarusian, Bulgarian, Serbian, Macedonian, Kazakh, Kyrgyz, Mongolian, and others.
|
||||
* **Latin Script (Basic, Supplement, Extended-A/B):** Covers English, German, French, Spanish, Portuguese, Italian, Dutch, Swedish, Norwegian, Danish, Finnish, Polish, Czech, Hungarian, Romanian, Slovak, Slovenian, Turkish, Catalan, and others.
|
||||
* **Cyrillic Script (Standard and Extended):** Covers Russian, Ukrainian, Belarusian, Bulgarian, Serbian, Macedonian, Kazakh, Kyrgyz, Mongolian, and others.
|
||||
* **Vietnamese:** Supported via extended Latin glyph coverage in the built-in reader fonts.
|
||||
|
||||
What is not supported: Chinese, Japanese, Korean, Vietnamese, Hebrew, Arabic, Greek and Farsi.
|
||||
What is not supported with built-in reader fonts: Chinese, Japanese, Korean, Arabic, Greek, Hebrew, and Farsi. However, **CJK, Hebrew, Greek, and other extended scripts can be enabled by installing custom SD card fonts** — see [Custom Fonts (SD Card)](#38-custom-fonts-sd-card).
|
||||
|
||||
---
|
||||
|
||||
## 5. Chapter Selection Screen
|
||||
## 5. Reader Menu
|
||||
|
||||
Accessible by pressing **Confirm** while inside a book.
|
||||
Press **Confirm** while reading to open the Reader Menu. From here you can access reading utilities and navigation options without leaving the book.
|
||||
|
||||
1. Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to highlight the desired chapter.
|
||||
2. Press **Confirm** to jump to that chapter.
|
||||
3. *Alternatively, press **Back** to cancel and return to your current page.*
|
||||
Available options include:
|
||||
|
||||
- **Select Chapter** – Open the table of contents to jump to a specific chapter (see [Chapter Selection](#51-chapter-selection) below).
|
||||
- **Footnotes** – Navigate to the footnotes for the current section *(only shown in books that contain footnotes)*.
|
||||
- **Look Up** – Select a word on the current page and show its dictionary definition (see [docs/dictionary.md](docs/dictionary.md)). Requires a dictionary to be selected in **Settings → Reader → Dictionary**.
|
||||
- **Reading Orientation** – Cycle through screen orientations without leaving the reader.
|
||||
- **Auto Turn (Pages Per Minute)** – Cycle through automatic page turn speed options for hands-free reading.
|
||||
- **Go to %** – Jump to a specific position in the book by percentage.
|
||||
- **Take screenshot** – Save a screenshot of the current page to the `screenshots/` folder.
|
||||
- **Show page as QR** – Display a QR code encoding the current reading position.
|
||||
- **Go Home** – Close the book and return to the Home screen.
|
||||
- **Sync Progress** – Push or pull reading progress with a KOReader sync server (see [KOReader Sync Quick Setup](#367-koreader-sync-quick-setup)).
|
||||
- **Delete Book Cache** – Clear the cached layout data for the current book, forcing a re-index on next open.
|
||||
|
||||
Press **Back** at any time to close the menu and return to your current page.
|
||||
|
||||
### 5.1 Chapter Selection
|
||||
|
||||
Accessible by selecting **Chapters** from the Reader Menu.
|
||||
|
||||
1. Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to highlight the desired chapter.
|
||||
2. Press **Confirm** to jump to that chapter.
|
||||
3. *Alternatively, press **Back** to cancel and return to your current page.*
|
||||
|
||||
---
|
||||
|
||||
### 5.2 Bookmarks
|
||||
|
||||
Bookmarks can be created to quickly save and restore your place in a book.
|
||||
|
||||
To create a bookmark, hold **Confirm** for about half a second while inside a book. A popup will appear letting you know a bookmark was created. The popup message will automatically disappear in a couple of seconds.
|
||||
|
||||
To open bookmarks, press **Confirm** while inside a book. Then navigate to the **Bookmarks** menu. Bookmarks can be opened by navigating to them and pressing **Confirm**, which will redirect you to that place in the book. You can delete bookmarks by holding **Confirm** for about 0.7 seconds, and then pressing **Confirm** again to confirm deletion, or **Back** to cancel.
|
||||
|
||||
Bookmarks are stored in the `.crosspoint/bookmarks` folder in the JSON format.
|
||||
|
||||
## 6. Current Limitations & Roadmap
|
||||
|
||||
Please note that this firmware is currently in active development. The following features are **not yet supported** but are planned for future updates:
|
||||
|
||||
* **Images:** Embedded images in e-books will not render.
|
||||
* **Cover Images:** Large cover images embedded into EPUB require several seconds (~10s for ~2000 pixel tall image) to convert for sleep screen and home screen thumbnail. Consider optimizing the EPUB with e.g. https://github.com/bigbag/epub-to-xtc-converter to speed this up.
|
||||
* **Unsupported Image Formats:** Most JPG and PNG images in EPUBs render correctly. GIFs and progressive JPEGs are not supported and will fall back to an `[Image]` placeholder.
|
||||
*
|
||||
* **Dictionary Lookup:** Inline word lookup is not yet implemented.
|
||||
|
||||
---
|
||||
|
||||
## 7. Troubleshooting Issues & Escaping Bootloop
|
||||
|
||||
If an issue or crash is encountered while using Crosspoint, feel free to raise an issue ticket and attach the serial monitor logs. The logs can be obtained by connecting the device to a computer and starting a serial monitor. Either [Serial Monitor](https://www.serialmonitor.org/) or the following command can be used:
|
||||
If an issue or crash is encountered while using Crosspoint, feel free to raise an issue ticket and attach the logs.
|
||||
|
||||
**Crash reports on SD card:** After a crash, CrossPoint automatically saves a crash report to the SD card (no USB connection needed). Check the root of the SD card for a crash log file and include it with any bug report.
|
||||
|
||||
**Serial monitor logs:** For more detailed debugging, connect the device to a computer and run the custom debugging monitor script (requires Python 3 with `pyserial`, `colorama`, and `matplotlib`; install via `pip3 install pyserial colorama matplotlib`):
|
||||
|
||||
```
|
||||
pio device monitor
|
||||
python3 scripts/debugging_monitor.py
|
||||
```
|
||||
|
||||
The script auto-detects the serial port. You can also specify one explicitly:
|
||||
|
||||
```
|
||||
python3 scripts/debugging_monitor.py /dev/ttyACM0 # Linux
|
||||
python3 scripts/debugging_monitor.py /dev/tty.usbmodem1 # macOS
|
||||
python3 scripts/debugging_monitor.py COM7 # Windows
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- Color-coded log output by category (errors, memory, display, EPUB parsing, etc.)
|
||||
- Live memory usage graph (free RAM, total RAM, max contiguous allocation) updated every second
|
||||
- Interactive command prompt — type a command and press Enter to send it to the device
|
||||
- Screenshot capture — saves the current display to `screenshot.bmp` when triggered by the device
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
| -------------------- | --------------------------------------------------------- |
|
||||
| `--baud RATE` | Baud rate (default: 115200) |
|
||||
| `--filter KEYWORD` | Show only lines containing the keyword (case-insensitive) |
|
||||
| `--suppress KEYWORD` | Hide lines containing the keyword (case-insensitive) |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```
|
||||
# Show only memory-related log lines
|
||||
python3 scripts/debugging_monitor.py --filter MEM
|
||||
|
||||
# Hide noisy SD card log lines
|
||||
python3 scripts/debugging_monitor.py --suppress "[SD]"
|
||||
```
|
||||
|
||||
Press **Ctrl-C** or close the graph window to exit.
|
||||
|
||||
If the device is stuck in a bootloop, press and release the Reset button. Then, press and hold on to the configured Back button and the Power Button to boot to the Home Screen.
|
||||
|
||||
There can be issues with broken cache or config. In this case, delete the `.crosspoint` directory on your SD card (or consider deleting only `settings.bin`, `state.bin`, or `epub_*` cache directories in the `.crosspoint/` folder).
|
||||
There can be issues with broken cache or config. In this case, delete the `.crosspoint` directory on your SD card (or consider deleting only `settings.json`, `state.json`, or `epub_*` cache directories in the `.crosspoint/` folder).
|
||||
|
||||
@@ -14,6 +14,7 @@ fi
|
||||
set -euo pipefail
|
||||
|
||||
GIT_LS_FILES_FLAGS=""
|
||||
# -g scopes formatting to tracked files currently modified in git status.
|
||||
if [[ "${1:-}" == "-g" ]]; then
|
||||
GIT_LS_FILES_FLAGS="--modified"
|
||||
fi
|
||||
@@ -21,6 +22,7 @@ fi
|
||||
CLANG_FORMAT_VERSION_RAW="$(${CLANG_FORMAT_BIN} --version)"
|
||||
CLANG_FORMAT_MAJOR="$(printf '%s\n' "${CLANG_FORMAT_VERSION_RAW}" | grep -oE '[0-9]+' | head -n1)"
|
||||
|
||||
# Guard against local binaries older than the repo formatting config.
|
||||
if [[ -z "${CLANG_FORMAT_MAJOR}" || "${CLANG_FORMAT_MAJOR}" -lt 21 ]]; then
|
||||
echo "Error: ${CLANG_FORMAT_BIN} is too old: ${CLANG_FORMAT_VERSION_RAW}"
|
||||
echo "This repository's .clang-format requires clang-format 21 or newer."
|
||||
@@ -37,9 +39,15 @@ fi
|
||||
# --exclude-standard: ignores files in .gitignore
|
||||
# Additionally exclude files in 'lib/EpdFont/builtinFonts/' as they are script-generated.
|
||||
# Also exclude files in 'lib/Epub/Epub/hyphenation/generated/' as they are script-generated.
|
||||
# Keep the no-match case non-fatal: grep returns 1 when no files match,
|
||||
# which is expected when there are no modified C/C++ files.
|
||||
set +o pipefail
|
||||
git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \
|
||||
| grep -E '\.(c|cpp|h|hpp)$' \
|
||||
| grep -v -E '^lib/EpdFont/builtinFonts/' \
|
||||
| grep -v -E '^lib/Epub/Epub/hyphenation/generated/' \
|
||||
| grep -v -E '^lib/uzlib/' \
|
||||
| grep -v -E '^lib/miniz/third_party/' \
|
||||
| xargs -r "${CLANG_FORMAT_BIN}" -style=file -i
|
||||
# Restore strict pipeline failure handling for the rest of the script.
|
||||
set -o pipefail
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
.DESCRIPTION
|
||||
Formats all C/C++ source and header files in the repository, excluding
|
||||
generated, vendored, and build directories (open-x4-sdk, builtinFonts,
|
||||
generated, vendored, and build directories (freeink-sdk, builtinFonts,
|
||||
hyphenation tries, uzlib, .pio, *.generated.h).
|
||||
|
||||
The clang-format binary path is resolved once and cached in
|
||||
@@ -92,11 +92,12 @@ function Resolve-ClangFormat {
|
||||
$clangFormat = Resolve-ClangFormat
|
||||
|
||||
$exclude = @(
|
||||
'open-x4-sdk'
|
||||
'freeink-sdk'
|
||||
'lib\EpdFont\builtinFonts'
|
||||
'lib\Epub\Epub\hyphenation\generated'
|
||||
'lib\uzlib'
|
||||
'.pio'
|
||||
'.venv'
|
||||
)
|
||||
|
||||
function Test-Excluded($fullPath) {
|
||||
|
||||
@@ -7,5 +7,6 @@ It is written for software developers who may be new to embedded development.
|
||||
- [Architecture Overview](./architecture.md)
|
||||
- [Development Workflow](./development-workflow.md)
|
||||
- [Testing and Debugging](./testing-debugging.md)
|
||||
- [Touch and UI Development](./touch-and-ui.md)
|
||||
|
||||
If you are new, start with [Getting Started](./getting-started.md).
|
||||
|
||||
@@ -8,17 +8,18 @@ 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[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]
|
||||
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]
|
||||
```
|
||||
|
||||
## Runtime lifecycle
|
||||
@@ -58,7 +59,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/`: WiFi selection, AP/STA mode, file transfer server
|
||||
- `src/activities/network/`: Wi-Fi selection, AP/STA mode, file transfer server
|
||||
- `src/activities/boot_sleep/`: boot and sleep transitions
|
||||
|
||||
## Reader and content pipeline
|
||||
@@ -73,10 +74,11 @@ 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/CSS]
|
||||
G --> H[Layout pages/sections]
|
||||
H --> I[Write section and metadata caches]
|
||||
I --> J[Render current page via GfxRenderer]
|
||||
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]
|
||||
```
|
||||
|
||||
Why caching matters:
|
||||
@@ -98,7 +100,7 @@ flowchart TD
|
||||
D --> E[Locate container and OPF]
|
||||
E --> F[Build or load BookMetadataCache]
|
||||
F --> G[Load TOC and spine]
|
||||
G --> H[Load or parse CSS rules]
|
||||
G --> H[Load CSS cache or parse manifest/base-dir CSS]
|
||||
|
||||
H --> I[EpubReaderActivity]
|
||||
I --> J{Section cache exists for current settings?}
|
||||
@@ -121,7 +123,12 @@ flowchart TD
|
||||
|
||||
Notes:
|
||||
|
||||
- "section cache exists" depends on cache-busting parameters such as font and layout-related settings
|
||||
- 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
|
||||
- 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
|
||||
|
||||
@@ -138,14 +145,18 @@ Typical persisted areas on SD:
|
||||
/.crosspoint/
|
||||
epub_<hash>/
|
||||
book.bin
|
||||
css_rules.cache
|
||||
progress.bin
|
||||
cover.bmp
|
||||
sections/*.bin
|
||||
settings.bin
|
||||
state.bin
|
||||
img_* cache files
|
||||
settings.json
|
||||
state.json
|
||||
```
|
||||
|
||||
For binary cache formats, see `docs/file-formats.md`.
|
||||
`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`.
|
||||
|
||||
## Networking architecture
|
||||
|
||||
@@ -153,14 +164,18 @@ Network file transfer is controlled by `src/activities/network/CrossPointWebServ
|
||||
|
||||
Modes:
|
||||
|
||||
- STA: join existing WiFi network
|
||||
- STA: join existing Wi-Fi 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`.
|
||||
@@ -170,6 +185,7 @@ 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.
|
||||
@@ -179,9 +195,10 @@ 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.)
|
||||
- `open-x4-sdk/`: hardware SDK submodule (display, input, storage, battery)
|
||||
- `freeink-sdk/`: hardware SDK submodule (display, input, storage, battery). Docs: https://freeink.org/docs
|
||||
- `docs/`: user and technical documentation
|
||||
|
||||
## Embedded constraints that shape design
|
||||
|
||||
@@ -6,6 +6,7 @@ This page defines the expected local workflow before opening a pull request.
|
||||
|
||||
- Fork the repository to your own GitHub account
|
||||
- Clone your fork locally and add the upstream repository if needed
|
||||
- Enable repo hooks once per clone: `git config core.hooksPath .githooks && chmod +x .githooks/pre-commit`
|
||||
|
||||
- Branch from `master`
|
||||
- Keep each PR focused on one fix or feature area
|
||||
|
||||
@@ -53,6 +53,13 @@ If you already cloned without submodules:
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
Enable the repository-managed Git hooks (required once per clone):
|
||||
|
||||
```sh
|
||||
git config core.hooksPath .githooks
|
||||
chmod +x .githooks/pre-commit
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
# Touch and UI Development
|
||||
|
||||
CrossPoint now runs on touch devices (Seeed Sticky, M5Paper, LilyGo T5) alongside the button-only Xteink X3/X4. Every screen must work with both input styles. There are two supported ways to get there:
|
||||
|
||||
1. **New screens: build them with FreeInkUI components.** Touch hit-testing, tap highlighting, long-press, and button focus navigation come with the component; you never hand-roll coordinate math.
|
||||
2. **Existing screens and in-flight features: use the MappedInputManager touch bridge.** A small set of helpers adds tap/hold/swipe support to hand-rolled rendering without restructuring the activity.
|
||||
|
||||
If you are starting a new activity, use FreeInkUI. If you have a feature branch with a hand-rolled screen already working on buttons, use the bridge; do not rewrite mid-flight.
|
||||
|
||||
---
|
||||
|
||||
## Path 1: New screens with FreeInkUI
|
||||
|
||||
FreeInkUI (`freeink-sdk/libs/ui/FreeInkUI`, namespace `freeink::ui`) is an immediate-mode component library. The core idea:
|
||||
|
||||
- While a component renders, it registers its tappable areas ("hit rects") into the frame's interaction buffer via `Frame::hit(rect, action, value, inputMask, state)`.
|
||||
- Each loop you build an `InputSnapshot` from the input manager and route it against that buffer. If a tap (or mapped button press) lands in a registered rect, the routed `ActionEvent` tells you which action fired and with what value.
|
||||
- Components query `frame.stateFor(action, value)` while painting, so touch-down highlight and focus states render correctly without any per-activity code.
|
||||
|
||||
You get touch, hold highlighting, long-press, minimum touch-target sizing, and orientation-aware coordinates for free. The same action IDs fire from physical buttons, so one code path serves both input styles.
|
||||
|
||||
### Component inventory
|
||||
|
||||
All under `freeink-sdk/libs/ui/FreeInkUI/include/components/`:
|
||||
|
||||
| Category | Components |
|
||||
|---|---|
|
||||
| Controls | `button`, `checkbox`, `slider`, `progress-bar`, `header` |
|
||||
| Lists | `list` (virtualized), `table`, `dropdown`, `radio-group`, `setting-row`, `toggle-row`, `stepper-row` |
|
||||
| Keyboard | `keyboard` (QWERTY/AZERTY/QWERTZ/ES layouts), `key-grid` |
|
||||
| Overlays | `popup`, `option-dialog`, `context-menu`, `message-panel`, `toast` |
|
||||
| Bars | `status-bar`, `tab-bar`, `reader-chrome`, `battery-indicator`, `gesture-bar`, `tap-zones` |
|
||||
| Media | `book-card`, `cover-grid`, `cover-carousel`, `metric-card` |
|
||||
| Text | `text-field`, `text-area` |
|
||||
|
||||
### Integration skeleton
|
||||
|
||||
The in-tree reference is [`src/activities/util/KeyboardEntryActivity.cpp`](../../src/activities/util/KeyboardEntryActivity.cpp); it drives the FreeInkUI keyboard component inside a normal Activity. The shape:
|
||||
|
||||
```cpp
|
||||
#include <FreeInkUIGfxRenderer.h>
|
||||
namespace fui = freeink::ui;
|
||||
|
||||
// Render step: draw the component and (re)register its hit rects.
|
||||
fui::GfxRendererTarget target(renderer); // adapts GfxRenderer to FreeInkUI's DrawTarget
|
||||
target.setFont(fui::GfxRendererTarget::FONT_BODY, UI_12_FONT_ID);
|
||||
const fui::DeviceContext device = target.deviceContext(); // orientation, safe area, touch sizing
|
||||
|
||||
fui::Frame<48> frame(target, device, fui::InputSnapshot{}, interactions); // 48 = max hit rects
|
||||
fui::KeyboardProps props;
|
||||
props.layout = ¤tLayout();
|
||||
props.keyAction = ACTION_KEY; // one action id; the key is the event value
|
||||
props.inputMask = fui::InputTouch | fui::InputLongPress;
|
||||
fui::keyboard(frame, kbRect, props); // draws AND registers hit rects
|
||||
```
|
||||
|
||||
```cpp
|
||||
// Input step (each loop): feed touch state, act on routed events.
|
||||
int tx, ty, tapX, tapY, hx, hy;
|
||||
const bool pressedDown = mappedInput.wasScreenTouchDown(tx, ty);
|
||||
const bool tapped = mappedInput.wasScreenTapped(tapX, tapY);
|
||||
const bool inContact = mappedInput.isScreenTouchHeld(hx, hy);
|
||||
|
||||
const auto result = touchRouter.update(interactions, pressedDown, tx, ty,
|
||||
tapped, tapX, tapY, inContact, millis());
|
||||
if (result.event) {
|
||||
activateValue(result.event.value, result.event.longPress);
|
||||
requestUpdate();
|
||||
} else if (result.activeChanged) {
|
||||
requestUpdate(); // repaint touch-down highlight
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `Frame<N>` is templated on the max number of hit rects; the buffer is stack/static, no heap.
|
||||
- `fui::TouchHoldRouter` (see `components/keyboard/keyboard.h`) synthesizes long-press while the finger is still down and swallows the eventual release. Use it whenever a component distinguishes tap from long-press.
|
||||
- For simpler screens without long-press, `FreeInkApp` / `Screen<N>` (`FreeInkApp.h`) is the ergonomic layer: it owns the interaction buffer, offers a top-to-bottom builder (`takeTop`, footer helpers, etc.), and its `render(input)` call routes the snapshot and dispatches registered action callbacks in one step. `snapshotFrom(input, device)` in `FreeInkUIInputManager.h` builds the `InputSnapshot` with orientation-aware touch mapping.
|
||||
- Everything stays allocation-free and works on the button-only devices unchanged: physical buttons route through the same interaction table via each hit rect's `inputMask`.
|
||||
|
||||
---
|
||||
|
||||
## Path 2: Adding touch to existing hand-rolled screens
|
||||
|
||||
For activities that draw their own rows, menus, and buttons, `MappedInputManager` ([src/MappedInputManager.h](../../src/MappedInputManager.h)) exposes bridge helpers. They all return **logical screen coordinates** (orientation already applied via `GfxRenderer::tapToLogical`); never touch raw normalized panel coordinates or the SDK `InputManager` directly.
|
||||
|
||||
| Helper | Use for |
|
||||
|---|---|
|
||||
| `wasScreenTapped(x, y)` | A completed tap (press + release), with logical coords |
|
||||
| `wasScreenTouchDown(x, y)` | Touch-down (held > 90 ms, not yet released): draw selection highlight |
|
||||
| `isScreenTouchHeld(x, y)` | Live contact position while the finger is down (drag tracking) |
|
||||
| `wasTapInRect(x, y, w, h)` | One-off hit test on a rectangle (a single button, a banner) |
|
||||
| `wasListItemTapped(index, count, selected, listTop, listHeight, hasSubtitle)` | Taps on a standard UITheme list; does the row/paging math for you |
|
||||
| `wasListItemTouchedDown(...)` | Same geometry, touch-down phase (highlight before activate) |
|
||||
| `rowTouch(row, top, rowStep, rowCount, xStart, xEnd, rowHeight)` | Any custom band of equal-height rows; returns `RowTouch::None/Down/Tap` |
|
||||
| `colTouch(col, left, colStep, colCount, yStart, yEnd, colWidth)` | Horizontal button bands (dialogs, prompts) |
|
||||
| `wasSwipe()` | Raw swipe direction if you need one beyond the global gestures |
|
||||
| `hasTouch()` | True when the device has a touch panel (rarely needed; helpers simply never fire without one) |
|
||||
|
||||
### Pattern A: standard themed list
|
||||
|
||||
One call per loop; UITheme owns the row geometry ([EpubReaderBookmarksActivity.cpp:123](../../src/activities/reader/EpubReaderBookmarksActivity.cpp)):
|
||||
|
||||
```cpp
|
||||
int tapped = -1;
|
||||
if (mappedInput.wasListItemTapped(tapped, bookmarks.size(), selectorIndex, listY, listHeight, true)) {
|
||||
selectorIndex = tapped;
|
||||
openBookmark();
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern B: custom rows with hold highlight
|
||||
|
||||
For non-theme row layouts, use `rowTouch` and distinguish `Down` (highlight) from `Tap` (activate), as in [EpubReaderFootnotesActivity.cpp](../../src/activities/reader/EpubReaderFootnotesActivity.cpp):
|
||||
|
||||
```cpp
|
||||
int row = -1;
|
||||
const auto touch = mappedInput.rowTouch(row, listTop, lineHeight, visibleCount,
|
||||
contentX, contentX + contentWidth);
|
||||
if (touch != MappedInputManager::RowTouch::None) {
|
||||
const int touched = scrollOffset + row;
|
||||
if (touch == MappedInputManager::RowTouch::Down) {
|
||||
if (selectedIndex != touched) { selectedIndex = touched; requestUpdate(); }
|
||||
} else { // RowTouch::Tap
|
||||
selectedIndex = touched;
|
||||
activateSelection();
|
||||
}
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
The `Down` state exists because e-ink repaints are slow: highlight on touch-down gives immediate feedback, activation happens on release.
|
||||
|
||||
### Global gestures: do not reimplement these
|
||||
|
||||
Three gestures are handled once, for every screen. Activities must not add their own edge-swipe handling:
|
||||
|
||||
| Gesture | Trigger | Where it is handled |
|
||||
|---|---|---|
|
||||
| Back | Right-swipe starting in the left 25% of the screen | Folded into `Button::Back`, so the existing `wasPressed(Button::Back)` in your activity already fires |
|
||||
| Home | Up-swipe starting in the bottom 14% | `ActivityManager::loop()`; pops to Home (activities can override via `handleHomeGesture()`) |
|
||||
| Menu | Down-swipe starting in the top 14% | Activities that have a menu check `wasMenuGesture()` themselves (the reader does this) |
|
||||
|
||||
Because the back gesture arrives as `Button::Back`, most button-era activities gain back-swipe support with zero changes. That is the bar to aim for: bridge helpers should make touch an additive layer over the button flow, not a second input state machine.
|
||||
|
||||
### Bridge rules
|
||||
|
||||
- Handle touch in `loop()` next to the existing button handling, one helper call per interaction zone, and `return` after consuming an event (mirrors the button pattern).
|
||||
- Never call the SDK `InputManager` or read GPIO directly; the HAL rule from the main guide applies to touch too.
|
||||
- Coordinates from the helpers are logical and orientation-correct on all four rotations; test at least Portrait and one Landscape mode before PR.
|
||||
- Nothing to clean up in `onExit()`; the helpers are stateless from the activity's point of view.
|
||||
|
||||
---
|
||||
|
||||
## Building and testing on non-Xteink devices
|
||||
|
||||
Each MCU family is its own binary: X3/X4 are ESP32-C3, Sticky and LilyGo T5 are ESP32-S3, M5Paper v1.1 is a classic ESP32. The Sticky env ships in `platformio.ini` (`pio run -e sticky`). Envs for other devices go in **`platformio.local.ini`**, a gitignored file that PlatformIO merges over `platformio.ini` (see `extra_configs`). Create it next to `platformio.ini`; personal envs, ports, and debug flags live there and never get committed.
|
||||
|
||||
Both envs below extend the repo's `[base]`, so they build against the `freeink-sdk` submodule with all the normal deps and scripts.
|
||||
|
||||
### M5Paper v1.1 (classic ESP32, IT8951 panel)
|
||||
|
||||
```ini
|
||||
[env:m5paper_v11]
|
||||
extends = base
|
||||
board = esp32dev
|
||||
board_build.mcu = esp32
|
||||
board_build.flash_mode = qio
|
||||
; CP2104 UART bridge: 921600 drops out on macOS after the stub baud switch
|
||||
upload_speed = 460800
|
||||
build_unflags =
|
||||
${base.build_unflags}
|
||||
; classic ESP32 has UART serial, not USB CDC; Logging.h keys off these
|
||||
-DARDUINO_USB_MODE=1
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
build_flags =
|
||||
${base.build_flags}
|
||||
-DFREEINK_DEVICE_M5PAPER=1
|
||||
; the 63KB 540x960 framebuffer lives in PSRAM (FREEINK_FB_PSRAM auto-on)
|
||||
-DBOARD_HAS_PSRAM
|
||||
-DCROSSPOINT_VERSION=\"${crosspoint.version}-m5paper\"
|
||||
-DENABLE_SERIAL_LOG
|
||||
-DLOG_LEVEL=2
|
||||
; touch-first device: hide front-button hint labels
|
||||
-DCROSSPOINT_SHOW_BUTTON_HINTS=0
|
||||
; archive-scan-order workaround: without these a full relink drops Wire's i2c symbols
|
||||
-Wl,-u,i2cInit
|
||||
-Wl,-u,i2cSlaveInit
|
||||
```
|
||||
|
||||
### LilyGo T5 S3 (ESP32-S3, controller-less panel via LovyanGFX)
|
||||
|
||||
```ini
|
||||
[env:lilygo_t5s3]
|
||||
extends = base
|
||||
board = esp32-s3-devkitc1-n16r8
|
||||
board_build.mcu = esp32s3
|
||||
build_flags =
|
||||
${base.build_flags}
|
||||
-DFREEINK_DEVICE_LILYGO=1
|
||||
; board injects the parallel-bus pins + PMIC power hooks (BoardT5S3)
|
||||
-DFREEINK_LGFX_EPD_CONFIG=lilygoT5S3LgfxConfig
|
||||
-DCROSSPOINT_VERSION=\"${crosspoint.version}-lilygo\"
|
||||
-DENABLE_SERIAL_LOG
|
||||
-DLOG_LEVEL=2
|
||||
-DCROSSPOINT_SHOW_BUTTON_HINTS=0
|
||||
lib_deps =
|
||||
${base.lib_deps}
|
||||
; LgfxEpdConfig for the T5 S3 (pins, PCA9535/TPS65185 power sequence)
|
||||
BoardT5S3=symlink://freeink-sdk/libs/hardware/BoardT5S3
|
||||
; LovyanGFX Panel_EPD drives the controller-less ED047TC1 panel
|
||||
m5stack/M5GFX @ 0.2.20
|
||||
```
|
||||
|
||||
Then `pio run -e m5paper_v11 -t upload` (or `-e lilygo_t5s3`). Gotchas worth knowing:
|
||||
|
||||
- **Flash mode matters.** The M5Paper is `qio`; the X4-family standalone envs need `dio`. A wrong flash-mode header boots into a `partition 0 invalid magic number 0xffff` loop even though esptool verified the write.
|
||||
- **One `FREEINK_DEVICE_*` flag per env** selects the board profile (pins, panel, touch controller) from the SDK's `BoardConfig`. See `freeink-sdk/platformio.sample.ini` for reference envs of every supported device.
|
||||
- **Serial logs:** `[base]` does not enable logging; without `-DENABLE_SERIAL_LOG` a non-default env prints nothing.
|
||||
- No touch hardware on your desk? The X4 build still exercises the same code paths through buttons; touch-specific behavior (tap zones, gestures) needs a real device.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Dictionary
|
||||
|
||||
Look up words while reading an EPUB using an offline StarDict dictionary stored on the SD card.
|
||||
|
||||
## Supported Format
|
||||
|
||||
The reader supports **StarDict** dictionaries. When searching for dictionaries online, look for "StarDict format" or files with `.dict`, `.idx`, and `.ifo` extensions.
|
||||
|
||||
A dictionary folder must contain:
|
||||
|
||||
- `.idx` — word index (required, **must be uncompressed** — a `.idx.gz` will not work; decompress it on your computer with `gzip -d` first)
|
||||
- `.dict` or `.dict.dz` — definition data (`.dict.dz` is supported as-is; entries are decompressed on the fly during lookup)
|
||||
- `.ifo` — metadata (optional)
|
||||
|
||||
Not supported: `.syn` synonym files (ignored), dictionaries with 64-bit index offsets (`idxoffsetbits=64` in the `.ifo` — rare, and rejected with an error), and HTML-formatted definitions render as raw markup rather than styled text.
|
||||
|
||||
## Setting Up a Dictionary
|
||||
|
||||
1. Copy your dictionary folder(s) to `/dictionaries/` on the SD card — one dictionary per folder, e.g. `/dictionaries/webster/webster.idx` + `webster.dict.dz`. A hidden `/.dictionaries/` folder (dot-prefixed) works the same way, for keeping it out of the file browser.
|
||||
2. Open **Settings → Reader → Dictionary** on the device.
|
||||
3. Select a dictionary from the list, or **None** to disable lookups.
|
||||
|
||||
The Dictionary setting only appears when at least one usable dictionary folder exists. Folders containing more than one dictionary (multiple `.idx` stems) are skipped as ambiguous.
|
||||
|
||||
## Looking Up a Word
|
||||
|
||||
Two ways to start a lookup while reading:
|
||||
|
||||
- Open the reader menu (**Confirm**) and choose **Look Up**.
|
||||
- Or set **Settings → Controls → Long-press Menu** to "Dictionary", then hold **Confirm** (~0.4s) on the reading page.
|
||||
|
||||
One word on the page becomes highlighted:
|
||||
|
||||
1. Use **Left/Right** to move between words in reading order, and the side **Up/Down** buttons to jump between lines.
|
||||
2. Press **Confirm** to look up the highlighted word.
|
||||
3. Press **Back** to return to the reader.
|
||||
|
||||
On the very first lookup with a dictionary (and again if the dictionary file changes), the reader shows *"Indexing dictionary…"* while it builds a small `.qidx` sidecar file next to the `.idx`. This takes a few seconds for large dictionaries and makes all subsequent lookups fast. The sidecar can be deleted safely at any time — it will simply be rebuilt.
|
||||
|
||||
### How Lookup Works
|
||||
|
||||
1. **Direct match** — the word is found as-is (case-insensitive) in the dictionary index. Surrounding punctuation is ignored.
|
||||
2. **Stemming** — on a miss, common English word forms are retried automatically: possessives and plurals (`dogs` → `dog`, `stories` → `story`) and verb endings (`walked` → `walk`, `running` → `run`, `making` → `make`).
|
||||
3. **Not found** — a short popup appears and you return to word selection.
|
||||
|
||||
## The Definition Screen
|
||||
|
||||
When a word is found, the definition screen shows the matched headword at the top and the definition text below, with a page counter for long definitions.
|
||||
|
||||
- **Left/Right** or side **Up/Down** — previous / next page
|
||||
- **Back** — return to word selection
|
||||
+195
-90
@@ -1,22 +1,26 @@
|
||||
# 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 3
|
||||
### Version 7
|
||||
|
||||
ImHex Pattern:
|
||||
`book.bin` stores EPUB metadata plus lookup tables for spine and TOC entries.
|
||||
The current firmware writes this version from `BookMetadataCache`.
|
||||
|
||||
ImHex pattern:
|
||||
|
||||
```c++
|
||||
import std.mem;
|
||||
import std.string;
|
||||
import std.core;
|
||||
|
||||
// === Configuration ===
|
||||
#define EXPECTED_VERSION 3
|
||||
#define EXPECTED_VERSION 7
|
||||
#define MAX_STRING_LENGTH 65535
|
||||
|
||||
// === String Structure ===
|
||||
|
||||
struct String {
|
||||
u32 length [[hidden, comment("String byte length")]];
|
||||
if (length > MAX_STRING_LENGTH) {
|
||||
@@ -29,74 +33,56 @@ 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 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 ===
|
||||
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")]];
|
||||
};
|
||||
|
||||
struct TocEntry {
|
||||
String title [[comment("Chapter/section title")]];
|
||||
String href [[comment("Resource path")]];
|
||||
String anchor [[comment("Fragment identifier")]];
|
||||
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 ===
|
||||
u8 level [[comment("Nesting level")]];
|
||||
s16 spineIndex [[comment("Index into spine (-1 if none)")]];
|
||||
};
|
||||
|
||||
struct BookBin {
|
||||
// Header
|
||||
u8 version [[comment("Format version"), color("FFD93D")]];
|
||||
|
||||
// Version validation
|
||||
u8 version;
|
||||
if (version != EXPECTED_VERSION) {
|
||||
std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION));
|
||||
}
|
||||
|
||||
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 section
|
||||
Metadata metadata [[comment("Book metadata")]];
|
||||
|
||||
// Validate LUT offset alignment
|
||||
|
||||
u32 lutOffset [[comment("Offset to lookup tables")]];
|
||||
u16 spineCount;
|
||||
u16 tocCount;
|
||||
|
||||
Metadata metadata;
|
||||
|
||||
u32 currentOffset = $;
|
||||
if (currentOffset != lutOffset) {
|
||||
std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset));
|
||||
}
|
||||
|
||||
// Lookup Tables
|
||||
u32 spineLut[spineCount] [[comment("Spine entry offsets"), color("4D96FF")]];
|
||||
u32 tocLut[tocCount] [[comment("TOC entry offsets"), color("FF6B9D")]];
|
||||
|
||||
// Data Entries
|
||||
SpineEntry spines[spineCount] [[comment("Spine entries (reading order)")]];
|
||||
TocEntry toc[tocCount] [[comment("Table of contents entries")]];
|
||||
};
|
||||
|
||||
// === File Parsing ===
|
||||
u32 spineLut[spineCount] [[comment("Spine entry offsets")]];
|
||||
u32 tocLut[tocCount] [[comment("TOC entry offsets")]];
|
||||
|
||||
SpineEntry spines[spineCount];
|
||||
TocEntry toc[tocCount];
|
||||
};
|
||||
|
||||
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));
|
||||
}
|
||||
@@ -104,20 +90,45 @@ if (parsedSize != fileSize) {
|
||||
|
||||
## `section.bin`
|
||||
|
||||
### Version 8
|
||||
### Version 30
|
||||
|
||||
ImHex Pattern:
|
||||
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 30 is binary-identical to version 29. The version was bumped because
|
||||
Arabic contextual shaping changed text measurement (`getTextAdvanceX` now
|
||||
measures the shaped visual text), so word positions cached by v29 no longer
|
||||
match what `drawText` renders.
|
||||
|
||||
Version 28 introduced serialized word style bits for underline, strikethrough,
|
||||
superscript, and subscript. The format also 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
|
||||
- serialized word style bits for underline, strikethrough, superscript, and
|
||||
subscript
|
||||
- flat TextBlock word storage (v29): per-word arrays plus one shared
|
||||
NUL-terminated text blob, replacing v28's length-prefixed word strings. The
|
||||
on-disk order mirrors the in-RAM arena so the firmware reads a whole block
|
||||
payload with a single allocation and a single SD read
|
||||
|
||||
ImHex pattern:
|
||||
|
||||
```c++
|
||||
import std.mem;
|
||||
import std.string;
|
||||
import std.core;
|
||||
|
||||
// === Configuration ===
|
||||
#define EXPECTED_VERSION 8
|
||||
#define EXPECTED_VERSION 30
|
||||
#define MAX_STRING_LENGTH 65535
|
||||
|
||||
// === String Structure ===
|
||||
#define FOOTNOTE_NUMBER_LEN 32
|
||||
#define FOOTNOTE_HREF_LEN 96
|
||||
|
||||
struct String {
|
||||
u32 length [[hidden, comment("String byte length")]];
|
||||
@@ -131,90 +142,184 @@ fn format_string(String s) {
|
||||
return s.data;
|
||||
};
|
||||
|
||||
// === Page Structure ===
|
||||
|
||||
enum StorageType : u8 {
|
||||
PageLine = 1
|
||||
enum PageElementTag : u8 {
|
||||
TAG_PageLine = 1,
|
||||
TAG_PageImage = 2,
|
||||
TAG_PageHorizontalRule = 3
|
||||
};
|
||||
|
||||
enum WordStyle : u8 {
|
||||
REGULAR = 0,
|
||||
BOLD = 1,
|
||||
ITALIC = 2,
|
||||
BOLD_ITALIC = 3
|
||||
BOLD_ITALIC = 3,
|
||||
UNDERLINE = 4,
|
||||
STRIKETHROUGH = 8,
|
||||
SUP = 16,
|
||||
SUB = 32
|
||||
};
|
||||
|
||||
enum BlockStyle : u8 {
|
||||
enum TextAlign : 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;
|
||||
u8 hasFocus;
|
||||
u16 textBytes [[comment("Total size of text[], including one NUL per word")]];
|
||||
|
||||
if (wordCount > 0) {
|
||||
u16 textOff[wordCount] [[comment("Byte offset of word i's text within text[]")]];
|
||||
s16 wordXPos[wordCount];
|
||||
if (hasFocus != 0) {
|
||||
u16 wordFocusSuffixX[wordCount] [[comment("Suffix x offset from word start")]];
|
||||
}
|
||||
WordStyle wordStyle[wordCount];
|
||||
if (hasFocus != 0) {
|
||||
u8 wordFocusBoundary[wordCount] [[comment("UTF-8 byte boundary between bold prefix and suffix")]];
|
||||
}
|
||||
char text[textBytes] [[comment("All words back to back, each NUL-terminated")]];
|
||||
}
|
||||
|
||||
BlockStyle blockStyle;
|
||||
};
|
||||
|
||||
struct ImageBlock {
|
||||
String imagePath;
|
||||
s16 width;
|
||||
s16 height;
|
||||
};
|
||||
|
||||
struct PageLine {
|
||||
s16 xPos;
|
||||
s16 yPos;
|
||||
u16 wordCount;
|
||||
String words[wordCount];
|
||||
u16 wordXPos[wordCount];
|
||||
WordStyle wordStyle[wordCount];
|
||||
BlockStyle blockStyle;
|
||||
s16 xPos;
|
||||
s16 yPos;
|
||||
TextBlock block;
|
||||
};
|
||||
|
||||
struct PageImage {
|
||||
s16 xPos;
|
||||
s16 yPos;
|
||||
ImageBlock image;
|
||||
};
|
||||
|
||||
struct PageHorizontalRule {
|
||||
s16 xPos;
|
||||
s16 yPos;
|
||||
u16 width;
|
||||
u8 thickness;
|
||||
};
|
||||
|
||||
struct PageElement {
|
||||
u8 pageElementType;
|
||||
if (pageElementType == 1) {
|
||||
PageElementTag pageElementType;
|
||||
if (pageElementType == TAG_PageLine) {
|
||||
PageLine pageLine [[inline]];
|
||||
} else if (pageElementType == TAG_PageImage) {
|
||||
PageImage pageImage [[inline]];
|
||||
} else if (pageElementType == TAG_PageHorizontalRule) {
|
||||
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];
|
||||
};
|
||||
|
||||
// === Section Bin Structure ===
|
||||
struct AnchorEntry {
|
||||
String anchor;
|
||||
u16 page;
|
||||
};
|
||||
|
||||
struct AnchorMap {
|
||||
u16 count;
|
||||
AnchorEntry entries[count];
|
||||
};
|
||||
|
||||
struct ParagraphLut {
|
||||
u16 count;
|
||||
u16 paragraphIndex[count];
|
||||
};
|
||||
|
||||
struct SectionBin {
|
||||
// Header
|
||||
u8 version [[comment("Format version"), color("FFD93D")]];
|
||||
|
||||
// Version validation
|
||||
u8 version;
|
||||
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 vieportHeight;
|
||||
u16 viewportHeight;
|
||||
bool hyphenationEnabled;
|
||||
bool embeddedStyle;
|
||||
u8 imageRendering;
|
||||
bool focusReadingEnabled;
|
||||
|
||||
u16 pageCount;
|
||||
u32 lutOffset;
|
||||
|
||||
Page page[pageCount];
|
||||
|
||||
// Validate LUT offset alignment
|
||||
u32 pageLutOffset;
|
||||
u32 anchorMapOffset;
|
||||
u32 paragraphLutOffset;
|
||||
u32 listItemLutOffset;
|
||||
|
||||
Page pages[pageCount];
|
||||
|
||||
u32 currentOffset = $;
|
||||
if (currentOffset != lutOffset) {
|
||||
std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset));
|
||||
if (currentOffset != pageLutOffset) {
|
||||
std::warning(std::format("Page LUT offset mismatch: expected 0x{:X}, got 0x{:X}", pageLutOffset, 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];
|
||||
};
|
||||
|
||||
// === File Parsing ===
|
||||
SectionBin section @ 0x00;
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Focus Reading
|
||||
|
||||
Focus Reading is a reading aid that bolds the first portion of each word, guiding your eyes to natural fixation points and helping you read faster with less effort. Some readers — particularly those with ADHD — find it helps them stay engaged with the text and reduces mind-wandering. It is inspired by the Bionic Reading technique.
|
||||
|
||||
<img src="./images/focus-reading/focus-reading.jpg" height="500" alt="Comparison of the same page with and without Focus Reading enabled" />
|
||||
|
||||
*Left: Focus Reading off. Right: Focus Reading on. Both using Literata.*
|
||||
|
||||
## Enabling Focus Reading
|
||||
|
||||
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.
|
||||
|
||||
## Examples
|
||||
|
||||
<img src="./images/focus-reading/focus-reading-notoserif.jpg" height="500" alt="Focus Reading with Noto Serif font" />
|
||||
|
||||
*Focus Reading with Noto Serif font*
|
||||
|
||||
<img src="./images/focus-reading/focus-reading-merriweather.jpg" height="500" alt="Focus Reading with Merriweather font" />
|
||||
|
||||
*Focus Reading with Merriweather font*
|
||||
|
||||
<img src="./images/focus-reading/focus-reading-atkinson.jpg" height="500" alt="Focus Reading with Atkinson Hyperlegible Next font" />
|
||||
|
||||
*Focus Reading with Atkinson Hyperlegible Next font*
|
||||
|
||||
## Notes
|
||||
|
||||
- Focus Reading only applies to regular body text. Already-bold text (headings, emphasis) is left unchanged.
|
||||
- The setting is per-device, not per-book — it applies to all books while enabled.
|
||||
@@ -49,5 +49,5 @@ A convenient script `update_hyphenation.sh` is used to update all languages.
|
||||
To use it, run:
|
||||
|
||||
```sh
|
||||
./scripts/update_hypenation.sh
|
||||
./scripts/update_hyphenation.sh
|
||||
```
|
||||
|
||||
+42
-23
@@ -5,17 +5,29 @@ This guide explains the multi-language support system in CrossPoint Reader.
|
||||
## Supported Languages
|
||||
|
||||
- English
|
||||
- French
|
||||
- German
|
||||
- Portuguese
|
||||
- Spanish
|
||||
- Swedish
|
||||
- Czech
|
||||
- Russian
|
||||
- Ukrainian
|
||||
- Polish
|
||||
- Danish
|
||||
- Turkish
|
||||
- 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à
|
||||
- עברית
|
||||
|
||||
---
|
||||
|
||||
@@ -108,7 +120,9 @@ 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));
|
||||
@@ -175,7 +189,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));
|
||||
Serial.printf("Status: %s\n", tr(STR_CONNECTED));
|
||||
LOG_INF("I18N", "Status: %s", tr(STR_CONNECTED));
|
||||
|
||||
// I18N - Shorthand for I18n::getInstance()
|
||||
I18N.setLanguage(Language::ES);
|
||||
@@ -191,34 +205,39 @@ const char* text = tr(STR_SETTINGS_TITLE); // Macro (recommended)
|
||||
const char* text = I18N.get(StrId::STR_SETTINGS_TITLE); // Direct call
|
||||
const char* text = I18N[StrId::STR_SETTINGS_TITLE]; // Operator overload
|
||||
|
||||
// Set language
|
||||
// Set runtime 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
|
||||
|
||||
Language settings are stored in:
|
||||
The selected language is stored with the rest of the device settings 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
|
||||
```
|
||||
|
||||
This file contains:
|
||||
- Version byte
|
||||
- Current language selection (1 byte)
|
||||
On load, current firmware migrates that legacy file into `settings.json` and
|
||||
renames it to `language.bin.bak`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 216 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 211 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 207 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 260 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 234 KiB |
@@ -0,0 +1,126 @@
|
||||
# SD Card Fonts
|
||||
|
||||
CrossPoint supports loading additional fonts from the SD card, including fonts
|
||||
with extended Unicode coverage (CJK, Cyrillic, Greek, etc.).
|
||||
|
||||
## Installing Fonts
|
||||
|
||||
There are three ways to install fonts:
|
||||
|
||||
### Option 1: Download from device (recommended)
|
||||
|
||||
1. Connect your CrossPoint reader to Wi-Fi
|
||||
2. Go to **Settings > System > Manage Fonts**
|
||||
3. Browse available font families and tap to download
|
||||
4. Downloaded fonts appear immediately in **Settings > Reader > Font Family**
|
||||
|
||||
### Option 2: Upload via web browser
|
||||
|
||||
1. Start **File Transfer** and connect through **Join Network** or **Create Hotspot**
|
||||
2. Open the web interface URL shown on the reader
|
||||
3. Navigate to the **Fonts** tab
|
||||
4. Upload `.cpfont` files using the upload form
|
||||
|
||||
### Option 3: Manual SD card copy
|
||||
|
||||
1. Download font files from the
|
||||
[crosspoint-fonts repository](https://github.com/crosspoint-reader/crosspoint-fonts)
|
||||
2. Copy font family folders to one of two locations on your SD card:
|
||||
|
||||
- `/.fonts/` — hidden directory (preferred; keeps the SD root tidy
|
||||
when mounted on a desktop)
|
||||
- `/fonts/` — visible directory (use this if your OS hides dot-files
|
||||
and you'd rather see the folder in your file manager)
|
||||
|
||||
Both roots are always scanned at boot and the results are merged: a
|
||||
family installed in `/fonts/` shows up even when `/.fonts/` also
|
||||
exists, and vice versa. The two roots only collide if the same family
|
||||
name appears in both — in that case the copy in `/.fonts/` wins and
|
||||
the duplicate in `/fonts/` is ignored.
|
||||
|
||||
SD Card Root/
|
||||
├── .fonts/ ← Hidden root (preferred)
|
||||
│ └── Literata/
|
||||
│ ├── Literata_12.cpfont
|
||||
│ ├── Literata_14.cpfont
|
||||
│ ├── Literata_16.cpfont
|
||||
│ └── Literata_18.cpfont
|
||||
└── fonts/ ← Visible root (equally valid)
|
||||
└── Merriweather/
|
||||
├── Merriweather_12.cpfont
|
||||
└── ...
|
||||
|
||||
3. Insert the SD card and power on your CrossPoint reader
|
||||
|
||||
## Available Pre-Built Fonts
|
||||
|
||||
The current list of pre-built fonts is maintained in the
|
||||
[crosspoint-fonts repository](https://github.com/crosspoint-reader/crosspoint-fonts).
|
||||
|
||||
## Converting Custom Fonts
|
||||
|
||||
To convert your own TrueType/OpenType fonts:
|
||||
|
||||
### Prerequisites
|
||||
|
||||
pip install freetype-py fonttools
|
||||
|
||||
### Single font (one style)
|
||||
|
||||
python3 lib/EpdFont/scripts/fontconvert_sdcard.py \
|
||||
MyFont-Regular.ttf \
|
||||
--intervals latin-ext \
|
||||
--sizes 12,14,16,18 \
|
||||
--style regular \
|
||||
--name MyFont \
|
||||
--output-dir ./MyFont/
|
||||
|
||||
### Multi-style font
|
||||
|
||||
python3 lib/EpdFont/scripts/fontconvert_sdcard.py \
|
||||
--regular MyFont-Regular.ttf \
|
||||
--bold MyFont-Bold.ttf \
|
||||
--italic MyFont-Italic.ttf \
|
||||
--bolditalic MyFont-BoldItalic.ttf \
|
||||
--intervals latin-ext \
|
||||
--sizes 12,14,16,18 \
|
||||
--name MyFont \
|
||||
--output-dir ./MyFont/
|
||||
|
||||
### Available Unicode interval presets
|
||||
|
||||
| Preset | Coverage |
|
||||
|--------|----------|
|
||||
| `ascii` | U+0020–U+007E (Basic Latin) |
|
||||
| `latin1` | U+0080–U+00FF (Latin-1 Supplement) |
|
||||
| `latin-ext` | European languages (Latin + Extended-A/B + punctuation + ligatures) |
|
||||
| `greek` | Greek + Extended Greek |
|
||||
| `cyrillic` | Cyrillic + Supplement |
|
||||
| `hebrew` | Hebrew + Alphabetic Presentation Forms |
|
||||
| `georgian` | Georgian + Georgian Supplement |
|
||||
| `armenian` | Armenian |
|
||||
| `ethiopic` | Ethiopic + Extended |
|
||||
| `vietnamese` | Vietnamese subset (ơ/ư and combining marks) |
|
||||
| `punctuation` | General punctuation (U+2000–U+206F) |
|
||||
| `cjk` | CJK Unified Ideographs + Hiragana + Katakana + Fullwidth |
|
||||
| `hangul` | Korean Hangul syllables + Jamo + Compatibility Jamo |
|
||||
| `cherokee` | Cherokee (historic + supplement block) |
|
||||
| `tifinagh` | Tifinagh |
|
||||
| `symbols` | Math, currency, arrows, box-drawing, misc symbols, dingbats |
|
||||
| `reading` | Literary fiction coverage: Latin, Greek, Cyrillic, math/symbol blocks, supplemental punctuation, and CJK quote marks |
|
||||
| `builtin` | Matches the firmware's built-in font conversion intervals |
|
||||
|
||||
Combine presets with commas: `--intervals latin-ext,greek,cyrillic`
|
||||
|
||||
You can also specify arbitrary Unicode ranges directly:
|
||||
`--intervals latin-ext,(0x2100-0x214F)`
|
||||
|
||||
To list all presets with codepoint counts:
|
||||
|
||||
python3 lib/EpdFont/scripts/fontconvert_sdcard.py --list-presets
|
||||
|
||||
### Additional options
|
||||
|
||||
`--force-autohint` — force FreeType's auto-hinter instead of the font's native hinting (useful when a font's built-in hints produce poor results at small sizes).
|
||||
|
||||
Install custom fonts via the web interface or manual SD card copy.
|
||||
+8
-2
@@ -1,7 +1,9 @@
|
||||
# Translators
|
||||
|
||||
Below is a list of users and languages CrossPoint may support in the future.
|
||||
Note because a language is below does not mean there is official support for the language at this time.
|
||||
Below is a list of translator credits for languages with known contributors.
|
||||
Official UI language support is determined by the YAML files in
|
||||
`lib/I18n/translations/`; see [i18n.md](./i18n.md) for the current supported
|
||||
language list.
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -26,6 +28,7 @@ If you'd like to add your name to this list, please open a PR adding yourself an
|
||||
## Italian
|
||||
- [andreaturchet](https://github.com/andreaturchet)
|
||||
- [fragolinux](https://github.com/fragolinux)
|
||||
- [alan0ford](https://github.com/alan0ford)
|
||||
|
||||
## Russian
|
||||
- [madebyKir](https://github.com/madebyKir)
|
||||
@@ -36,6 +39,7 @@ If you'd like to add your name to this list, please open a PR adding yourself an
|
||||
- [Skrzakk](https://github.com/Skrzakk)
|
||||
- [pablohc](https://github.com/pablohc)
|
||||
- [DaniPhii](https://github.com/DaniPhii)
|
||||
- [lpla](https://github.com/lpla)
|
||||
|
||||
## Swedish
|
||||
- [dawiik](https://github.com/dawiik)
|
||||
@@ -46,12 +50,14 @@ If you'd like to add your name to this list, please open a PR adding yourself an
|
||||
|
||||
## Catalan
|
||||
- [angeldenom](https://github.com/angeldenom)
|
||||
- [lpla](https://github.com/lpla)
|
||||
|
||||
## Finnish
|
||||
- [plahteenlahti](https://github.com/plahteenlahti)
|
||||
|
||||
## Ukrainian
|
||||
- [mirus-ua](https://github.com/mirus-ua)
|
||||
- [KymAndriy](https://github.com/KymAndriy)
|
||||
|
||||
## Belarusian
|
||||
- [Dexif](https://github.com/dexif)
|
||||
|
||||
+13
-10
@@ -1,6 +1,6 @@
|
||||
# Troubleshooting
|
||||
|
||||
This document show most common issues and possible solutions while using the device features.
|
||||
This document shows common issues and possible solutions while using the device features.
|
||||
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Cannot See the Device on the Network](#cannot-see-the-device-on-the-network)
|
||||
@@ -14,25 +14,27 @@ This document show most common issues and possible solutions while using the dev
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify both devices are on the **same WiFi network**
|
||||
- Check your computer/phone WiFi settings
|
||||
- Confirm the CrossPoint Reader shows "Connected" status
|
||||
1. Verify both devices are on the correct network
|
||||
- Check your computer/phone Wi-Fi settings
|
||||
- In **Join Network** mode, your computer/phone and CrossPoint Reader must be on the same Wi-Fi network
|
||||
- In **Create Hotspot** mode, your computer/phone must be connected to the `CrossPoint-Reader` hotspot
|
||||
2. Double-check the IP address
|
||||
- Make sure you typed it correctly
|
||||
- Include `http://` at the beginning
|
||||
- Try the displayed IP address if `http://crosspoint.local/` does not resolve
|
||||
3. Try disabling VPN if you're using one
|
||||
4. Some networks have "client isolation" enabled - check with your network administrator
|
||||
4. Some networks have "client isolation" enabled - use Create Hotspot mode or check with your network administrator
|
||||
|
||||
### Connection Drops or Times Out
|
||||
|
||||
**Problem:** WiFi connection is unstable
|
||||
**Problem:** Wi-Fi connection is unstable
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Move closer to the WiFi router
|
||||
1. Move closer to the Wi-Fi router, or use Create Hotspot mode for a direct connection
|
||||
2. Check signal strength on the device (should be at least `||` or better)
|
||||
3. Avoid interference from other devices
|
||||
4. Try a different WiFi network if available
|
||||
4. Try a different Wi-Fi network if available
|
||||
|
||||
### Upload Fails
|
||||
|
||||
@@ -40,10 +42,11 @@ This document show most common issues and possible solutions while using the dev
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Ensure the file is a valid `.epub` file
|
||||
2. Check that the SD card has enough free space
|
||||
1. Check that the SD card has enough free space
|
||||
2. Check that the filename is valid for the SD card filesystem
|
||||
3. Try uploading a smaller file first to test
|
||||
4. Refresh the browser page and try again
|
||||
5. If WebSocket upload fails repeatedly, refresh the page and retry with the HTTP fallback path
|
||||
|
||||
### Saved Password Not Working
|
||||
|
||||
|
||||
+400
-231
@@ -1,72 +1,36 @@
|
||||
# Webserver Endpoints
|
||||
|
||||
This document describes all HTTP and WebSocket endpoints available on the CrossPoint Reader webserver.
|
||||
This document describes the HTTP, WebSocket, WebDAV, and discovery endpoints
|
||||
available while CrossPoint Reader is in File Transfer or Calibre Wireless mode.
|
||||
|
||||
- [Webserver Endpoints](#webserver-endpoints)
|
||||
- [Overview](#overview)
|
||||
- [HTTP Endpoints](#http-endpoints)
|
||||
- [GET `/` - Home Page](#get----home-page)
|
||||
- [GET `/files` - File Browser Page](#get-files---file-browser-page)
|
||||
- [GET `/api/status` - Device Status](#get-apistatus---device-status)
|
||||
- [GET `/api/files` - List Files](#get-apifiles---list-files)
|
||||
- [POST `/upload` - Upload File](#post-upload---upload-file)
|
||||
- [POST `/mkdir` - Create Folder](#post-mkdir---create-folder)
|
||||
- [POST `/delete` - Delete File or Folder](#post-delete---delete-file-or-folder)
|
||||
- [WebSocket Endpoint](#websocket-endpoint)
|
||||
- [Port 81 - Fast Binary Upload](#port-81---fast-binary-upload)
|
||||
- [Network Modes](#network-modes)
|
||||
- [Station Mode (STA)](#station-mode-sta)
|
||||
- [Access Point Mode (AP)](#access-point-mode-ap)
|
||||
- [Notes](#notes)
|
||||
- HTTP server: port 80
|
||||
- WebSocket upload server: port 81
|
||||
- UDP discovery listener: port 8134
|
||||
- WebDAV: port 80, handled by the same HTTP server
|
||||
|
||||
Examples use `crosspoint.local`. If mDNS does not resolve on your network, use
|
||||
the IP address shown on the device screen.
|
||||
|
||||
## Overview
|
||||
## HTTP Pages
|
||||
|
||||
The CrossPoint Reader exposes a webserver for file management and device monitoring:
|
||||
| Method | Path | Purpose |
|
||||
|--------|------|---------|
|
||||
| `GET` | `/` | Home/status page |
|
||||
| `GET` | `/files` | File manager page |
|
||||
| `GET` | `/settings` | Web settings page |
|
||||
| `GET` | `/fonts` | SD-card font manager page |
|
||||
| `GET` | `/js/jszip.min.js` | JavaScript asset used by the file manager |
|
||||
|
||||
- **HTTP Server**: Port 80
|
||||
- **WebSocket Server**: Port 81 (for fast binary uploads)
|
||||
## Device Status
|
||||
|
||||
---
|
||||
### `GET /api/status`
|
||||
|
||||
## HTTP Endpoints
|
||||
|
||||
### GET `/` - Home Page
|
||||
|
||||
Serves the home page HTML interface.
|
||||
|
||||
**Request:**
|
||||
```bash
|
||||
curl http://crosspoint.local/
|
||||
```
|
||||
|
||||
**Response:** HTML page (200 OK)
|
||||
|
||||
---
|
||||
|
||||
### GET `/files` - File Browser Page
|
||||
|
||||
Serves the file browser HTML interface.
|
||||
|
||||
**Request:**
|
||||
```bash
|
||||
curl http://crosspoint.local/files
|
||||
```
|
||||
|
||||
**Response:** HTML page (200 OK)
|
||||
|
||||
---
|
||||
|
||||
### GET `/api/status` - Device Status
|
||||
|
||||
Returns JSON with device status information.
|
||||
|
||||
**Request:**
|
||||
```bash
|
||||
curl http://crosspoint.local/api/status
|
||||
```
|
||||
|
||||
**Response (200 OK):**
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0.0",
|
||||
@@ -74,258 +38,463 @@ curl http://crosspoint.local/api/status
|
||||
"mode": "STA",
|
||||
"rssi": -45,
|
||||
"freeHeap": 123456,
|
||||
"uptime": 3600
|
||||
"uptime": 3600,
|
||||
"device": "X4"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
| ---------- | ------ | --------------------------------------------------------- |
|
||||
| `version` | string | CrossPoint firmware version |
|
||||
| `ip` | string | Device IP address |
|
||||
| `mode` | string | `"STA"` (connected to WiFi) or `"AP"` (access point mode) |
|
||||
| `rssi` | number | WiFi signal strength in dBm (0 in AP mode) |
|
||||
| `freeHeap` | number | Free heap memory in bytes |
|
||||
| `uptime` | number | Seconds since device boot |
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `version` | string | Firmware version |
|
||||
| `ip` | string | Device IP address |
|
||||
| `mode` | string | `"STA"` for joined Wi-Fi or `"AP"` for hotspot mode |
|
||||
| `rssi` | number | Wi-Fi RSSI in dBm; `0` in AP mode |
|
||||
| `freeHeap` | number | Free heap in bytes |
|
||||
| `uptime` | number | Seconds since boot |
|
||||
| `device` | string | `"X3"` or `"X4"` hardware detection |
|
||||
|
||||
---
|
||||
## File Management
|
||||
|
||||
### GET `/api/files` - List Files
|
||||
### `GET /api/files`
|
||||
|
||||
Returns a JSON array of files and folders in the specified directory.
|
||||
Lists files and folders under a directory.
|
||||
|
||||
**Request:**
|
||||
```bash
|
||||
# List root directory
|
||||
curl http://crosspoint.local/api/files
|
||||
|
||||
# List specific directory
|
||||
curl "http://crosspoint.local/api/files?path=/Books"
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
Query parameters:
|
||||
|
||||
| Parameter | Required | Default | Description |
|
||||
| --------- | -------- | ------- | ---------------------- |
|
||||
| `path` | No | `/` | Directory path to list |
|
||||
| Parameter | Required | Default | Description |
|
||||
|-----------|----------|---------|-------------|
|
||||
| `path` | No | `/` | Directory to list |
|
||||
|
||||
Response:
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
[
|
||||
{"name": "MyBook.epub", "size": 1234567, "isDirectory": false, "isEpub": true},
|
||||
{"name": "Notes", "size": 0, "isDirectory": true, "isEpub": false},
|
||||
{"name": "document.pdf", "size": 54321, "isDirectory": false, "isEpub": false}
|
||||
{"name":"MyBook.epub","size":1234567,"isDirectory":false,"isEpub":true},
|
||||
{"name":"Notes","size":0,"isDirectory":true,"isEpub":false}
|
||||
]
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
| ------------- | ------- | ---------------------------------------- |
|
||||
| `name` | string | File or folder name |
|
||||
| `size` | number | Size in bytes (0 for directories) |
|
||||
| `isDirectory` | boolean | `true` if the item is a folder |
|
||||
| `isEpub` | boolean | `true` if the file has `.epub` extension |
|
||||
Hidden dotfiles are omitted unless the device setting `showHiddenFiles` is
|
||||
enabled. `System Volume Information` and `XTCache` are always hidden/protected.
|
||||
|
||||
**Notes:**
|
||||
- Hidden files (starting with `.`) are automatically filtered out
|
||||
- System folders (`System Volume Information`, `XTCache`) are hidden
|
||||
### `GET /download`
|
||||
|
||||
---
|
||||
Downloads a file from the SD card.
|
||||
|
||||
### POST `/upload` - Upload File
|
||||
|
||||
Uploads a file to the SD card via multipart form data.
|
||||
|
||||
**Request:**
|
||||
```bash
|
||||
# Upload to root directory
|
||||
curl -X POST -F "file=@mybook.epub" http://crosspoint.local/upload
|
||||
curl -OJ "http://crosspoint.local/download?path=/Books/MyBook.epub"
|
||||
```
|
||||
|
||||
# Upload to specific directory
|
||||
Query parameters:
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|-----------|----------|-------------|
|
||||
| `path` | Yes | File path to download |
|
||||
|
||||
Protected dotfiles, `System Volume Information`, and `XTCache` cannot be
|
||||
downloaded. EPUB files are served as `application/epub+zip`; other files use
|
||||
`application/octet-stream`.
|
||||
|
||||
### `POST /upload`
|
||||
|
||||
Uploads a file with HTTP multipart form data.
|
||||
|
||||
```bash
|
||||
curl -X POST -F "file=@mybook.epub" "http://crosspoint.local/upload?path=/Books"
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
Query parameters:
|
||||
|
||||
| Parameter | Required | Default | Description |
|
||||
| --------- | -------- | ------- | ------------------------------- |
|
||||
| `path` | No | `/` | Target directory for the upload |
|
||||
| Parameter | Required | Default | Description |
|
||||
|-----------|----------|---------|-------------|
|
||||
| `path` | No | `/` | Destination directory |
|
||||
|
||||
**Response (200 OK):**
|
||||
```
|
||||
Successful response:
|
||||
|
||||
```text
|
||||
File uploaded successfully: mybook.epub
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
Notes:
|
||||
|
||||
| Status | Body | Cause |
|
||||
| ------ | ----------------------------------------------- | --------------------------- |
|
||||
| 400 | `Failed to create file on SD card` | Cannot create file |
|
||||
| 400 | `Failed to write to SD card - disk may be full` | Write error during upload |
|
||||
| 400 | `Failed to write final data to SD card` | Error flushing final buffer |
|
||||
| 400 | `Upload aborted` | Client aborted the upload |
|
||||
| 400 | `Unknown error during upload` | Unspecified error |
|
||||
- Existing files with the same name are overwritten.
|
||||
- EPUB cache data for the uploaded path is cleared after a successful upload.
|
||||
- HTTP upload uses a 4 KB write buffer before flushing to the SD card.
|
||||
|
||||
**Notes:**
|
||||
- Existing files with the same name will be overwritten
|
||||
- Uses a 4KB buffer for efficient SD card writes
|
||||
### `POST /mkdir`
|
||||
|
||||
---
|
||||
Creates a folder.
|
||||
|
||||
### POST `/mkdir` - Create Folder
|
||||
|
||||
Creates a new folder on the SD card.
|
||||
|
||||
**Request:**
|
||||
```bash
|
||||
curl -X POST -d "name=NewFolder&path=/" http://crosspoint.local/mkdir
|
||||
```
|
||||
|
||||
**Form Parameters:**
|
||||
Form parameters:
|
||||
|
||||
| Parameter | Required | Default | Description |
|
||||
| --------- | -------- | ------- | ---------------------------- |
|
||||
| `name` | Yes | - | Name of the folder to create |
|
||||
| `path` | No | `/` | Parent directory path |
|
||||
| Parameter | Required | Default | Description |
|
||||
|-----------|----------|---------|-------------|
|
||||
| `name` | Yes | - | New folder name |
|
||||
| `path` | No | `/` | Parent folder |
|
||||
|
||||
**Response (200 OK):**
|
||||
```
|
||||
Folder created: NewFolder
|
||||
```
|
||||
### `POST /rename`
|
||||
|
||||
**Error Responses:**
|
||||
Renames a file.
|
||||
|
||||
| Status | Body | Cause |
|
||||
| ------ | ----------------------------- | ----------------------------- |
|
||||
| 400 | `Missing folder name` | `name` parameter not provided |
|
||||
| 400 | `Folder name cannot be empty` | Empty folder name |
|
||||
| 400 | `Folder already exists` | Folder with same name exists |
|
||||
| 500 | `Failed to create folder` | SD card error |
|
||||
|
||||
---
|
||||
|
||||
### POST `/delete` - Delete File or Folder
|
||||
|
||||
Deletes a file or folder from the SD card.
|
||||
|
||||
**Request:**
|
||||
```bash
|
||||
# Delete a file
|
||||
curl -X POST -d "path=/Books/mybook.epub&type=file" http://crosspoint.local/delete
|
||||
|
||||
# Delete an empty folder
|
||||
curl -X POST -d "path=/OldFolder&type=folder" http://crosspoint.local/delete
|
||||
curl -X POST -d "path=/Books/old.epub&name=new.epub" http://crosspoint.local/rename
|
||||
```
|
||||
|
||||
**Form Parameters:**
|
||||
Form parameters:
|
||||
|
||||
| Parameter | Required | Default | Description |
|
||||
| --------- | -------- | ------- | -------------------------------- |
|
||||
| `path` | Yes | - | Path to the item to delete |
|
||||
| `type` | No | `file` | Type of item: `file` or `folder` |
|
||||
| Parameter | Required | Description |
|
||||
|-----------|----------|-------------|
|
||||
| `path` | Yes | Existing file path |
|
||||
| `name` | Yes | New file name, not a path |
|
||||
|
||||
**Response (200 OK):**
|
||||
```
|
||||
Deleted successfully
|
||||
Only files can be renamed through this endpoint. The old EPUB cache path is
|
||||
cleared before the rename.
|
||||
|
||||
### `POST /move`
|
||||
|
||||
Moves a file into an existing folder.
|
||||
|
||||
```bash
|
||||
curl -X POST -d "path=/Books/mybook.epub&dest=/Read" http://crosspoint.local/move
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
Form parameters:
|
||||
|
||||
| Status | Body | Cause |
|
||||
| ------ | --------------------------------------------- | ----------------------------- |
|
||||
| 400 | `Missing path` | `path` parameter not provided |
|
||||
| 400 | `Cannot delete root directory` | Attempted to delete `/` |
|
||||
| 400 | `Folder is not empty. Delete contents first.` | Non-empty folder |
|
||||
| 403 | `Cannot delete system files` | Hidden file (starts with `.`) |
|
||||
| 403 | `Cannot delete protected items` | Protected system folder |
|
||||
| 404 | `Item not found` | Path does not exist |
|
||||
| 500 | `Failed to delete item` | SD card error |
|
||||
| Parameter | Required | Description |
|
||||
|-----------|----------|-------------|
|
||||
| `path` | Yes | Existing file path |
|
||||
| `dest` | Yes | Existing destination folder |
|
||||
|
||||
**Protected Items:**
|
||||
- Files/folders starting with `.`
|
||||
- `System Volume Information`
|
||||
- `XTCache`
|
||||
Only files can be moved through this endpoint. The old EPUB cache path is
|
||||
cleared before the move.
|
||||
|
||||
---
|
||||
### `POST /delete`
|
||||
|
||||
## WebSocket Endpoint
|
||||
Deletes one or more files or empty folders.
|
||||
|
||||
### Port 81 - Fast Binary Upload
|
||||
|
||||
A WebSocket endpoint for high-speed binary file uploads. More efficient than HTTP multipart for large files.
|
||||
|
||||
**Connection:**
|
||||
```bash
|
||||
curl -X POST -d "path=/Books/mybook.epub" http://crosspoint.local/delete
|
||||
curl -X POST -d 'paths=["/Books/old.epub","/OldFolder"]' http://crosspoint.local/delete
|
||||
```
|
||||
|
||||
Form parameters:
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|-----------|----------|-------------|
|
||||
| `path` | Yes, unless `paths` is provided | Single path to delete |
|
||||
| `paths` | Yes, unless `path` is provided | JSON array of paths to delete |
|
||||
|
||||
Protected items cannot be deleted. Non-empty folders are rejected. EPUB cache
|
||||
data for deleted files is cleared.
|
||||
|
||||
## Settings API
|
||||
|
||||
### `GET /api/settings`
|
||||
|
||||
Returns a streamed JSON array of editable settings. Each item contains common
|
||||
fields plus type-specific fields.
|
||||
|
||||
```bash
|
||||
curl http://crosspoint.local/api/settings
|
||||
```
|
||||
|
||||
Example item:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "fontSize",
|
||||
"name": "Font Size",
|
||||
"category": "Reader",
|
||||
"type": "enum",
|
||||
"value": 1,
|
||||
"options": ["Small", "Medium", "Large"]
|
||||
}
|
||||
```
|
||||
|
||||
Types:
|
||||
|
||||
| Type | Extra fields |
|
||||
|------|--------------|
|
||||
| `toggle` | `value` (`0` or `1`) |
|
||||
| `enum` | `value`, `options` |
|
||||
| `value` | `value`, `min`, `max`, `step` |
|
||||
| `string` | `value` |
|
||||
|
||||
The font-family setting includes SD-card font families when they are installed.
|
||||
|
||||
### `POST /api/settings`
|
||||
|
||||
Applies a partial settings update from a JSON object.
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"fontSize":2,"showHiddenFiles":1}' \
|
||||
http://crosspoint.local/api/settings
|
||||
```
|
||||
|
||||
Successful response:
|
||||
|
||||
```text
|
||||
Applied 2 setting(s)
|
||||
```
|
||||
|
||||
## Font Management API
|
||||
|
||||
### `GET /api/fonts`
|
||||
|
||||
Lists installed SD-card font families.
|
||||
|
||||
```bash
|
||||
curl http://crosspoint.local/api/fonts
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"maxFamilies": 128,
|
||||
"families": [
|
||||
{
|
||||
"name": "Literata",
|
||||
"sizes": [12, 14, 16, 18],
|
||||
"files": [
|
||||
{"name": "Literata_12.cpfont", "size": 123456}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /api/fonts/upload`
|
||||
|
||||
Uploads one `.cpfont` file into a family folder.
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-F "family=Literata" \
|
||||
-F "file=@Literata_12.cpfont" \
|
||||
http://crosspoint.local/api/fonts/upload
|
||||
```
|
||||
|
||||
The handler validates the family name, `.cpfont` filename, and `CPFONT` magic
|
||||
bytes before accepting the file.
|
||||
|
||||
Successful response:
|
||||
|
||||
```json
|
||||
{"ok":true}
|
||||
```
|
||||
|
||||
### `POST /api/fonts/delete`
|
||||
|
||||
Deletes an installed font family.
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"family":"Literata"}' \
|
||||
http://crosspoint.local/api/fonts/delete
|
||||
```
|
||||
|
||||
Successful response:
|
||||
|
||||
```json
|
||||
{"ok":true}
|
||||
```
|
||||
|
||||
## OPDS Server API
|
||||
|
||||
### `GET /api/opds`
|
||||
|
||||
Lists saved OPDS servers. Passwords are never returned.
|
||||
|
||||
```bash
|
||||
curl http://crosspoint.local/api/opds
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"index": 0,
|
||||
"name": "My Catalog",
|
||||
"url": "http://calibre.local:8080/opds",
|
||||
"username": "reader",
|
||||
"hasPassword": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### `POST /api/opds`
|
||||
|
||||
Adds or updates an OPDS server. Include `index` to update an existing entry.
|
||||
If `password` is omitted during an update, the existing password is preserved.
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"My Catalog","url":"http://calibre.local:8080/opds","username":"reader","password":"secret"}' \
|
||||
http://crosspoint.local/api/opds
|
||||
```
|
||||
|
||||
### `POST /api/opds/delete`
|
||||
|
||||
Deletes an OPDS server by index.
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"index":0}' \
|
||||
http://crosspoint.local/api/opds/delete
|
||||
```
|
||||
|
||||
## Wi-Fi Credential API
|
||||
|
||||
### `GET /api/wifi`
|
||||
|
||||
Lists saved Wi-Fi networks. Passwords are never returned.
|
||||
|
||||
```bash
|
||||
curl http://crosspoint.local/api/wifi
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"index": 0,
|
||||
"ssid": "HomeWiFi",
|
||||
"hasPassword": true,
|
||||
"isLastConnected": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### `POST /api/wifi`
|
||||
|
||||
Adds or updates a saved Wi-Fi network. Include `index` to update an existing
|
||||
entry. If `password` is omitted during an update, the existing password is
|
||||
preserved.
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"ssid":"HomeWiFi","password":"secret"}' \
|
||||
http://crosspoint.local/api/wifi
|
||||
```
|
||||
|
||||
### `POST /api/wifi/delete`
|
||||
|
||||
Deletes a saved Wi-Fi network by index.
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"index":0}' \
|
||||
http://crosspoint.local/api/wifi/delete
|
||||
```
|
||||
|
||||
## WebSocket Upload
|
||||
|
||||
### Port 81
|
||||
|
||||
The WebSocket path is used for fast binary uploads from the file manager and
|
||||
Calibre plugin workflows.
|
||||
|
||||
Connection:
|
||||
|
||||
```text
|
||||
ws://crosspoint.local:81/
|
||||
```
|
||||
|
||||
**Protocol:**
|
||||
Protocol:
|
||||
|
||||
1. **Client** sends TEXT message: `START:<filename>:<size>:<path>`
|
||||
2. **Server** responds with TEXT: `READY`
|
||||
3. **Client** sends BINARY messages with file data chunks
|
||||
4. **Server** sends TEXT progress updates: `PROGRESS:<received>:<total>`
|
||||
5. **Server** sends TEXT when complete: `DONE` or `ERROR:<message>`
|
||||
1. Client sends text: `START:<filename>:<size>:<path>`
|
||||
2. Server replies `READY`
|
||||
3. Client sends binary chunks
|
||||
4. Server sends `PROGRESS:<received>:<total>` every 64 KB or at completion
|
||||
5. Server sends `DONE` when complete or `ERROR:<message>` on failure
|
||||
|
||||
**Example Session:**
|
||||
Example session:
|
||||
|
||||
```
|
||||
Client -> "START:mybook.epub:1234567:/Books"
|
||||
Server -> "READY"
|
||||
Client -> [binary chunk 1]
|
||||
Client -> [binary chunk 2]
|
||||
Server -> "PROGRESS:65536:1234567"
|
||||
Client -> [binary chunk 3]
|
||||
```text
|
||||
Client -> START:mybook.epub:1234567:/Books
|
||||
Server -> READY
|
||||
Client -> [binary chunk]
|
||||
Server -> PROGRESS:65536:1234567
|
||||
...
|
||||
Server -> "PROGRESS:1234567:1234567"
|
||||
Server -> "DONE"
|
||||
Server -> DONE
|
||||
```
|
||||
|
||||
**Error Messages:**
|
||||
Error messages include:
|
||||
|
||||
| Message | Cause |
|
||||
| --------------------------------- | ---------------------------------- |
|
||||
| `ERROR:Failed to create file` | Cannot create file on SD card |
|
||||
| `ERROR:Invalid START format` | Malformed START message |
|
||||
| `ERROR:No upload in progress` | Binary data received without START |
|
||||
| `ERROR:Write failed - disk full?` | SD card write error |
|
||||
| Message | Cause |
|
||||
|---------|-------|
|
||||
| `ERROR:Upload already in progress` | A second upload was started before the first completed |
|
||||
| `ERROR:Invalid START format` | Malformed START message or invalid size token |
|
||||
| `ERROR:Failed to create file` | Destination file could not be opened |
|
||||
| `ERROR:No upload in progress` | Binary data arrived without a matching START |
|
||||
| `ERROR:Upload overflow` | Client sent more bytes than declared |
|
||||
| `ERROR:Write failed - disk full?` | SD write failed |
|
||||
|
||||
**Example with `websocat`:**
|
||||
```bash
|
||||
# Interactive session
|
||||
websocat ws://crosspoint.local:81
|
||||
Incomplete WebSocket uploads are deleted on disconnect or error.
|
||||
|
||||
# Then type:
|
||||
START:mybook.epub:1234567:/Books
|
||||
# Wait for READY, then send binary data
|
||||
## WebDAV
|
||||
|
||||
The same HTTP server registers a WebDAV-compatible handler for file manager clients.
|
||||
|
||||
Supported methods:
|
||||
|
||||
```text
|
||||
OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, MKCOL, MOVE, COPY, LOCK, UNLOCK
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Progress updates are sent every 64KB or at completion
|
||||
- Disconnection during upload will delete the incomplete file
|
||||
- Existing files with the same name will be overwritten
|
||||
Notes:
|
||||
|
||||
---
|
||||
- `PUT` writes to a temporary `.davtmp` file first, then renames it into place.
|
||||
- Protected paths are rejected.
|
||||
- `LOCK` and `UNLOCK` are accepted for client compatibility only. The server
|
||||
does not implement full WebDAV Class 2 locking semantics such as persistent
|
||||
locks or lock discovery.
|
||||
|
||||
## UDP Discovery
|
||||
|
||||
The server listens on UDP port `8134`. When it receives the text payload
|
||||
`hello`, it replies to the sender with:
|
||||
|
||||
```text
|
||||
crosspoint (on <hostname>);81
|
||||
```
|
||||
|
||||
The final field is the WebSocket upload port.
|
||||
|
||||
## Network Modes
|
||||
|
||||
The device can operate in two network modes:
|
||||
|
||||
### Station Mode (STA)
|
||||
- Device connects to an existing WiFi network
|
||||
- IP address assigned by router/DHCP
|
||||
- `mode` field in `/api/status` returns `"STA"`
|
||||
- `rssi` field shows signal strength
|
||||
|
||||
- Device joins an existing 2.4 GHz Wi-Fi network.
|
||||
- `crosspoint.local` is advertised with mDNS when available.
|
||||
- `/api/status` returns `"mode": "STA"` and RSSI in dBm.
|
||||
|
||||
### Access Point Mode (AP)
|
||||
- Device creates its own WiFi hotspot
|
||||
- Default IP is typically `192.168.4.1`
|
||||
- `mode` field in `/api/status` returns `"AP"`
|
||||
- `rssi` field returns `0`
|
||||
|
||||
---
|
||||
- Device creates an open hotspot named `CrossPoint-Reader`.
|
||||
- The device shows a Wi-Fi QR code and URL QR code.
|
||||
- The fallback IP is typically `192.168.4.1`.
|
||||
- `/api/status` returns `"mode": "AP"` and `"rssi": 0`.
|
||||
|
||||
## Notes
|
||||
### Calibre Wireless
|
||||
|
||||
- These examples use `crosspoint.local`. If your network does not support mDNS or the address does not resolve, replace it with the specific **IP Address** displayed on your device screen (e.g., `http://192.168.1.102/`).
|
||||
- All paths on the SD card start with `/`
|
||||
- Trailing slashes are automatically stripped (except for root `/`)
|
||||
- The webserver uses chunked transfer encoding for file listings
|
||||
Calibre Wireless starts the same web server in STA mode and displays setup
|
||||
instructions plus WebSocket upload progress on the device screen.
|
||||
|
||||
+103
-185
@@ -1,235 +1,153 @@
|
||||
# Web Server Guide
|
||||
|
||||
This guide explains how to connect your CrossPoint Reader to WiFi and use the built-in web server to upload files from your computer or phone.
|
||||
This guide explains how to use CrossPoint Reader's built-in web server for file
|
||||
transfer, device settings, Wi-Fi/OPDS management, and SD-card font management.
|
||||
|
||||
## Overview
|
||||
|
||||
CrossPoint Reader includes a built-in web server that allows you to:
|
||||
The web server is available while the device is in **File Transfer** or
|
||||
**Calibre Wireless** mode. It can:
|
||||
|
||||
- Upload files wirelessly from any device on the same WiFi network
|
||||
- Browse and manage files on your device's SD card
|
||||
- Create folders to organize your library
|
||||
- Delete files and folders
|
||||
- Upload, download, rename, move, and delete files on the SD card
|
||||
- Create folders
|
||||
- Edit many device settings from a browser
|
||||
- Manage saved Wi-Fi networks and OPDS servers
|
||||
- Upload and delete `.cpfont` SD-card font families
|
||||
- Accept WebDAV clients and Calibre wireless uploads
|
||||
|
||||
## Prerequisites
|
||||
The server does not require authentication. Use it only on trusted private
|
||||
networks or in hotspot mode when you control who is connected.
|
||||
|
||||
- Your CrossPoint Reader device
|
||||
- A WiFi network
|
||||
- A computer, phone, or tablet connected to the **same WiFi network**
|
||||
## Starting File Transfer
|
||||
|
||||
---
|
||||
1. From the Home screen, select **File Transfer**.
|
||||
2. Choose one of the available modes:
|
||||
|
||||
## Step 1: Accessing the WiFi Screen
|
||||
| Mode | Use when |
|
||||
|------|----------|
|
||||
| **Join Network** | You want the reader to join an existing Wi-Fi network. |
|
||||
| **Calibre Wireless** | You want to receive books from the CrossPoint Calibre plugin workflow. |
|
||||
| **Create Hotspot** | You want the reader to create its own open Wi-Fi network. |
|
||||
|
||||
1. From the main menu or file browser, navigate to the **Settings** screen
|
||||
2. Select the **WiFi** option
|
||||
3. The device will automatically start scanning for available networks
|
||||
## Join Network Mode
|
||||
|
||||
---
|
||||
1. Select **Join Network**.
|
||||
2. If you have saved Wi-Fi credentials, CrossPoint first tries the last
|
||||
connected network, then other visible saved networks in signal-strength
|
||||
order. Press **Back** to cancel or **Confirm** to stop auto-connect and show
|
||||
the network list.
|
||||
3. If the network list is shown, pick a 2.4 GHz Wi-Fi network from the scan
|
||||
results.
|
||||
4. Enter the password if prompted.
|
||||
5. Save credentials if you want the reader to reconnect automatically next time.
|
||||
|
||||
## Step 2: Connecting to WiFi
|
||||
After connection, the reader shows:
|
||||
|
||||
### Viewing Available Networks
|
||||
- The connected SSID
|
||||
- A QR code for the web URL
|
||||
- The direct IP URL, for example `http://192.168.1.102/`
|
||||
- The mDNS fallback URL, usually `http://crosspoint.local/`
|
||||
|
||||
Once the scan completes, you'll see a list of available WiFi networks with the following indicators:
|
||||
Use either URL from a phone, tablet, or computer on the same network.
|
||||
|
||||
- **Signal strength bars** (`||||`, `|||`, `||`, `|`) - Shows connection quality
|
||||
- **`*` symbol** - Indicates the network is password-protected (encrypted)
|
||||
- **`+` symbol** - Indicates you have previously saved credentials for this network
|
||||
## Create Hotspot Mode
|
||||
|
||||
<img src="./images/wifi/wifi_networks.jpeg" height="500">
|
||||
1. Select **Create Hotspot**.
|
||||
2. Connect your phone or computer to the open Wi-Fi network:
|
||||
|
||||
### Selecting a Network
|
||||
```text
|
||||
CrossPoint-Reader
|
||||
```
|
||||
|
||||
1. Use the **Left/Right** (or **Volume Up/Down**) buttons to navigate through the network list
|
||||
2. Press **Confirm** to select the highlighted network
|
||||
3. Open the URL shown on the reader. `http://crosspoint.local/` is preferred
|
||||
when supported; the fallback IP is typically `http://192.168.4.1/`.
|
||||
|
||||
### Entering Password (for encrypted networks)
|
||||
The reader displays one QR code for joining the hotspot and another QR code for
|
||||
opening the web interface.
|
||||
|
||||
If the network requires a password:
|
||||
## Calibre Wireless Mode
|
||||
|
||||
1. An on-screen keyboard will appear
|
||||
2. Use the navigation buttons to select characters
|
||||
3. Press **Confirm** to enter each character
|
||||
4. When complete, select the **Done** option on the keyboard
|
||||
Calibre Wireless starts the same web server in station mode, then displays setup
|
||||
instructions and upload progress on the reader. Use this mode with the
|
||||
CrossPoint Calibre plugin or other clients that speak the documented WebSocket
|
||||
upload protocol.
|
||||
|
||||
<img src="./images/wifi/wifi_password.jpeg" height="500">
|
||||
For Calibre OPDS browsing, add `/opds` to the catalog URL when configuring an
|
||||
OPDS server.
|
||||
|
||||
**Note:** If you've previously connected to this network, the saved password will be used automatically.
|
||||
## Web Interface
|
||||
|
||||
### Connection Process
|
||||
The browser UI has four primary pages.
|
||||
|
||||
The device will display "Connecting..." while establishing the connection. This typically takes 5-10 seconds.
|
||||
### Home
|
||||
|
||||
### Saving Credentials
|
||||
|
||||
If this is a new network, you'll be prompted to save the password:
|
||||
|
||||
- Select **Yes** to save credentials for automatic connection next time (NOTE: These are stored in plaintext on the device's SD card. Do not use this for sensitive networks.)
|
||||
- Select **No** to connect without saving
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Connection Success
|
||||
|
||||
Once connected, the screen will display:
|
||||
|
||||
- **Network name** (SSID)
|
||||
- **IP Address** (e.g., `192.168.1.102`)
|
||||
- **Web server URL** (e.g., `http://192.168.1.102/`)
|
||||
|
||||
<img src="./images/wifi/wifi_connected.jpeg" height="500">
|
||||
|
||||
**Important:** Make note of the IP address - you'll need this to access the web interface from your computer or phone.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Accessing the Web Interface
|
||||
|
||||
### From a Computer
|
||||
|
||||
1. Ensure your computer is connected to the **same WiFi network** as your CrossPoint Reader
|
||||
2. Open any web browser (Chrome is recommended)
|
||||
3. Type the IP address shown on your device into the browser's address bar
|
||||
- Example: `http://192.168.1.102/`
|
||||
4. Press Enter
|
||||
|
||||
### From a Phone or Tablet
|
||||
|
||||
1. Ensure your phone/tablet is connected to the **same WiFi network** as your CrossPoint Reader
|
||||
2. Open your mobile browser (Safari, Chrome, etc.)
|
||||
3. Type the IP address into the address bar
|
||||
- Example: `http://192.168.1.102/`
|
||||
4. Tap Go
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Using the Web Interface
|
||||
|
||||
### Home Page
|
||||
|
||||
The home page displays:
|
||||
|
||||
- Device status and version information
|
||||
- WiFi connection status
|
||||
- Current IP address
|
||||
- Available memory
|
||||
|
||||
Navigation links:
|
||||
|
||||
- **Home** - Returns to the status page
|
||||
- **File Manager** - Access file management features
|
||||
|
||||
<img src="./images/wifi/webserver_homepage.png" width="600">
|
||||
The Home page shows firmware status, network mode, IP address, device type,
|
||||
uptime, and free heap.
|
||||
|
||||
### File Manager
|
||||
|
||||
Click **File Manager** to access file management features.
|
||||
The File Manager page can:
|
||||
|
||||
#### Browsing Files
|
||||
- Browse SD-card folders
|
||||
- Upload files, using WebSocket upload when available and HTTP upload as a fallback
|
||||
- Create folders
|
||||
- Download files
|
||||
- Rename files
|
||||
- Move files into existing folders
|
||||
- Delete one or more selected files or empty folders
|
||||
|
||||
- The file manager displays all files and folders on your SD card
|
||||
- **Folders** are highlighted in yellow and indicated with a 📁 icon
|
||||
- **EPUB Files** are highlighted in green and indicated with a 📗 icon
|
||||
- **All Other Files** are not highlighted and indicated with a 📄 icon
|
||||
- Click on a folder name to navigate into it
|
||||
- Use the breadcrumb navigation at the top to go back to parent folders
|
||||
Existing files with the same name are overwritten by uploads. When EPUB files
|
||||
are overwritten, moved, renamed, or deleted through the web server, the matching
|
||||
book cache is cleared so stale metadata is not reused.
|
||||
|
||||
<img src="./images/wifi/webserver_files.png" width="600">
|
||||
### Settings
|
||||
|
||||
#### Uploading Files
|
||||
The Settings page exposes many firmware settings in the browser. It also has
|
||||
cards for:
|
||||
|
||||
1. Click the **📤 Upload** button in the top-right corner
|
||||
2. Click **Choose File** and select a file from your device
|
||||
3. Click **Upload**
|
||||
4. A progress bar will show the upload status
|
||||
5. The page will automatically refresh when the upload is complete
|
||||
- Saved Wi-Fi networks
|
||||
- OPDS servers
|
||||
|
||||
<img src="./images/wifi/webserver_upload.png" width="600">
|
||||
Passwords are accepted when adding or editing entries, but saved passwords are
|
||||
not returned by the API.
|
||||
|
||||
#### Creating Folders
|
||||
### Fonts
|
||||
|
||||
1. Click the **📁 New Folder** button in the top-right corner
|
||||
2. Enter a folder name (must not contain characters \" * : < > ? / \\ | and must not be . or ..)
|
||||
3. Click **Create Folder**
|
||||
The Fonts page lists installed SD-card font families and lets you upload
|
||||
`.cpfont` files. Upload files from one font family at a time. The server validates
|
||||
the font family name, filename, and `.cpfont` magic bytes before accepting the
|
||||
upload.
|
||||
|
||||
This is useful for organizing your library by genre, author, series or file type.
|
||||
Installed fonts appear in **Settings > Reader > Font Family** after the font
|
||||
registry refreshes.
|
||||
|
||||
#### Deleting Files and Folders
|
||||
## Command Line Use
|
||||
|
||||
1. Click the **🗑️** (trash) icon next to any file or folder
|
||||
2. Confirm the deletion in the popup dialog
|
||||
3. Click **Delete** to permanently remove the item
|
||||
Power users can use `curl`, WebDAV clients, or WebSocket clients while the web
|
||||
server is running.
|
||||
|
||||
**Warning:** Deletion is permanent and cannot be undone!
|
||||
|
||||
**Note:** Folders must be empty before they can be deleted.
|
||||
|
||||
#### Moving Files
|
||||
|
||||
1. Click the **📂** (folder) icon next to any file
|
||||
2. Enter a folder name or select one from the dropdown
|
||||
3. Click **Move** to relocate the file
|
||||
|
||||
**Note:** Typing in a nonexistent folder name will result in the following error: "Failed to move: Destination not found"
|
||||
|
||||
#### Renaming Files
|
||||
|
||||
1. Click the **✏️** (pencil) icon next to any file
|
||||
2. Enter a file name (must not contain characters \" * : < > ? / \\ | and must not be . or ..)
|
||||
3. Click **Rename** to permanently rename the file
|
||||
|
||||
---
|
||||
|
||||
## Command Line File Management
|
||||
|
||||
For power users, you can manage files directly from your terminal using `curl` while the device is in File Upload mode. Detailed documentation can be found [here](./webserver-endpoints.md).
|
||||
Endpoint details are documented in [webserver-endpoints.md](./webserver-endpoints.md).
|
||||
|
||||
## Security Notes
|
||||
|
||||
- The web server runs on port 80 (standard HTTP)
|
||||
- **No authentication is required** - anyone on the same network can access the interface
|
||||
- The web server is only accessible while the WiFi screen shows "Connected"
|
||||
- The web server automatically stops when you exit the WiFi screen
|
||||
- For security, only use on trusted private networks
|
||||
- The HTTP server runs on port 80.
|
||||
- The WebSocket upload server runs on port 81.
|
||||
- There is no authentication.
|
||||
- Anyone on the same network can access the web interface while it is running.
|
||||
- The server stops when you exit File Transfer or Calibre Wireless mode.
|
||||
- Hotspot mode creates an open network for connectivity fallback; disconnect when done.
|
||||
|
||||
---
|
||||
## Tips
|
||||
|
||||
## Technical Details
|
||||
|
||||
- **Supported WiFi:** 2.4GHz networks (802.11 b/g/n)
|
||||
- **Web Server Port:** 80 (HTTP)
|
||||
- **Maximum Upload Size:** Limited by available SD card space
|
||||
- **Browser Compatibility:** All modern browsers (Chrome, Firefox, Safari, Edge)
|
||||
|
||||
---
|
||||
|
||||
## Tips and Best Practices
|
||||
|
||||
1. **Organize with folders** - Create folders before uploading to keep your library organized
|
||||
2. **Check signal strength** - Stronger signals (`|||` or `||||`) provide faster, more reliable uploads
|
||||
3. **Upload multiple files** - You can select and upload multiple files at once; the manager will queue them and refresh when the batch is finished
|
||||
4. **Use descriptive names** - Name your folders clearly (e.g., "SciFi", "Mystery", "Non-Fiction")
|
||||
5. **Keep credentials saved** - Save your WiFi password for quick reconnection in the future
|
||||
6. **Exit when done** - Press **Back** to exit the WiFi screen and save battery
|
||||
|
||||
---
|
||||
|
||||
## Exiting WiFi Mode
|
||||
|
||||
When you're finished uploading files:
|
||||
|
||||
1. Press the **Back** button on your CrossPoint Reader
|
||||
2. The web server will automatically stop
|
||||
3. WiFi will disconnect to conserve battery
|
||||
4. You'll return to the previous screen
|
||||
|
||||
Your uploaded files will be immediately available in the file browser!
|
||||
|
||||
---
|
||||
1. Use **Create Hotspot** when no trusted network is available.
|
||||
2. Prefer `crosspoint.local` when available, but keep the displayed IP address as a fallback.
|
||||
3. Move closer to the router if upload progress stalls in Join Network mode.
|
||||
4. Upload custom fonts through the Fonts page or copy them to `/.fonts/` or `/fonts/` on the SD card.
|
||||
5. Exit File Transfer mode when finished to conserve battery.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [User Guide](../USER_GUIDE.md) - General device operation
|
||||
- [Troubleshooting](./troubleshooting.md) - Troubleshooting
|
||||
- [README](../README.md) - Project overview and features
|
||||
- [User Guide](../USER_GUIDE.md)
|
||||
- [Webserver Endpoints](./webserver-endpoints.md)
|
||||
- [SD Card Fonts](./sd-card-fonts.md)
|
||||
- [Troubleshooting](./troubleshooting.md)
|
||||
|
||||
Submodule
+1
Submodule freeink-sdk added at 566fce3d4f
+46
-29
@@ -15,11 +15,11 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t cursorXFP = fp4::fromPixel(startX); // 12.4 fixed-point accumulator
|
||||
int lastBaseX = startX;
|
||||
int lastBaseAdvanceFP = 0; // 12.4 fixed-point
|
||||
int lastBaseLeft = 0;
|
||||
int lastBaseWidth = 0;
|
||||
int lastBaseTop = 0;
|
||||
constexpr int MIN_COMBINING_GAP_PX = 1;
|
||||
int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap
|
||||
uint32_t cp;
|
||||
uint32_t prevCp = 0;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
|
||||
@@ -31,24 +31,30 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
||||
|
||||
const EpdGlyph* glyph = getGlyph(cp);
|
||||
if (!glyph) {
|
||||
prevCp = 0;
|
||||
// Keep cursor movement stable when a base glyph is missing, but don't attach subsequent
|
||||
// combining marks to stale base metrics.
|
||||
if (!isCombining) {
|
||||
lastBaseX += fp4::toPixel(prevAdvanceFP); // flush pending advance before resetting
|
||||
prevCp = 0;
|
||||
prevAdvanceFP = 0;
|
||||
lastBaseLeft = 0;
|
||||
lastBaseWidth = 0;
|
||||
lastBaseTop = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
int raiseBy = 0;
|
||||
if (isCombining) {
|
||||
const int currentGap = glyph->top - glyph->height - lastBaseTop;
|
||||
if (currentGap < MIN_COMBINING_GAP_PX) {
|
||||
raiseBy = MIN_COMBINING_GAP_PX - currentGap;
|
||||
}
|
||||
}
|
||||
const combiningMark::Anchor anchor = combiningMark::anchorFor(cp);
|
||||
const int raiseBy = isCombining ? combiningMark::raiseAboveBase(anchor, glyph->top, glyph->height, lastBaseTop) : 0;
|
||||
|
||||
if (!isCombining && prevCp != 0) {
|
||||
cursorXFP += getKerning(prevCp, cp); // 4.4 fixed-point kern
|
||||
const auto kernFP = getKerning(prevCp, cp); // 4.4 fixed-point kern
|
||||
lastBaseX += fp4::toPixel(prevAdvanceFP + kernFP);
|
||||
}
|
||||
|
||||
const int cursorXPixels = fp4::toPixel(cursorXFP); // snap 12.4 fixed-point to nearest pixel
|
||||
const int glyphBaseX = isCombining ? (lastBaseX + fp4::toPixel(lastBaseAdvanceFP / 2)) : cursorXPixels;
|
||||
const int glyphBaseX = isCombining ? combiningMark::anchorOver(anchor, lastBaseX, lastBaseLeft, lastBaseWidth,
|
||||
glyph->left, glyph->width)
|
||||
: lastBaseX;
|
||||
const int glyphBaseY = startY - raiseBy;
|
||||
|
||||
*minX = std::min(*minX, glyphBaseX + glyph->left);
|
||||
@@ -57,10 +63,10 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
||||
*maxY = std::max(*maxY, glyphBaseY + glyph->top);
|
||||
|
||||
if (!isCombining) {
|
||||
lastBaseX = cursorXPixels;
|
||||
lastBaseAdvanceFP = glyph->advanceX; // 12.4 fixed-point
|
||||
lastBaseLeft = glyph->left;
|
||||
lastBaseWidth = glyph->width;
|
||||
lastBaseTop = glyph->top;
|
||||
cursorXFP += glyph->advanceX; // 12.4 fixed-point advance
|
||||
prevAdvanceFP = glyph->advanceX; // 12.4 fixed-point
|
||||
prevCp = cp;
|
||||
}
|
||||
}
|
||||
@@ -96,6 +102,9 @@ static uint8_t lookupKernClass(const EpdKernClassEntry* entries, const uint16_t
|
||||
}
|
||||
|
||||
int8_t EpdFont::getKerning(const uint32_t leftCp, const uint32_t rightCp) const {
|
||||
if (utf8IsCjkBreakable(leftCp) || utf8IsCjkBreakable(rightCp)) {
|
||||
return 0;
|
||||
}
|
||||
if (!data->kernMatrix) {
|
||||
return 0;
|
||||
}
|
||||
@@ -148,24 +157,32 @@ uint32_t EpdFont::applyLigatures(uint32_t cp, const char*& text) const {
|
||||
|
||||
const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const {
|
||||
const int count = data->intervalCount;
|
||||
if (count == 0) return nullptr;
|
||||
if (count == 0 && !data->glyphMissHandler) return nullptr;
|
||||
|
||||
const EpdUnicodeInterval* intervals = data->intervals;
|
||||
const auto* end = intervals + count;
|
||||
if (count > 0) {
|
||||
const EpdUnicodeInterval* intervals = data->intervals;
|
||||
const auto* end = intervals + count;
|
||||
|
||||
// upper_bound: range lookup. Finds the first interval with first > cp, so the
|
||||
// interval just before it is the last one with first <= cp. That's the only
|
||||
// candidate that could contain cp. Then we verify cp <= candidate.last.
|
||||
const auto it = std::upper_bound(
|
||||
intervals, end, cp, [](uint32_t value, const EpdUnicodeInterval& interval) { return value < interval.first; });
|
||||
// upper_bound: range lookup. Finds the first interval with first > cp, so the
|
||||
// interval just before it is the last one with first <= cp. That's the only
|
||||
// candidate that could contain cp. Then we verify cp <= candidate.last.
|
||||
const auto it = std::upper_bound(
|
||||
intervals, end, cp, [](uint32_t value, const EpdUnicodeInterval& interval) { return value < interval.first; });
|
||||
|
||||
if (it != intervals) {
|
||||
const auto& interval = *(it - 1);
|
||||
if (cp <= interval.last) {
|
||||
return &data->glyph[interval.offset + (cp - interval.first)];
|
||||
if (it != intervals) {
|
||||
const auto& interval = *(it - 1);
|
||||
if (cp <= interval.last) {
|
||||
return &data->glyph[interval.offset + (cp - interval.first)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Codepoint not in interval table — try on-demand loading (SD card fonts).
|
||||
if (data->glyphMissHandler) {
|
||||
const EpdGlyph* loaded = data->glyphMissHandler(data->glyphMissCtx, cp);
|
||||
if (loaded) return loaded;
|
||||
}
|
||||
|
||||
if (cp != REPLACEMENT_GLYPH) {
|
||||
return getGlyph(REPLACEMENT_GLYPH);
|
||||
}
|
||||
|
||||
+116
-6
@@ -7,10 +7,12 @@
|
||||
/// Font metrics use "fixed-point 4" (4 fractional bits, i.e. 1/16-pixel
|
||||
/// resolution). Both the 12.4 glyph advances (uint16_t) and the 4.4 kern
|
||||
/// values (int8_t) share the same 4 fractional bits, so they can be freely
|
||||
/// added into a single int32_t accumulator during text layout. The
|
||||
/// accumulator is snapped to the nearest whole pixel only at render time,
|
||||
/// which avoids the per-character rounding errors that plagued integer-only
|
||||
/// layout.
|
||||
/// added before snapping to whole pixels.
|
||||
///
|
||||
/// Rendering and measurement use "differential rounding": each glyph step
|
||||
/// (previous advance + current kern) is combined in fixed-point and snapped
|
||||
/// to a pixel as one unit. This guarantees identical character pairs always
|
||||
/// produce the same pixel spacing, regardless of position on the line.
|
||||
///
|
||||
/// The helpers below eliminate the raw bit-shifts that would otherwise be
|
||||
/// scattered across every layout / measurement call site.
|
||||
@@ -28,6 +30,98 @@ constexpr int toPixel(int32_t fp) { return static_cast<int>((fp + HALF) >> FRAC_
|
||||
constexpr float toFloat(int32_t fp) { return fp / static_cast<float>(1 << FRAC_BITS); }
|
||||
} // namespace fp4
|
||||
|
||||
/// Helpers for positioning Unicode combining marks (U+0300 ff.) over a
|
||||
/// preceding base glyph without GPOS anchor tables.
|
||||
namespace combiningMark {
|
||||
|
||||
constexpr int MIN_GAP_PX = 1;
|
||||
|
||||
/// Placement of a mark relative to its base glyph. The default heuristic —
|
||||
/// centered over the base, raised clear of its top — suits Latin diacritics
|
||||
/// and Arabic harakat, but misplaces the Hebrew niqqud whose identity depends
|
||||
/// on position: dagesh sits inside the letter body, the shin/sin dots
|
||||
/// distinguish the letter by sitting over its right/left arm, and holam hangs
|
||||
/// over the left corner. "Native" anchors keep the glyph's font-designed
|
||||
/// height (which may overlap the base) instead of raising it.
|
||||
enum class Anchor : uint8_t {
|
||||
CenterRaised, ///< centered over the base, lifted above its top (default)
|
||||
CenterNative, ///< centered over the base at font-native height
|
||||
RightNative, ///< right edges aligned, font-native height
|
||||
LeftNative, ///< left edges aligned, font-native height
|
||||
};
|
||||
|
||||
constexpr Anchor anchorFor(const uint32_t cp) {
|
||||
switch (cp) {
|
||||
case 0x05BC: // dagesh / mapiq / shuruk dot: inside the letter body
|
||||
case 0x05BA: // holam haser for vav: straight above the vav stem
|
||||
return Anchor::CenterNative;
|
||||
case 0x05C1: // shin dot: over the letter's right arm
|
||||
return Anchor::RightNative;
|
||||
case 0x05B9: // holam: above the letter's left corner
|
||||
case 0x05C2: // sin dot: over the letter's left arm
|
||||
return Anchor::LeftNative;
|
||||
default:
|
||||
return Anchor::CenterRaised;
|
||||
}
|
||||
}
|
||||
|
||||
/// Horizontal offset from the base bitmap's left edge to the mark bitmap's
|
||||
/// left edge for a given anchor.
|
||||
constexpr int anchorShift(const Anchor anchor, const int baseWidth, const int markWidth) {
|
||||
switch (anchor) {
|
||||
case Anchor::LeftNative:
|
||||
return 0;
|
||||
case Anchor::RightNative:
|
||||
return baseWidth - markWidth;
|
||||
default:
|
||||
return baseWidth / 2 - markWidth / 2;
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the cursor-X at which to render a combining mark so its bitmap
|
||||
/// lands at its anchor position over the base glyph's bitmap.
|
||||
constexpr int anchorOver(const Anchor anchor, const int baseCursorPos, const int baseLeft, const int baseWidth,
|
||||
const int markLeft, const int markWidth) {
|
||||
return baseCursorPos + baseLeft + anchorShift(anchor, baseWidth, markWidth) - markLeft;
|
||||
}
|
||||
|
||||
/// Rotated-90CW variant of anchorOver. In the rotated coordinate system
|
||||
/// renderCharImpl uses (cursorY - left) instead of (cursorX + left), so
|
||||
/// every left/width term inverts sign.
|
||||
constexpr int anchorOverRotated90CW(const Anchor anchor, const int baseCursorPos, const int baseLeft,
|
||||
const int baseWidth, const int markLeft, const int markWidth) {
|
||||
return baseCursorPos - baseLeft - anchorShift(anchor, baseWidth, markWidth) + markLeft;
|
||||
}
|
||||
|
||||
/// For combining marks that sit entirely above the baseline, compute how many
|
||||
/// pixels to raise the mark so there is at least MIN_GAP_PX between its bottom
|
||||
/// edge and the top of the base glyph. Returns 0 for marks that extend to or
|
||||
/// below the baseline (e.g. cedilla, dot-below, ogonek) and for anchors that
|
||||
/// keep the font-native height (dagesh must stay inside the letter, the
|
||||
/// shin/sin dots touch its arms).
|
||||
constexpr int raiseAboveBase(const Anchor anchor, const int markTop, const int markHeight, const int baseTop) {
|
||||
if (anchor != Anchor::CenterRaised) return 0;
|
||||
if (markTop - markHeight <= 0) return 0;
|
||||
const int gap = markTop - markHeight - baseTop;
|
||||
return (gap < MIN_GAP_PX) ? (MIN_GAP_PX - gap) : 0;
|
||||
}
|
||||
|
||||
} // namespace combiningMark
|
||||
|
||||
/// GCC/Clang (the ESP32 firmware toolchain) pack structs with __attribute__((packed)).
|
||||
/// MSVC (host unit tests) has no equivalent attribute and instead needs a #pragma pack
|
||||
/// region achieving the same 1-byte alignment. These macros keep the on-disk font layout
|
||||
/// identical across both toolchains.
|
||||
#if defined(_MSC_VER)
|
||||
#define EPD_PACKED_BEGIN __pragma(pack(push, 1))
|
||||
#define EPD_PACKED_END __pragma(pack(pop))
|
||||
#define EPD_PACKED_ATTR
|
||||
#else
|
||||
#define EPD_PACKED_BEGIN
|
||||
#define EPD_PACKED_END
|
||||
#define EPD_PACKED_ATTR __attribute__((packed))
|
||||
#endif
|
||||
|
||||
/// Fixed-point conventions used by EpdGlyph and EpdFontData:
|
||||
/// advanceX: 12.4 unsigned fixed-point in uint16_t (use fp4::toPixel)
|
||||
/// kernMatrix: 4.4 signed fixed-point in int8_t (use fp4::toPixel)
|
||||
@@ -62,17 +156,21 @@ typedef struct {
|
||||
|
||||
/// Maps a codepoint to a kerning class ID, sorted by codepoint for binary search.
|
||||
/// Class IDs are 1-based; codepoints not in the table have implicit class 0 (no kerning).
|
||||
EPD_PACKED_BEGIN
|
||||
typedef struct {
|
||||
uint16_t codepoint; ///< Unicode codepoint
|
||||
uint8_t classId; ///< 1-based kerning class ID
|
||||
} __attribute__((packed)) EpdKernClassEntry;
|
||||
} EPD_PACKED_ATTR EpdKernClassEntry;
|
||||
EPD_PACKED_END
|
||||
|
||||
/// Ligature substitution for a specific glyph pair, sorted by `pair` for binary search.
|
||||
/// `pair` encodes (leftCodepoint << 16 | rightCodepoint) for single-key lookup.
|
||||
EPD_PACKED_BEGIN
|
||||
typedef struct {
|
||||
uint32_t pair; ///< Packed codepoint pair (left << 16 | right)
|
||||
uint32_t ligatureCp; ///< Codepoint of the replacement ligature glyph
|
||||
} __attribute__((packed)) EpdLigaturePair;
|
||||
} EPD_PACKED_ATTR EpdLigaturePair;
|
||||
EPD_PACKED_END
|
||||
|
||||
/// Data stored for FONT AS A WHOLE
|
||||
typedef struct {
|
||||
@@ -96,4 +194,16 @@ typedef struct {
|
||||
uint8_t kernRightClassCount; ///< Number of distinct right classes (matrix cols)
|
||||
const EpdLigaturePair* ligaturePairs; ///< Sorted ligature pair table (nullptr if none)
|
||||
uint32_t ligaturePairCount; ///< Number of entries in ligaturePairs
|
||||
|
||||
/// On-demand glyph loading for fonts that don't keep all glyphs in RAM (e.g. SD card fonts).
|
||||
/// Called by getGlyph() when a codepoint is not found in the interval table.
|
||||
/// Returns a valid EpdGlyph* with correct metadata, or nullptr to fall back to the
|
||||
/// replacement glyph. The returned pointer is valid until the next glyphMissHandler
|
||||
/// call that causes a ring-buffer eviction — callers must consume it (measure or draw)
|
||||
/// before requesting another missed glyph.
|
||||
const EpdGlyph* (*glyphMissHandler)(void* ctx, uint32_t codepoint);
|
||||
|
||||
/// Context pointer for glyphMissHandler (typically SdCardFont*). Also used by
|
||||
/// GfxRenderer::getGlyphBitmap() to retrieve overflow bitmaps via SdCardFont.
|
||||
void* glyphMissCtx;
|
||||
} EpdFontData;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "EpdFontFamily.h"
|
||||
|
||||
const EpdFont* EpdFontFamily::getFont(const Style style) const {
|
||||
// Extract font style bits (ignore UNDERLINE bit for font selection)
|
||||
// Extract font style bits; render-time overlay bits do not affect font selection.
|
||||
const bool hasBold = (style & BOLD) != 0;
|
||||
const bool hasItalic = (style & ITALIC) != 0;
|
||||
|
||||
|
||||
@@ -3,7 +3,21 @@
|
||||
|
||||
class EpdFontFamily {
|
||||
public:
|
||||
enum Style : uint8_t { REGULAR = 0, BOLD = 1, ITALIC = 2, BOLD_ITALIC = 3, UNDERLINE = 4 };
|
||||
// Bitmask of text style flags carried per-word through layout and serialized in page cache.
|
||||
// Bits 0-1 select the font variant (BOLD/ITALIC); bits 2-5 are decoration/positioning overlays
|
||||
// applied at render time without changing the underlying font. getFont() ignores all bits
|
||||
// above bit 1 so decorations compose freely with bold/italic (e.g. BOLD | UNDERLINE | SUP).
|
||||
enum Style : uint8_t {
|
||||
REGULAR = 0,
|
||||
BOLD = 1,
|
||||
ITALIC = 2,
|
||||
BOLD_ITALIC = 3,
|
||||
UNDERLINE = 4, // drawn as a line below baseline by TextBlock::render()
|
||||
STRIKETHROUGH = 8, // drawn as a line through midline by TextBlock::render()
|
||||
SUP = 16, // superscript: glyph scaled 50%, raised ~40% of ascender
|
||||
SUB = 32, // subscript: glyph scaled 50%, lowered ~25% of ascender
|
||||
};
|
||||
static constexpr uint8_t TEXT_DECORATION_MASK = static_cast<uint8_t>(UNDERLINE | STRIKETHROUGH);
|
||||
|
||||
explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr,
|
||||
const EpdFont* boldItalic = nullptr)
|
||||
@@ -14,6 +28,9 @@ class EpdFontFamily {
|
||||
const EpdGlyph* getGlyph(uint32_t cp, Style style = REGULAR) const;
|
||||
int8_t getKerning(uint32_t leftCp, uint32_t rightCp, Style style = REGULAR) const;
|
||||
uint32_t applyLigatures(uint32_t cp, const char*& text, Style style = REGULAR) const;
|
||||
static constexpr bool hasTextDecoration(const Style style) {
|
||||
return (static_cast<uint8_t>(style) & TEXT_DECORATION_MASK) != 0;
|
||||
}
|
||||
|
||||
private:
|
||||
const EpdFont* regular;
|
||||
|
||||
@@ -33,12 +33,24 @@ void FontDecompressor::freePageBuffer() {
|
||||
}
|
||||
|
||||
void FontDecompressor::freeHotGroup() {
|
||||
hotGroup.clear();
|
||||
hotGroup.shrink_to_fit();
|
||||
free(hotGroup);
|
||||
hotGroup = nullptr;
|
||||
hotGroupCapacity = 0;
|
||||
hotGroupFont = nullptr;
|
||||
hotGroupIndex = UINT16_MAX;
|
||||
hotGlyphBuf.clear();
|
||||
hotGlyphBuf.shrink_to_fit();
|
||||
free(hotGlyphBuf);
|
||||
hotGlyphBuf = nullptr;
|
||||
hotGlyphBufCapacity = 0;
|
||||
}
|
||||
|
||||
bool FontDecompressor::ensureCapacity(uint8_t*& buf, uint32_t& capacity, uint32_t needed) {
|
||||
if (capacity >= needed) return true;
|
||||
// Grow-only, free-then-malloc: every caller fully rewrites the buffer after a grow, so the
|
||||
// old contents are dead -- freeing first gives the allocator its best shot on a tight heap.
|
||||
free(buf);
|
||||
buf = static_cast<uint8_t*>(malloc(needed)); // owned by FontDecompressor, freed in freeHotGroup()
|
||||
capacity = buf ? needed : 0;
|
||||
return buf != nullptr;
|
||||
}
|
||||
|
||||
uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint32_t glyphIndex) {
|
||||
@@ -170,24 +182,20 @@ const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const Ep
|
||||
}
|
||||
|
||||
// Check if hot group already has this group decompressed — if not, decompress it
|
||||
if (!(!hotGroup.empty() && hotGroupFont == fontData && hotGroupIndex == groupIndex)) {
|
||||
if (!(hotGroup != nullptr && hotGroupFont == fontData && hotGroupIndex == groupIndex)) {
|
||||
stats.cacheMisses++;
|
||||
const EpdFontGroup& group = fontData->groups[groupIndex];
|
||||
|
||||
hotGroup.resize(group.uncompressedSize);
|
||||
if (hotGroup.empty()) {
|
||||
// ensureCapacity may free the buffer, so the cached-group identity dies with it either way.
|
||||
hotGroupFont = nullptr;
|
||||
hotGroupIndex = UINT16_MAX;
|
||||
if (!ensureCapacity(hotGroup, hotGroupCapacity, group.uncompressedSize)) {
|
||||
LOG_ERR("FDC", "Failed to allocate %u bytes for hot group %u", group.uncompressedSize, groupIndex);
|
||||
hotGroupFont = nullptr;
|
||||
hotGroupIndex = UINT16_MAX;
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!decompressGroup(fontData, groupIndex, hotGroup.data(), group.uncompressedSize)) {
|
||||
hotGroup.clear();
|
||||
hotGroup.shrink_to_fit();
|
||||
hotGroupFont = nullptr;
|
||||
hotGroupIndex = UINT16_MAX;
|
||||
if (!decompressGroup(fontData, groupIndex, hotGroup, group.uncompressedSize)) {
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return nullptr;
|
||||
}
|
||||
@@ -200,18 +208,16 @@ const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const Ep
|
||||
}
|
||||
|
||||
// Compact just the requested glyph from byte-aligned data into scratch buffer
|
||||
if (glyph->dataLength > hotGlyphBuf.size()) {
|
||||
hotGlyphBuf.resize(glyph->dataLength);
|
||||
}
|
||||
if (hotGlyphBuf.empty()) {
|
||||
if (!ensureCapacity(hotGlyphBuf, hotGlyphBufCapacity, glyph->dataLength)) {
|
||||
LOG_ERR("FDC", "Failed to allocate %u bytes for glyph scratch", (unsigned)glyph->dataLength);
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint32_t alignedOff = getAlignedOffset(fontData, groupIndex, glyphIndex);
|
||||
compactSingleGlyph(&hotGroup[alignedOff], hotGlyphBuf.data(), glyph->width, glyph->height);
|
||||
compactSingleGlyph(&hotGroup[alignedOff], hotGlyphBuf, glyph->width, glyph->height);
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return hotGlyphBuf.data();
|
||||
return hotGlyphBuf;
|
||||
}
|
||||
|
||||
// --- Prewarm: pre-decompress glyph bitmaps for a page of text ---
|
||||
@@ -284,6 +290,43 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8
|
||||
}
|
||||
}
|
||||
|
||||
// Add ligature output glyphs: if both input codepoints of a ligature pair are
|
||||
// in the needed set, the output glyph will be queried during rendering.
|
||||
if (fontData->ligaturePairs && fontData->ligaturePairCount > 0) {
|
||||
for (uint32_t li = 0; li < fontData->ligaturePairCount && glyphCount < MAX_PAGE_GLYPHS; li++) {
|
||||
uint32_t leftCp = fontData->ligaturePairs[li].pair >> 16;
|
||||
uint32_t rightCp = fontData->ligaturePairs[li].pair & 0xFFFF;
|
||||
|
||||
int32_t leftIdx = findGlyphIndex(fontData, leftCp);
|
||||
int32_t rightIdx = findGlyphIndex(fontData, rightCp);
|
||||
if (leftIdx < 0 || rightIdx < 0) continue;
|
||||
|
||||
// Check if both inputs are in neededGlyphs
|
||||
bool hasLeft = false, hasRight = false;
|
||||
for (uint16_t i = 0; i < glyphCount; i++) {
|
||||
if (neededGlyphs[i] == static_cast<uint32_t>(leftIdx)) hasLeft = true;
|
||||
if (neededGlyphs[i] == static_cast<uint32_t>(rightIdx)) hasRight = true;
|
||||
if (hasLeft && hasRight) break;
|
||||
}
|
||||
if (!hasLeft || !hasRight) continue;
|
||||
|
||||
int32_t outIdx = findGlyphIndex(fontData, fontData->ligaturePairs[li].ligatureCp);
|
||||
if (outIdx < 0) continue;
|
||||
|
||||
// Deduplicate
|
||||
bool found = false;
|
||||
for (uint16_t i = 0; i < glyphCount; i++) {
|
||||
if (neededGlyphs[i] == static_cast<uint32_t>(outIdx)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
neededGlyphs[glyphCount++] = static_cast<uint32_t>(outIdx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (glyphCount == 0) return 0;
|
||||
|
||||
// Step 2: Compute total buffer size and collect unique groups
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
#include <InflateReader.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "EpdFontData.h"
|
||||
|
||||
class FontDecompressor {
|
||||
@@ -67,13 +65,22 @@ class FontDecompressor {
|
||||
|
||||
// Hot group: last decompressed group (byte-aligned) for non-prewarmed fallback path.
|
||||
// Kept in byte-aligned format; individual glyphs are compacted on demand into hotGlyphBuf.
|
||||
// Nothrow high-water malloc buffers, NOT std::vector: getBitmap() runs on the render path,
|
||||
// and under -fno-exceptions a vector resize that hits OOM abort()s the firmware instead of
|
||||
// failing (field crash: hotGroup.resize() -> std::bad_alloc -> abort with ~11 KB free).
|
||||
// ensureCapacity() returns false on OOM so the caller can skip the glyph gracefully.
|
||||
const EpdFontData* hotGroupFont = nullptr;
|
||||
uint16_t hotGroupIndex = UINT16_MAX;
|
||||
std::vector<uint8_t> hotGroup;
|
||||
uint8_t* hotGroup = nullptr; // owned; freed in freeHotGroup()/dtor
|
||||
uint32_t hotGroupCapacity = 0;
|
||||
|
||||
// Scratch buffer for compacting a single glyph from the hot group.
|
||||
// Valid until the next getBitmap() call.
|
||||
std::vector<uint8_t> hotGlyphBuf;
|
||||
// Valid until the next getBitmap() call. Same ownership/OOM contract as hotGroup.
|
||||
uint8_t* hotGlyphBuf = nullptr;
|
||||
uint32_t hotGlyphBufCapacity = 0;
|
||||
|
||||
// Grow (never shrink) an owned buffer to at least `needed` bytes; false on OOM, buffer freed.
|
||||
static bool ensureCapacity(uint8_t*& buf, uint32_t& capacity, uint32_t needed);
|
||||
|
||||
void freePageBuffer();
|
||||
void freeHotGroup();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,268 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "EpdFont.h"
|
||||
#include "EpdFontData.h"
|
||||
|
||||
// On-disk binary format version for .cpfont files. Defined as a preprocessor
|
||||
// macro (rather than a constexpr) so it can be stringified into the SD-fonts
|
||||
// release URL — see FONT_MANIFEST_URL in FontDownloadActivity.h. No integer
|
||||
// suffix because stringification would include it (e.g. `4U` → `"4U"`).
|
||||
//
|
||||
// The canonical version for the build tooling lives in
|
||||
// lib/EpdFont/scripts/cpfont_version.py. This firmware-side copy must be
|
||||
// bumped manually when the firmware is updated to support a new format.
|
||||
// Reader enforcement: SdCardFont::load().
|
||||
#define CPFONT_VERSION 4
|
||||
|
||||
class SdCardFont {
|
||||
public:
|
||||
static constexpr uint16_t MAX_PAGE_GLYPHS = 512;
|
||||
static constexpr uint8_t MAX_STYLES = 4;
|
||||
|
||||
SdCardFont() = default;
|
||||
~SdCardFont();
|
||||
// Owns raw buffers freed in dtor — no shallow-copy semantics. Make any
|
||||
// accidental pass-by-value or move a compile-time error.
|
||||
SdCardFont(const SdCardFont&) = delete;
|
||||
SdCardFont& operator=(const SdCardFont&) = delete;
|
||||
SdCardFont(SdCardFont&&) = delete;
|
||||
SdCardFont& operator=(SdCardFont&&) = delete;
|
||||
|
||||
// Load .cpfont file: reads header + intervals into RAM, records file layout offsets.
|
||||
// Supports v4 (multi-style) format.
|
||||
// Returns true on success.
|
||||
bool load(const char* path);
|
||||
|
||||
// Pre-read glyphs needed for the given UTF-8 text from SD card.
|
||||
// styleMask: bitmask of styles to prewarm (bit 0=regular, 1=bold, 2=italic, 3=bolditalic).
|
||||
// Default 0x0F = all present styles.
|
||||
// When metadataOnly=true, only glyph metrics are loaded (no bitmap data).
|
||||
// Returns number of glyphs that couldn't be loaded (0 on full success).
|
||||
int prewarm(const char* utf8Text, uint8_t styleMask = 0x0F, bool metadataOnly = false);
|
||||
|
||||
// Build a compact advance-only table for layout measurement.
|
||||
// Extracts ALL unique codepoints from words (no MAX_PAGE_GLYPHS cap),
|
||||
// batch-reads advanceX from SD, stores in a sorted per-style table.
|
||||
// extraText: optional additional codepoints to warm in the same SD pass
|
||||
// (e.g. shaped Arabic presentation forms the measurement path will look up).
|
||||
// Returns number of codepoints not found in font coverage.
|
||||
int buildAdvanceTable(const char* utf8Text, uint8_t styleMask = 0x0F, const char* extraText = nullptr);
|
||||
int buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask = 0x0F,
|
||||
const char* extraText = nullptr);
|
||||
|
||||
// Look up advanceX for a codepoint from the advance table.
|
||||
// Returns the 12.4 fixed-point advance, or 0 if not found.
|
||||
uint16_t getAdvance(uint32_t codepoint, uint8_t style) const;
|
||||
|
||||
// Returns true if advance table is populated for at least one style.
|
||||
bool hasAdvanceTable() const;
|
||||
|
||||
// Free mini data for all styles and restore stub EpdFontData.
|
||||
// Preserves the persistent advance cache so repeated layout passes can reuse
|
||||
// previously fetched metrics.
|
||||
void clearCache();
|
||||
|
||||
// Drop the persistent advance cache. Call when unloading the SD font or
|
||||
// when font/size/family/glyph-table state changes.
|
||||
void clearPersistentCache();
|
||||
|
||||
// Returns pointer to the managed EpdFont for a given style.
|
||||
// Returns nullptr if the style is not present.
|
||||
EpdFont* getEpdFont(uint8_t style = 0);
|
||||
|
||||
// Returns true if the given style is present in this font file.
|
||||
bool hasStyle(uint8_t style) const;
|
||||
|
||||
// Resolve requested style bits to the closest present style.
|
||||
uint8_t resolveStyle(uint8_t style) const;
|
||||
|
||||
// Resolve every requested style bit through fallback and return the actual
|
||||
// styles that need cache/advance preparation.
|
||||
uint8_t resolveStyleMask(uint8_t styleMask) const;
|
||||
|
||||
// Number of styles present in this font file.
|
||||
uint8_t styleCount() const { return styleCount_; }
|
||||
|
||||
// Returns true if the glyph pointer points into the overflow buffer.
|
||||
bool isOverflowGlyph(const EpdGlyph* glyph) const;
|
||||
|
||||
// Returns the bitmap for an on-demand-loaded (overflow) glyph.
|
||||
const uint8_t* getOverflowBitmap(const EpdGlyph* glyph) const;
|
||||
|
||||
// Extract SdCardFont* from an opaque glyphMissCtx pointer.
|
||||
// Used by GfxRenderer::getGlyphBitmap() to recover the SdCardFont from EpdFontData::glyphMissCtx.
|
||||
static SdCardFont* fromMissCtx(void* ctx);
|
||||
|
||||
struct Stats {
|
||||
uint32_t prewarmTotalMs = 0;
|
||||
uint32_t sdReadTimeMs = 0;
|
||||
uint32_t seekCount = 0;
|
||||
uint32_t uniqueGlyphs = 0;
|
||||
uint32_t bitmapBytes = 0;
|
||||
};
|
||||
void logStats(const char* label = "SDCF");
|
||||
void resetStats();
|
||||
const Stats& getStats() const { return stats_; }
|
||||
|
||||
// Content hash of the file header + style TOC entries (computed during load).
|
||||
// Used to generate deterministic font IDs for section cache invalidation.
|
||||
uint32_t contentHash() const { return contentHash_; }
|
||||
|
||||
private:
|
||||
// Per-style metadata (parsed from file header/TOC)
|
||||
struct CpFontHeader {
|
||||
uint32_t intervalCount = 0;
|
||||
uint32_t glyphCount = 0;
|
||||
uint8_t advanceY = 0;
|
||||
int16_t ascender = 0;
|
||||
int16_t descender = 0;
|
||||
bool is2Bit = false;
|
||||
uint16_t kernLeftEntryCount = 0;
|
||||
uint16_t kernRightEntryCount = 0;
|
||||
uint8_t kernLeftClassCount = 0;
|
||||
uint8_t kernRightClassCount = 0;
|
||||
uint8_t ligaturePairCount = 0;
|
||||
};
|
||||
|
||||
// All per-style data: file offsets, intervals, kern/lig, prewarm cache, EpdFont
|
||||
struct PerStyle {
|
||||
CpFontHeader header{};
|
||||
|
||||
// File layout offsets for this style's data sections
|
||||
uint32_t intervalsFileOffset = 0;
|
||||
uint32_t glyphsFileOffset = 0;
|
||||
uint32_t kernLeftFileOffset = 0;
|
||||
uint32_t kernRightFileOffset = 0;
|
||||
uint32_t kernMatrixFileOffset = 0;
|
||||
uint32_t ligatureFileOffset = 0;
|
||||
uint32_t bitmapFileOffset = 0;
|
||||
|
||||
// Full intervals loaded from file (kept in RAM for codepoint lookup)
|
||||
EpdUnicodeInterval* fullIntervals = nullptr;
|
||||
EPD_PACKED_BEGIN
|
||||
struct BmpInterval16 {
|
||||
uint16_t first;
|
||||
uint16_t last;
|
||||
uint16_t offset;
|
||||
} EPD_PACKED_ATTR;
|
||||
EPD_PACKED_END
|
||||
static_assert(sizeof(BmpInterval16) == 6, "BmpInterval16 must remain compact");
|
||||
BmpInterval16* bmpIntervals = nullptr;
|
||||
bool intervalsAreBmp16 = false;
|
||||
|
||||
// Persistent kern-class + ligature tables (lazy-loaded on first prewarm).
|
||||
// The full kern MATRIX is NOT resident — on Literata-class fonts a single
|
||||
// style's matrix is ~36-42KB contiguous, and 4 styles' worth won't fit
|
||||
// alongside bitmaps + framebuffer on a 380KB device. Only kernLeftClasses
|
||||
// and kernRightClasses (small codepoint→classId tables, ~3KB each) stay
|
||||
// resident; the matrix is reconstructed per-page as miniKernMatrix.
|
||||
EpdKernClassEntry* kernLeftClasses = nullptr;
|
||||
EpdKernClassEntry* kernRightClasses = nullptr;
|
||||
EpdLigaturePair* ligaturePairs = nullptr;
|
||||
bool kernLigLoaded = false;
|
||||
|
||||
// Stub EpdFontData returned when not prewarmed
|
||||
EpdFontData stubData{};
|
||||
|
||||
// Mini EpdFontData built during prewarm
|
||||
EpdFontData miniData{};
|
||||
EpdUnicodeInterval* miniIntervals = nullptr;
|
||||
EpdGlyph* miniGlyphs = nullptr;
|
||||
uint8_t* miniBitmap = nullptr;
|
||||
uint32_t miniIntervalCount = 0;
|
||||
uint32_t miniGlyphCount = 0;
|
||||
|
||||
// Per-page mini kern matrix (built by buildMiniKernMatrix on each full
|
||||
// prewarm). miniKernLeftClasses/miniKernRightClasses map ONLY the codepoints
|
||||
// used on the current page to renumbered class IDs (1..miniKern*ClassCount).
|
||||
// miniKernMatrix is a small miniKernLeftClassCount × miniKernRightClassCount
|
||||
// flat matrix. Typical Latin page: ~25×25 matrix = ~625 bytes per style vs
|
||||
// ~36KB for the full Literata matrix — ~50× reduction.
|
||||
EpdKernClassEntry* miniKernLeftClasses = nullptr;
|
||||
EpdKernClassEntry* miniKernRightClasses = nullptr;
|
||||
uint16_t miniKernLeftEntryCount = 0;
|
||||
uint16_t miniKernRightEntryCount = 0;
|
||||
uint8_t miniKernLeftClassCount = 0;
|
||||
uint8_t miniKernRightClassCount = 0;
|
||||
int8_t* miniKernMatrix = nullptr;
|
||||
|
||||
// The EpdFont whose data pointer we manage
|
||||
EpdFont epdFont{&stubData};
|
||||
|
||||
bool present = false;
|
||||
};
|
||||
|
||||
PerStyle styles_[MAX_STYLES] = {};
|
||||
uint8_t styleCount_ = 0;
|
||||
|
||||
char filePath_[128] = {};
|
||||
|
||||
// Overflow context: glyphMissHandler needs to know which style it's serving
|
||||
struct OverflowContext {
|
||||
SdCardFont* self;
|
||||
uint8_t styleIdx;
|
||||
};
|
||||
OverflowContext overflowCtx_[MAX_STYLES] = {};
|
||||
|
||||
// Shared on-demand overflow buffer (ring buffer of glyphs loaded via glyphMissHandler)
|
||||
static constexpr uint32_t OVERFLOW_CAPACITY = 8;
|
||||
struct OverflowEntry {
|
||||
EpdGlyph glyph;
|
||||
uint8_t* bitmap = nullptr;
|
||||
uint32_t codepoint = 0;
|
||||
uint8_t styleIdx = 0;
|
||||
};
|
||||
OverflowEntry overflow_[OVERFLOW_CAPACITY] = {};
|
||||
uint32_t overflowCount_ = 0;
|
||||
uint32_t overflowNext_ = 0;
|
||||
|
||||
// Compact advance-only table for layout measurement (per-style).
|
||||
// Built by buildAdvanceTable(), queried by getAdvance().
|
||||
struct AdvanceEntry {
|
||||
uint32_t codepoint;
|
||||
uint16_t advanceX; // 12.4 fixed-point
|
||||
};
|
||||
// Per-style advance table. Sorted by codepoint for binary lookup.
|
||||
// Bounded to ADVANCE_CACHE_LIMIT entries; persists across layout passes
|
||||
// (across calls to clearCache()) so repeated indexing of the same font
|
||||
// amortizes SD reads. Cleared only on font unload or clearPersistentCache().
|
||||
static constexpr uint32_t ADVANCE_CACHE_LIMIT = 768;
|
||||
AdvanceEntry* advanceTable_[MAX_STYLES] = {};
|
||||
uint32_t advanceTableSize_[MAX_STYLES] = {};
|
||||
bool advanceTableLookup(uint8_t styleIdx, uint32_t codepoint, uint16_t* outAdvance) const;
|
||||
// Merge sortedNew (sorted by codepoint, no overlap with existing) into the
|
||||
// advance table for styleIdx, preserving sort order; cap-truncates the tail.
|
||||
void mergeIntoAdvanceTable(uint8_t styleIdx, const AdvanceEntry* sortedNew, uint32_t newCount);
|
||||
|
||||
Stats stats_;
|
||||
uint32_t contentHash_ = 0;
|
||||
bool loaded_ = false;
|
||||
|
||||
// Per-style helpers
|
||||
void freeStyleMiniData(PerStyle& s);
|
||||
void freeStyleAll(PerStyle& s);
|
||||
void freeStyleKernLigatureData(PerStyle& s);
|
||||
void freeStyleMiniKern(PerStyle& s);
|
||||
bool loadStyleKernLigatureData(PerStyle& s);
|
||||
bool buildMiniKernMatrix(PerStyle& s, const uint32_t* codepoints, uint32_t cpCount);
|
||||
void applyKernLigaturePointers(PerStyle& s, EpdFontData& data) const;
|
||||
void applyGlyphMissCallback(uint8_t styleIdx);
|
||||
int32_t findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint) const;
|
||||
int fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCount, uint8_t styleMask);
|
||||
template <typename Iter>
|
||||
int buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask,
|
||||
const char* extraText = nullptr);
|
||||
int prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly);
|
||||
|
||||
// Global helpers
|
||||
void freeAll();
|
||||
void clearOverflow();
|
||||
static void computeStyleFileOffsets(PerStyle& s, uint32_t baseOffset);
|
||||
|
||||
// Static callback for EpdFontData::glyphMissHandler (per-style via OverflowContext)
|
||||
static const EpdGlyph* onGlyphMiss(void* ctx, uint32_t codepoint);
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
#include "SdCardFontManager.h"
|
||||
|
||||
#include <EpdFontFamily.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <Logging.h>
|
||||
#include <SdCardFont.h>
|
||||
#include <SdCardFontRegistry.h>
|
||||
|
||||
SdCardFontManager::~SdCardFontManager() {
|
||||
for (auto& lf : loaded_) {
|
||||
delete lf.font;
|
||||
}
|
||||
}
|
||||
|
||||
// FNV-1a continuation: seeds with contentHash, then hashes family name + point size.
|
||||
// Produces a deterministic ID that is stable across load/unload cycles and reboots,
|
||||
// and changes when font content changes (different header/TOC = different contentHash).
|
||||
int SdCardFontManager::computeFontId(uint32_t contentHash, const char* familyName, uint8_t pointSize) {
|
||||
static constexpr uint32_t FNV_PRIME = 16777619u;
|
||||
uint32_t hash = contentHash;
|
||||
while (*familyName) {
|
||||
hash ^= static_cast<uint8_t>(*familyName++);
|
||||
hash *= FNV_PRIME;
|
||||
}
|
||||
hash ^= pointSize;
|
||||
hash *= FNV_PRIME;
|
||||
int id = static_cast<int>(hash);
|
||||
return id != 0 ? id : 1; // 0 is reserved as "not found" sentinel
|
||||
}
|
||||
|
||||
bool SdCardFontManager::loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t fontSizeEnum) {
|
||||
// Unload any previously loaded family first
|
||||
if (!loadedFamilyName_.empty()) {
|
||||
unloadAll(renderer);
|
||||
}
|
||||
|
||||
// Select the physical point size closest to the built-in reader sizes. Some
|
||||
// CJK font packs only ship larger sizes, so ordinal selection can make
|
||||
// MEDIUM load 18pt+ and produce oversized pages on small devices.
|
||||
const SdCardFontFileInfo* selected = family.findClosestReaderSize(fontSizeEnum);
|
||||
if (!selected) {
|
||||
LOG_ERR("SDMGR", "Family %s has no files to load", family.name.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* font = new (std::nothrow) SdCardFont();
|
||||
if (!font) {
|
||||
LOG_ERR("SDMGR", "Failed to allocate SdCardFont for %s", selected->path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!font->load(selected->path.c_str())) {
|
||||
LOG_ERR("SDMGR", "Failed to load %s", selected->path.c_str());
|
||||
delete font;
|
||||
return false;
|
||||
}
|
||||
|
||||
int fontId = computeFontId(font->contentHash(), family.name.c_str(), selected->pointSize);
|
||||
// Guard against collision with built-in font IDs (astronomically unlikely
|
||||
// with FNV-1a hashes, but provides a safety net)
|
||||
if (renderer.getFontMap().count(fontId) != 0) {
|
||||
LOG_ERR("SDMGR", "Font ID %d collides with existing font, skipping %s", fontId, selected->path.c_str());
|
||||
delete font;
|
||||
return false;
|
||||
}
|
||||
renderer.registerSdCardFont(fontId, font);
|
||||
loaded_.push_back({font, fontId, selected->pointSize});
|
||||
|
||||
LOG_DBG("SDMGR", "Loaded %s size=%u id=%d styles=%u (sizeEnum=%u)", selected->path.c_str(), selected->pointSize,
|
||||
fontId, font->styleCount(), fontSizeEnum);
|
||||
|
||||
EpdFontFamily fontFamily(font->getEpdFont(0), font->getEpdFont(1), font->getEpdFont(2), font->getEpdFont(3));
|
||||
renderer.insertFont(fontId, fontFamily);
|
||||
|
||||
loadedFamilyName_ = family.name;
|
||||
loadedPointSize_ = selected->pointSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
void SdCardFontManager::unloadAll(GfxRenderer& renderer) {
|
||||
renderer.clearSdCardFonts();
|
||||
for (auto& lf : loaded_) {
|
||||
renderer.removeFont(lf.fontId);
|
||||
delete lf.font;
|
||||
}
|
||||
loaded_.clear();
|
||||
loadedFamilyName_.clear();
|
||||
loadedPointSize_ = 0;
|
||||
}
|
||||
|
||||
int SdCardFontManager::getFontId(const std::string& familyName) const {
|
||||
if (familyName != loadedFamilyName_ || loaded_.empty()) return 0;
|
||||
return loaded_.front().fontId;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class GfxRenderer;
|
||||
class SdCardFont;
|
||||
struct SdCardFontFamilyInfo;
|
||||
|
||||
class SdCardFontManager {
|
||||
public:
|
||||
SdCardFontManager() = default;
|
||||
~SdCardFontManager();
|
||||
SdCardFontManager(const SdCardFontManager&) = delete;
|
||||
SdCardFontManager& operator=(const SdCardFontManager&) = delete;
|
||||
|
||||
// Load the font file whose physical point size is closest to the reader
|
||||
// fontSizeEnum (SMALL=12, MEDIUM=14, LARGE=16, EXTRA_LARGE=18). Only one
|
||||
// .cpfont file is loaded; other sizes remain on disk. This keeps resident
|
||||
// interval + kern/ligature tables to one size's worth of memory.
|
||||
// Returns true on success.
|
||||
bool loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t fontSizeEnum);
|
||||
|
||||
// Unload everything, unregister from renderer.
|
||||
void unloadAll(GfxRenderer& renderer);
|
||||
|
||||
// Look up the font ID for the loaded family. Returns 0 if nothing loaded
|
||||
// or familyName doesn't match.
|
||||
int getFontId(const std::string& familyName) const;
|
||||
|
||||
// Get name of currently loaded family (empty if none).
|
||||
const std::string& currentFamilyName() const { return loadedFamilyName_; };
|
||||
|
||||
// Point size that was actually loaded.
|
||||
// 0 if nothing loaded.
|
||||
uint8_t currentPointSize() const { return loadedPointSize_; };
|
||||
|
||||
private:
|
||||
struct LoadedFont {
|
||||
SdCardFont* font; // heap-allocated, owned
|
||||
int fontId;
|
||||
uint8_t size;
|
||||
};
|
||||
static int computeFontId(uint32_t contentHash, const char* familyName, uint8_t pointSize);
|
||||
|
||||
std::string loadedFamilyName_;
|
||||
uint8_t loadedPointSize_ = 0;
|
||||
std::vector<LoadedFont> loaded_;
|
||||
};
|
||||
@@ -0,0 +1,284 @@
|
||||
#include "SdCardFontRegistry.h"
|
||||
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
// --- SdCardFontFamilyInfo helpers ---
|
||||
|
||||
const SdCardFontFileInfo* SdCardFontFamilyInfo::findFile(uint8_t size, uint8_t style) const {
|
||||
for (const auto& f : files) {
|
||||
if (f.pointSize == size && f.style == style) return &f;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const SdCardFontFileInfo* SdCardFontFamilyInfo::findClosestReaderSize(const uint8_t fontSizeEnum,
|
||||
const uint8_t style) const {
|
||||
if (files.empty()) return nullptr;
|
||||
|
||||
// Collect sizes matching the requested style, sorted ascending.
|
||||
std::vector<uint8_t> sizes;
|
||||
for (const auto& f : files) {
|
||||
if (f.style != style) continue;
|
||||
sizes.push_back(f.pointSize);
|
||||
}
|
||||
if (sizes.empty()) return nullptr;
|
||||
std::sort(sizes.begin(), sizes.end());
|
||||
|
||||
// When the family provides at least 4 sizes, use ordinal (index-based)
|
||||
// selection so custom-built font sets (e.g. 10/12/14/16) map SMALL to
|
||||
// the smallest file, not to a hardcoded 12pt target.
|
||||
if (sizes.size() >= 4) {
|
||||
uint8_t idx = fontSizeEnum;
|
||||
if (idx >= sizes.size()) idx = sizes.size() - 1;
|
||||
return findFile(sizes[idx], style);
|
||||
}
|
||||
|
||||
// Fewer sizes than enum slots (e.g. CJK packs with only 2-3 sizes):
|
||||
// fall back to closest-match against the built-in reader targets.
|
||||
uint8_t target = 14;
|
||||
switch (fontSizeEnum) {
|
||||
case 0:
|
||||
target = 12;
|
||||
break;
|
||||
case 2:
|
||||
target = 16;
|
||||
break;
|
||||
case 3:
|
||||
target = 18;
|
||||
break;
|
||||
case 1:
|
||||
default:
|
||||
target = 14;
|
||||
break;
|
||||
}
|
||||
|
||||
const SdCardFontFileInfo* best = nullptr;
|
||||
uint8_t bestDelta = 255;
|
||||
for (const auto& f : files) {
|
||||
if (f.style != style) continue;
|
||||
const uint8_t delta = f.pointSize > target ? f.pointSize - target : target - f.pointSize;
|
||||
if (!best || delta < bestDelta || (delta == bestDelta && f.pointSize < best->pointSize)) {
|
||||
best = &f;
|
||||
bestDelta = delta;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
bool SdCardFontFamilyInfo::hasSize(uint8_t size) const {
|
||||
for (const auto& f : files) {
|
||||
if (f.pointSize == size) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> SdCardFontFamilyInfo::availableSizes() const {
|
||||
std::vector<uint8_t> sizes;
|
||||
for (const auto& f : files) {
|
||||
bool found = false;
|
||||
for (uint8_t s : sizes) {
|
||||
if (s == f.pointSize) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) sizes.push_back(f.pointSize);
|
||||
}
|
||||
std::sort(sizes.begin(), sizes.end());
|
||||
return sizes;
|
||||
}
|
||||
|
||||
// --- SdCardFontRegistry ---
|
||||
|
||||
bool SdCardFontRegistry::parseFilename(const char* filename, uint8_t& size, uint8_t& style) {
|
||||
// V4 naming: <name>_<size>.cpfont (e.g. Bookerly-SD_14.cpfont)
|
||||
// Use an ends-with check rather than strstr() so that in-progress downloads
|
||||
// like "Foo_14.cpfont.tmp" or backups like "Foo_14.cpfont~" aren't accepted.
|
||||
static constexpr char kExt[] = ".cpfont";
|
||||
static constexpr size_t kExtLen = sizeof(kExt) - 1;
|
||||
const size_t nameLen = strlen(filename);
|
||||
if (nameLen <= kExtLen) return false;
|
||||
if (strcmp(filename + nameLen - kExtLen, kExt) != 0) return false;
|
||||
const char* ext = filename + nameLen - kExtLen;
|
||||
|
||||
size_t baseLen = ext - filename;
|
||||
if (baseLen == 0 || baseLen > 127) return false;
|
||||
|
||||
char base[128];
|
||||
memcpy(base, filename, baseLen);
|
||||
base[baseLen] = '\0';
|
||||
|
||||
char* lastUnderscore = strrchr(base, '_');
|
||||
if (!lastUnderscore || lastUnderscore == base) return false;
|
||||
|
||||
const char* sizeStr = lastUnderscore + 1;
|
||||
char* endPtr;
|
||||
long sizeVal = strtol(sizeStr, &endPtr, 10);
|
||||
if (endPtr == sizeStr || *endPtr != '\0' || sizeVal < 1 || sizeVal > 255) return false;
|
||||
size = static_cast<uint8_t>(sizeVal);
|
||||
// V4 .cpfont files bundle every style (regular/bold/italic/bold-italic) into
|
||||
// one file, so style is always 0 at the registry level. The per-style
|
||||
// bitstream is selected later by SdCardFont::getEpdFont(style). The `style`
|
||||
// field in SdCardFontFileInfo is reserved for future formats that split
|
||||
// styles across files; scanDirectory() defends against accidental
|
||||
// (pointSize, style) collisions in that scenario.
|
||||
style = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void SdCardFontRegistry::scanDirectory(const char* dirPath, SdCardFontFamilyInfo& family) {
|
||||
HalFile dir = Storage.open(dirPath);
|
||||
if (!dir || !dir.isDirectory()) return;
|
||||
|
||||
char nameBuffer[128];
|
||||
while (true) {
|
||||
HalFile entry = dir.openNextFile();
|
||||
if (!entry) break;
|
||||
if (entry.isDirectory()) {
|
||||
entry.close();
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.getName(nameBuffer, sizeof(nameBuffer));
|
||||
entry.close();
|
||||
|
||||
// Skip macOS resource fork files (._*) and other hidden files
|
||||
if (nameBuffer[0] == '.' || nameBuffer[0] == '_') continue;
|
||||
|
||||
uint8_t size, style;
|
||||
if (!parseFilename(nameBuffer, size, style)) continue;
|
||||
|
||||
// Reject duplicate (pointSize, style) entries in the same family. With
|
||||
// v4's bundle-everything design parseFilename always returns style=0, so
|
||||
// two files at the same size in the same family would silently shadow
|
||||
// each other in findFile(). Skip the duplicate and warn.
|
||||
bool duplicate = false;
|
||||
for (const auto& existing : family.files) {
|
||||
if (existing.pointSize == size && existing.style == style) {
|
||||
duplicate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (duplicate) {
|
||||
LOG_ERR("SDREG", "Duplicate font %s in %s — skipping", nameBuffer, dirPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
SdCardFontFileInfo info;
|
||||
info.path = std::string(dirPath) + "/" + nameBuffer;
|
||||
info.pointSize = size;
|
||||
info.style = style;
|
||||
family.files.push_back(std::move(info));
|
||||
}
|
||||
}
|
||||
|
||||
// Scan a single root (e.g. "/.fonts") and append its families to `out`.
|
||||
// Skips families whose names already exist in `out` (de-duplicates between
|
||||
// the hidden and visible roots — first scan wins).
|
||||
void SdCardFontRegistry::scanRoot(const char* rootPath, std::vector<SdCardFontFamilyInfo>& out) {
|
||||
HalFile root = Storage.open(rootPath);
|
||||
if (!root) {
|
||||
LOG_DBG("SDREG", "Fonts directory not found: %s", rootPath);
|
||||
return;
|
||||
}
|
||||
if (!root.isDirectory()) {
|
||||
LOG_ERR("SDREG", "Fonts path is not a directory: %s", rootPath);
|
||||
return;
|
||||
}
|
||||
|
||||
char nameBuffer[128];
|
||||
while (true) {
|
||||
HalFile entry = root.openNextFile();
|
||||
if (!entry) break;
|
||||
if (entry.isDirectory()) {
|
||||
entry.getName(nameBuffer, sizeof(nameBuffer));
|
||||
entry.close();
|
||||
|
||||
// Skip hidden/system directories inside the root (macOS ._*, .Trashes, etc.)
|
||||
if (nameBuffer[0] == '.' || nameBuffer[0] == '_') continue;
|
||||
|
||||
// De-dup by family name across roots.
|
||||
bool exists = false;
|
||||
for (const auto& fam : out) {
|
||||
if (fam.name == nameBuffer) {
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (exists) continue;
|
||||
|
||||
SdCardFontFamilyInfo family;
|
||||
family.name = nameBuffer;
|
||||
std::string subDirPath = std::string(rootPath) + "/" + nameBuffer;
|
||||
SdCardFontRegistry::scanDirectory(subDirPath.c_str(), family);
|
||||
|
||||
if (!family.files.empty()) {
|
||||
out.push_back(std::move(family));
|
||||
LOG_DBG("SDREG", "Found family: %s (%d files) in %s", out.back().name.c_str(),
|
||||
static_cast<int>(out.back().files.size()), rootPath);
|
||||
}
|
||||
} else {
|
||||
entry.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool SdCardFontRegistry::discover() {
|
||||
families_.clear();
|
||||
families_.reserve(MAX_SD_FAMILIES);
|
||||
|
||||
// Hidden root is scanned first so it wins on name collisions, matching the
|
||||
// sleep-folder pattern (/.sleep preferred over /sleep).
|
||||
scanRoot(FONTS_DIR_HIDDEN, families_);
|
||||
scanRoot(FONTS_DIR_VISIBLE, families_);
|
||||
|
||||
// Sort families alphabetically
|
||||
std::sort(families_.begin(), families_.end(),
|
||||
[](const SdCardFontFamilyInfo& a, const SdCardFontFamilyInfo& b) { return a.name < b.name; });
|
||||
|
||||
// Cap at MAX_SD_FAMILIES
|
||||
if (static_cast<int>(families_.size()) > MAX_SD_FAMILIES) {
|
||||
families_.resize(MAX_SD_FAMILIES);
|
||||
}
|
||||
|
||||
LOG_DBG("SDREG", "Discovery complete: %d families", static_cast<int>(families_.size()));
|
||||
return !families_.empty();
|
||||
}
|
||||
|
||||
const char* SdCardFontRegistry::findFamilyRoot(const char* familyName) {
|
||||
if (!familyName || !*familyName) return nullptr;
|
||||
char path[160];
|
||||
snprintf(path, sizeof(path), "%s/%s", FONTS_DIR_HIDDEN, familyName);
|
||||
if (Storage.exists(path)) return FONTS_DIR_HIDDEN;
|
||||
snprintf(path, sizeof(path), "%s/%s", FONTS_DIR_VISIBLE, familyName);
|
||||
if (Storage.exists(path)) return FONTS_DIR_VISIBLE;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char* SdCardFontRegistry::defaultWriteRoot() {
|
||||
// If exactly one of the roots already exists, keep using it. Otherwise
|
||||
// (neither exists, or both exist) prefer the hidden root for new installs.
|
||||
bool hiddenExists = Storage.exists(FONTS_DIR_HIDDEN);
|
||||
bool visibleExists = Storage.exists(FONTS_DIR_VISIBLE);
|
||||
if (hiddenExists) return FONTS_DIR_HIDDEN;
|
||||
if (visibleExists) return FONTS_DIR_VISIBLE;
|
||||
return FONTS_DIR_HIDDEN;
|
||||
}
|
||||
|
||||
const SdCardFontFamilyInfo* SdCardFontRegistry::findFamily(const std::string& name) const {
|
||||
for (const auto& f : families_) {
|
||||
if (f.name == name) return &f;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int SdCardFontRegistry::getFamilyIndex(const std::string& name) const {
|
||||
for (int i = 0; i < static_cast<int>(families_.size()); i++) {
|
||||
if (families_[i].name == name) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct SdCardFontFileInfo {
|
||||
std::string path; // v4 on-disk naming: "/<root>/<Family>/<Family>_<size>.cpfont"
|
||||
// where <root> is "/.fonts" (preferred, hidden) or "/fonts" (visible).
|
||||
// e.g. "/.fonts/NotoSansCJK/NotoSansCJK_14.cpfont"
|
||||
uint8_t pointSize; // parsed from filename: 14
|
||||
uint8_t style; // always 0 in v4 (all 4 styles bundled in one file);
|
||||
// kept for potential future formats
|
||||
};
|
||||
|
||||
struct SdCardFontFamilyInfo {
|
||||
std::string name; // directory name, e.g. "NotoSansCJK"
|
||||
std::vector<SdCardFontFileInfo> files;
|
||||
|
||||
const SdCardFontFileInfo* findFile(uint8_t size, uint8_t style = 0) const;
|
||||
const SdCardFontFileInfo* findClosestReaderSize(uint8_t fontSizeEnum, uint8_t style = 0) const;
|
||||
bool hasSize(uint8_t size) const;
|
||||
std::vector<uint8_t> availableSizes() const;
|
||||
};
|
||||
|
||||
class SdCardFontRegistry {
|
||||
public:
|
||||
static constexpr int MAX_SD_FAMILIES = 128;
|
||||
// Two top-level roots are scanned at discovery time. Hidden is preferred
|
||||
// when creating new installs; both are read from if present.
|
||||
static constexpr const char* FONTS_DIR_HIDDEN = "/.fonts";
|
||||
static constexpr const char* FONTS_DIR_VISIBLE = "/fonts";
|
||||
|
||||
// Returns the existing root for `familyName` (the one that contains
|
||||
// /<root>/<familyName>/), or nullptr if the family is not installed in
|
||||
// either root. Used by writers to keep re-installs in their existing dir.
|
||||
static const char* findFamilyRoot(const char* familyName);
|
||||
|
||||
// Returns the root path that should be used when creating a brand-new
|
||||
// family on disk (no prior install): the existing root if exactly one of
|
||||
// the two roots exists, otherwise the hidden root.
|
||||
static const char* defaultWriteRoot();
|
||||
|
||||
// Scan SD card, populate families_. Returns true if any families found.
|
||||
bool discover();
|
||||
|
||||
const std::vector<SdCardFontFamilyInfo>& getFamilies() const { return families_; }
|
||||
const SdCardFontFamilyInfo* findFamily(const std::string& name) const;
|
||||
int getFamilyIndex(const std::string& name) const;
|
||||
int getFamilyCount() const { return static_cast<int>(families_.size()); }
|
||||
|
||||
private:
|
||||
std::vector<SdCardFontFamilyInfo> families_; // sorted alphabetically
|
||||
|
||||
static bool parseFilename(const char* filename, uint8_t& size, uint8_t& style);
|
||||
static void scanDirectory(const char* dirPath, SdCardFontFamilyInfo& family);
|
||||
// Scan one root (e.g. "/.fonts"), append families to `out`, dedup by name.
|
||||
static void scanRoot(const char* rootPath, std::vector<SdCardFontFamilyInfo>& out);
|
||||
};
|
||||
@@ -1,21 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <builtinFonts/bookerly_12_bold.h>
|
||||
#include <builtinFonts/bookerly_12_bolditalic.h>
|
||||
#include <builtinFonts/bookerly_12_italic.h>
|
||||
#include <builtinFonts/bookerly_12_regular.h>
|
||||
#include <builtinFonts/bookerly_14_bold.h>
|
||||
#include <builtinFonts/bookerly_14_bolditalic.h>
|
||||
#include <builtinFonts/bookerly_14_italic.h>
|
||||
#include <builtinFonts/bookerly_14_regular.h>
|
||||
#include <builtinFonts/bookerly_16_bold.h>
|
||||
#include <builtinFonts/bookerly_16_bolditalic.h>
|
||||
#include <builtinFonts/bookerly_16_italic.h>
|
||||
#include <builtinFonts/bookerly_16_regular.h>
|
||||
#include <builtinFonts/bookerly_18_bold.h>
|
||||
#include <builtinFonts/bookerly_18_bolditalic.h>
|
||||
#include <builtinFonts/bookerly_18_italic.h>
|
||||
#include <builtinFonts/bookerly_18_regular.h>
|
||||
#include <builtinFonts/notoserif_12_bold.h>
|
||||
#include <builtinFonts/notoserif_12_bolditalic.h>
|
||||
#include <builtinFonts/notoserif_12_italic.h>
|
||||
#include <builtinFonts/notoserif_12_regular.h>
|
||||
#include <builtinFonts/notoserif_14_bold.h>
|
||||
#include <builtinFonts/notoserif_14_bolditalic.h>
|
||||
#include <builtinFonts/notoserif_14_italic.h>
|
||||
#include <builtinFonts/notoserif_14_regular.h>
|
||||
#include <builtinFonts/notoserif_16_bold.h>
|
||||
#include <builtinFonts/notoserif_16_bolditalic.h>
|
||||
#include <builtinFonts/notoserif_16_italic.h>
|
||||
#include <builtinFonts/notoserif_16_regular.h>
|
||||
#include <builtinFonts/notoserif_18_bold.h>
|
||||
#include <builtinFonts/notoserif_18_bolditalic.h>
|
||||
#include <builtinFonts/notoserif_18_italic.h>
|
||||
#include <builtinFonts/notoserif_18_regular.h>
|
||||
#include <builtinFonts/notosans_8_regular.h>
|
||||
#include <builtinFonts/notosans_12_bold.h>
|
||||
#include <builtinFonts/notosans_12_bolditalic.h>
|
||||
@@ -33,22 +33,6 @@
|
||||
#include <builtinFonts/notosans_18_bolditalic.h>
|
||||
#include <builtinFonts/notosans_18_italic.h>
|
||||
#include <builtinFonts/notosans_18_regular.h>
|
||||
#include <builtinFonts/opendyslexic_10_bold.h>
|
||||
#include <builtinFonts/opendyslexic_10_bolditalic.h>
|
||||
#include <builtinFonts/opendyslexic_10_italic.h>
|
||||
#include <builtinFonts/opendyslexic_10_regular.h>
|
||||
#include <builtinFonts/opendyslexic_12_bold.h>
|
||||
#include <builtinFonts/opendyslexic_12_bolditalic.h>
|
||||
#include <builtinFonts/opendyslexic_12_italic.h>
|
||||
#include <builtinFonts/opendyslexic_12_regular.h>
|
||||
#include <builtinFonts/opendyslexic_14_bold.h>
|
||||
#include <builtinFonts/opendyslexic_14_bolditalic.h>
|
||||
#include <builtinFonts/opendyslexic_14_italic.h>
|
||||
#include <builtinFonts/opendyslexic_14_regular.h>
|
||||
#include <builtinFonts/opendyslexic_8_bold.h>
|
||||
#include <builtinFonts/opendyslexic_8_bolditalic.h>
|
||||
#include <builtinFonts/opendyslexic_8_italic.h>
|
||||
#include <builtinFonts/opendyslexic_8_regular.h>
|
||||
#include <builtinFonts/ubuntu_10_bold.h>
|
||||
#include <builtinFonts/ubuntu_10_regular.h>
|
||||
#include <builtinFonts/ubuntu_12_bold.h>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1756
-1755
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2121
-2121
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2466
-2466
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,239 +3,239 @@
|
||||
* name: notosans_16_regular
|
||||
* size: 16
|
||||
* mode: 2-bit compressed: true
|
||||
* Command used: fontconvert.py notosans_16_regular 16 ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf --2bit --compress
|
||||
* Command used: fontconvert.py notosans_16_regular 16 ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf --2bit --compress --pnum
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
static const uint8_t notosans_16_regularBitmaps[35113] = {
|
||||
0xBD, 0x5A, 0x3D, 0x8C, 0xE4, 0xC8, 0x75, 0x2E, 0x92, 0xBB, 0x4B, 0x40, 0x84, 0xC1, 0x0B, 0xB4,
|
||||
0xB2, 0xA0, 0x0D, 0x98, 0x5C, 0x24, 0xC1, 0xC7, 0x54, 0xC1, 0x0E, 0x8B, 0x82, 0xA1, 0x7C, 0x83,
|
||||
0x9D, 0x5B, 0xC0, 0x1B, 0x9C, 0x53, 0x1D, 0x20, 0x4C, 0xA2, 0xC0, 0xC1, 0xCC, 0x90, 0xF6, 0x05,
|
||||
0x0A, 0x14, 0x68, 0x01, 0xE1, 0x0C, 0x03, 0x97, 0x28, 0x38, 0x19, 0x72, 0x6F, 0xB0, 0x89, 0x03,
|
||||
0x09, 0xE8, 0x69, 0x12, 0x36, 0xE0, 0xC4, 0xC1, 0xD1, 0x06, 0x04, 0x87, 0xA2, 0xE0, 0xDB, 0x40,
|
||||
0xE7, 0xE0, 0x08, 0x9B, 0x07, 0xB3, 0x57, 0x45, 0x96, 0xBF, 0xEF, 0x15, 0xD9, 0xDD, 0xB3, 0x3B,
|
||||
0xFB, 0xAB, 0x85, 0x39, 0xAC, 0xC7, 0x6E, 0x76, 0xFD, 0xBC, 0x7A, 0xFF, 0xEF, 0xD5, 0x68, 0xA5,
|
||||
0xAF, 0xF8, 0xCB, 0xD4, 0x7B, 0xF3, 0xDF, 0x9F, 0xA9, 0x6B, 0x8A, 0x97, 0x56, 0x55, 0x5E, 0x95,
|
||||
0x05, 0xBE, 0xAD, 0xA3, 0x66, 0xBE, 0xC3, 0x1A, 0xF7, 0x29, 0xEE, 0x80, 0xF7, 0x77, 0x83, 0x52,
|
||||
0xA9, 0x7F, 0xF4, 0x5A, 0x74, 0xEE, 0x03, 0xC2, 0x2E, 0x68, 0x94, 0xF2, 0xBA, 0xA0, 0x56, 0xCA,
|
||||
0xEF, 0x42, 0xC2, 0x36, 0x02, 0x0C, 0x9A, 0xA8, 0x54, 0x5F, 0x7B, 0xF8, 0xE4, 0xD1, 0x17, 0x79,
|
||||
0x6C, 0x71, 0x95, 0xD7, 0xEF, 0xFF, 0xE6, 0xE4, 0x38, 0x57, 0x51, 0x99, 0x62, 0x50, 0x9C, 0x67,
|
||||
0x02, 0x8F, 0x00, 0x13, 0x75, 0x1B, 0x30, 0x55, 0xA7, 0x58, 0x1E, 0x1D, 0xA7, 0x19, 0xAA, 0x23,
|
||||
0xD5, 0xE3, 0xFD, 0x6D, 0xD5, 0x01, 0x9E, 0x7A, 0x84, 0x6B, 0x9F, 0x70, 0xF0, 0xDD, 0xE2, 0xCD,
|
||||
0xB2, 0xB8, 0xF2, 0xD0, 0xB0, 0x9A, 0xB4, 0x5B, 0x0F, 0x54, 0x68, 0x6D, 0x93, 0x58, 0xDB, 0x15,
|
||||
0x9F, 0xFC, 0x6D, 0x77, 0x81, 0x77, 0x66, 0x6E, 0x23, 0x5A, 0xF1, 0x93, 0x52, 0xA5, 0xB6, 0x44,
|
||||
0x9F, 0x41, 0xA9, 0x02, 0x1F, 0x22, 0xDB, 0xA9, 0xE8, 0xC7, 0x03, 0xC6, 0x1A, 0xB4, 0x35, 0x9F,
|
||||
0x79, 0xF4, 0xD7, 0x66, 0xBA, 0xFF, 0xA8, 0x07, 0x1A, 0x4D, 0x41, 0x4C, 0x6E, 0x95, 0xFB, 0xF9,
|
||||
0xD9, 0xDC, 0xE5, 0x4F, 0xDC, 0x81, 0x8A, 0x6D, 0x0D, 0xE4, 0x55, 0xFA, 0xE7, 0x2D, 0x30, 0x56,
|
||||
0x19, 0x10, 0x5D, 0x13, 0xF7, 0x8E, 0xD8, 0x9F, 0x02, 0xCD, 0x4E, 0x1E, 0x24, 0x0C, 0x1E, 0x71,
|
||||
0x29, 0x8F, 0x44, 0x36, 0xD6, 0x67, 0x37, 0x1E, 0xA8, 0x23, 0xAF, 0x5B, 0x6B, 0xA3, 0xDE, 0xF3,
|
||||
0xBB, 0x7E, 0x53, 0xA9, 0xE4, 0x93, 0x7F, 0xEF, 0xFA, 0x38, 0x8F, 0xEC, 0x47, 0xFF, 0xD6, 0xC5,
|
||||
0xA5, 0xF7, 0xEB, 0xF8, 0x9F, 0x3A, 0x12, 0x33, 0xF1, 0x5B, 0x4E, 0x90, 0x05, 0xF2, 0x38, 0x75,
|
||||
0x8F, 0x41, 0x5E, 0xFA, 0x9D, 0xCF, 0x2E, 0x41, 0xE3, 0x61, 0x80, 0xC2, 0xA7, 0x21, 0xC9, 0x89,
|
||||
0x56, 0x31, 0x91, 0x9B, 0x2A, 0x19, 0x04, 0x57, 0x2F, 0x5F, 0xB0, 0x4E, 0x2C, 0x3F, 0x56, 0x16,
|
||||
0x34, 0xF4, 0xA6, 0xFB, 0xC0, 0x31, 0xE8, 0x3D, 0xF4, 0x01, 0x9A, 0x66, 0x0F, 0xF9, 0xC6, 0x1B,
|
||||
0x02, 0xD2, 0x7F, 0x4C, 0x48, 0x6D, 0x5D, 0x71, 0xDB, 0xC9, 0xC8, 0x19, 0x0A, 0xBE, 0xF6, 0xED,
|
||||
0xA8, 0x82, 0x36, 0xEA, 0x8B, 0x32, 0x6C, 0x92, 0x3A, 0x69, 0xE3, 0x5A, 0xE7, 0xE1, 0x00, 0x88,
|
||||
0x59, 0x75, 0xAE, 0x49, 0x5B, 0x59, 0xDE, 0x10, 0x46, 0x46, 0xA5, 0xB5, 0xD2, 0x93, 0x8A, 0xB7,
|
||||
0xF7, 0x2C, 0x46, 0x58, 0x3B, 0xC4, 0x2D, 0x70, 0x28, 0x03, 0xA3, 0xBC, 0x3B, 0x64, 0x2C, 0xAF,
|
||||
0xD3, 0xD3, 0xD3, 0xEF, 0x42, 0x24, 0xD7, 0x58, 0xB8, 0x0B, 0x9B, 0xB8, 0x4E, 0xCA, 0x34, 0xA7,
|
||||
0x94, 0x9E, 0xE3, 0xEF, 0xE2, 0xE0, 0x8F, 0xDF, 0xF9, 0x3E, 0x2D, 0xE3, 0x3A, 0x6E, 0xC2, 0xD6,
|
||||
0x27, 0xD2, 0x67, 0xEA, 0xDA, 0x1A, 0xEF, 0x92, 0x3C, 0xAE, 0xC3, 0x36, 0xE8, 0x7C, 0x6E, 0xCA,
|
||||
0xE0, 0x6F, 0x19, 0xB0, 0xFC, 0x5D, 0xA8, 0x91, 0xEF, 0x7D, 0xAE, 0xD1, 0x45, 0x4D, 0x5C, 0x6A,
|
||||
0x0C, 0x7D, 0xC7, 0x11, 0x28, 0x6C, 0x5D, 0xA3, 0x4C, 0xA1, 0xBD, 0x1B, 0xD4, 0xD7, 0x8B, 0x9F,
|
||||
0xFD, 0xCB, 0x54, 0x41, 0x0E, 0xBE, 0x55, 0x4D, 0xBF, 0x50, 0x31, 0x36, 0xAE, 0xC1, 0xDC, 0x0B,
|
||||
0x6C, 0xD2, 0xA4, 0x65, 0xD0, 0xC7, 0x8D, 0xD0, 0xFE, 0x1D, 0x8F, 0x42, 0xAF, 0x5E, 0x02, 0x0A,
|
||||
0x4C, 0x93, 0x0B, 0xB8, 0x79, 0xEF, 0xAB, 0xE3, 0x97, 0x8D, 0x50, 0xC0, 0x27, 0x26, 0x11, 0x6A,
|
||||
0x12, 0x21, 0x83, 0x40, 0x9D, 0x12, 0x4D, 0x72, 0x2E, 0xC7, 0x7D, 0x7C, 0x7C, 0x49, 0x59, 0x15,
|
||||
0x54, 0x58, 0x61, 0x77, 0x8A, 0xB2, 0x47, 0x16, 0x7A, 0xBD, 0x30, 0x53, 0x51, 0x43, 0x23, 0x6C,
|
||||
0x27, 0xA6, 0x10, 0x95, 0xD4, 0x32, 0x91, 0x0A, 0xEC, 0x59, 0xBA, 0x29, 0xF0, 0xD9, 0x9F, 0xBB,
|
||||
0x86, 0x2F, 0xE8, 0xEA, 0xAE, 0x14, 0x22, 0xE5, 0x59, 0x88, 0x7B, 0x30, 0x7E, 0xD0, 0xA9, 0xB8,
|
||||
0xF5, 0x07, 0x76, 0x06, 0x6F, 0x73, 0xE1, 0x88, 0x30, 0xC5, 0x81, 0x94, 0xB8, 0xA4, 0xF9, 0x73,
|
||||
0x41, 0x46, 0xB0, 0x1F, 0x21, 0x13, 0x40, 0x7A, 0x2F, 0xB0, 0x65, 0xCF, 0xA8, 0x60, 0x7B, 0xAB,
|
||||
0xE7, 0x42, 0x40, 0x26, 0xA5, 0xB4, 0x8A, 0x88, 0x80, 0x2A, 0x49, 0xA7, 0xAA, 0x2E, 0xB0, 0x5D,
|
||||
0xFC, 0x9F, 0x9D, 0xFE, 0xA8, 0xFB, 0x13, 0x20, 0xFD, 0x06, 0xB7, 0x5C, 0x54, 0x8E, 0x88, 0x0B,
|
||||
0xE8, 0xED, 0x49, 0x0F, 0xA1, 0xF1, 0x0D, 0x89, 0x48, 0x82, 0x00, 0x87, 0x4B, 0x80, 0xEF, 0xA8,
|
||||
0x0D, 0x6E, 0x68, 0x44, 0xD6, 0x40, 0x94, 0x31, 0x03, 0xDF, 0xF3, 0xC7, 0x90, 0xEF, 0x93, 0xE7,
|
||||
0xBD, 0xFA, 0x62, 0xB5, 0x3A, 0x60, 0xBC, 0x53, 0xC5, 0x6A, 0xA2, 0x16, 0x42, 0x01, 0xF5, 0x63,
|
||||
0x2C, 0xFE, 0x9E, 0xF2, 0x65, 0xDD, 0xF1, 0x8A, 0x75, 0x09, 0x22, 0x4C, 0xF6, 0x61, 0x85, 0xC9,
|
||||
0x2C, 0x7B, 0x70, 0x18, 0xEC, 0x91, 0x8C, 0x98, 0xD4, 0xBC, 0xA8, 0xB0, 0xE9, 0x00, 0x14, 0xEA,
|
||||
0x8E, 0xF2, 0x46, 0xB5, 0xC9, 0xD1, 0xAF, 0xE2, 0x88, 0xC4, 0x4E, 0x58, 0xFB, 0xA6, 0xAC, 0x2F,
|
||||
0x93, 0x06, 0x04, 0x31, 0x41, 0x4A, 0x70, 0x44, 0x60, 0x0C, 0x0D, 0xB8, 0xA1, 0x20, 0x18, 0x4A,
|
||||
0x81, 0x21, 0x1B, 0x0D, 0xF9, 0x6F, 0x28, 0x27, 0xC6, 0x19, 0x87, 0xB0, 0x01, 0x00, 0xAB, 0x44,
|
||||
0xAF, 0x8D, 0xEC, 0xAD, 0x17, 0xF0, 0xED, 0xD5, 0x6A, 0xFA, 0xF9, 0x8C, 0xF8, 0xB3, 0x00, 0xFB,
|
||||
0x6D, 0x52, 0xA0, 0x92, 0xF2, 0x39, 0xE3, 0xB9, 0xB4, 0x6C, 0x69, 0x1F, 0x3E, 0x50, 0x85, 0x33,
|
||||
0xFF, 0xCD, 0x35, 0x32, 0x5B, 0x05, 0xC3, 0x8C, 0xEF, 0x53, 0xCD, 0x1F, 0x72, 0x10, 0x79, 0xFB,
|
||||
0xBD, 0xA2, 0x21, 0x55, 0x53, 0x9A, 0xF2, 0x9B, 0x8B, 0x90, 0x7A, 0xFC, 0x96, 0x10, 0x54, 0x8F,
|
||||
0xEF, 0x2A, 0x7F, 0xDC, 0xB1, 0x8E, 0x8E, 0x82, 0xB2, 0x0D, 0x76, 0x2B, 0x27, 0xDD, 0x4A, 0xDF,
|
||||
0x00, 0x5D, 0x74, 0x0A, 0x0F, 0xA0, 0x37, 0xF7, 0xB1, 0xA3, 0x16, 0x34, 0xD5, 0xE5, 0x2C, 0xC2,
|
||||
0x94, 0xCF, 0xE4, 0x00, 0x40, 0x48, 0x53, 0x58, 0x17, 0xFC, 0x06, 0x29, 0xD8, 0x40, 0x07, 0x42,
|
||||
0x43, 0x7E, 0x74, 0x4A, 0xFC, 0x89, 0x02, 0xCD, 0x1D, 0xA7, 0xAB, 0x43, 0x76, 0x0B, 0x3F, 0xCE,
|
||||
0x9F, 0x15, 0x24, 0x2A, 0x1B, 0x84, 0x7D, 0x41, 0x29, 0xDD, 0xF5, 0xDD, 0x31, 0xDF, 0x7F, 0xAA,
|
||||
0x6F, 0xBD, 0xC8, 0x94, 0xCE, 0x17, 0xB6, 0x8F, 0x3B, 0x02, 0xCB, 0x75, 0x82, 0xCD, 0xF8, 0x64,
|
||||
0x77, 0xB4, 0xBD, 0x2F, 0x9A, 0x39, 0x72, 0xAB, 0xE7, 0x4E, 0xBB, 0x04, 0x24, 0xA2, 0x62, 0x2D,
|
||||
0xD0, 0x08, 0x46, 0xCA, 0x95, 0x25, 0xE2, 0x7A, 0x12, 0xB9, 0xAA, 0x55, 0x38, 0x24, 0xD0, 0x85,
|
||||
0x1A, 0x92, 0x43, 0xF3, 0x4D, 0x96, 0x1C, 0xAA, 0xB1, 0xD3, 0x56, 0xF8, 0x15, 0x95, 0xB4, 0x37,
|
||||
0x46, 0xFA, 0xD8, 0x9E, 0xC6, 0xA8, 0x9C, 0xD5, 0xD3, 0x61, 0x30, 0xD2, 0x31, 0x60, 0xAA, 0x68,
|
||||
0x7B, 0xD2, 0x61, 0x2A, 0xEC, 0x01, 0x23, 0x44, 0x54, 0x32, 0xCE, 0xA7, 0x65, 0x3E, 0x07, 0xF8,
|
||||
0x55, 0xFC, 0x44, 0x9E, 0x96, 0x9E, 0xCD, 0xE3, 0xFE, 0x96, 0x55, 0x81, 0xB5, 0xE0, 0x4B, 0x35,
|
||||
0xEC, 0x45, 0x39, 0xBB, 0x4C, 0x93, 0xDE, 0x29, 0xC4, 0x35, 0x0F, 0x0A, 0x11, 0x59, 0x51, 0xE2,
|
||||
0xCF, 0xF1, 0xEA, 0x2E, 0xDA, 0x35, 0x67, 0x0E, 0x73, 0xAD, 0xAE, 0xBA, 0xF6, 0xA6, 0xD2, 0x03,
|
||||
0x5E, 0x69, 0x93, 0x36, 0x42, 0xD0, 0x67, 0xAE, 0x84, 0x06, 0xB7, 0xA4, 0x00, 0x9C, 0xC1, 0x94,
|
||||
0x0E, 0x6A, 0xB7, 0x35, 0x15, 0x0A, 0xD9, 0xE9, 0x25, 0x2C, 0x3F, 0xD0, 0x55, 0x4C, 0x87, 0x1F,
|
||||
0x74, 0x33, 0xFF, 0xAE, 0xD2, 0x41, 0xC4, 0x90, 0xDF, 0x13, 0xD1, 0xD1, 0x8A, 0x38, 0x46, 0x56,
|
||||
0x2D, 0x1D, 0xC2, 0xFC, 0x00, 0x31, 0x08, 0x8A, 0x03, 0x57, 0x5D, 0x50, 0xAA, 0xD5, 0xD3, 0x5D,
|
||||
0x7E, 0xB4, 0x9B, 0x27, 0x75, 0x8E, 0xF8, 0xAA, 0x8F, 0xDE, 0x6E, 0xB5, 0x88, 0x46, 0xBB, 0x6A,
|
||||
0x9C, 0xF3, 0xF6, 0xED, 0x03, 0x2C, 0x39, 0x2C, 0x13, 0xFC, 0xE8, 0xD2, 0x62, 0x37, 0xC0, 0xD1,
|
||||
0x82, 0x4B, 0xFD, 0xA2, 0x2A, 0xDF, 0x15, 0x81, 0x0B, 0x1B, 0xE7, 0x41, 0x0E, 0x5D, 0x03, 0x18,
|
||||
0x0A, 0x09, 0x51, 0x61, 0x3F, 0x7F, 0xA5, 0xF9, 0x60, 0x80, 0x36, 0xB7, 0xAF, 0x1F, 0x4C, 0x48,
|
||||
0xCF, 0x61, 0xE7, 0x36, 0x2D, 0xBF, 0x79, 0x8F, 0x3E, 0x15, 0xD4, 0x44, 0xF6, 0x02, 0xFB, 0xF1,
|
||||
0x09, 0xD5, 0xB5, 0x45, 0xD4, 0xE0, 0x84, 0x1A, 0xC2, 0x30, 0x88, 0xB6, 0x42, 0xF4, 0xAF, 0x43,
|
||||
0xFE, 0xC3, 0x9A, 0x91, 0x06, 0xE2, 0xA6, 0x18, 0xEC, 0x1E, 0x12, 0x2C, 0x8F, 0x08, 0x21, 0x05,
|
||||
0x6F, 0x32, 0x75, 0x9A, 0x81, 0x41, 0x78, 0x1C, 0xC1, 0xD9, 0xC9, 0xA3, 0xA3, 0x97, 0x74, 0x8F,
|
||||
0x35, 0x5F, 0x9E, 0xA9, 0xE1, 0x08, 0x5D, 0x46, 0xD5, 0x65, 0xEA, 0xC2, 0xB7, 0x41, 0x9B, 0x2A,
|
||||
0xFD, 0xE5, 0x67, 0x5F, 0xD6, 0x89, 0x8A, 0xA7, 0xC0, 0xD2, 0x8A, 0xDD, 0xC1, 0x1F, 0x37, 0x49,
|
||||
0x55, 0xEB, 0x1D, 0xD2, 0x93, 0x8B, 0x05, 0xD2, 0xED, 0xF7, 0x1F, 0x95, 0x64, 0xB6, 0xD3, 0xE3,
|
||||
0xEF, 0xD8, 0x4F, 0x77, 0x32, 0x49, 0x13, 0x41, 0xAE, 0x8A, 0x00, 0x09, 0x9B, 0xBD, 0xFF, 0xA5,
|
||||
0x8A, 0x06, 0xFF, 0x4D, 0x6D, 0x0D, 0x7E, 0x27, 0x42, 0xDA, 0x88, 0x85, 0xAD, 0x29, 0xBA, 0x49,
|
||||
0x79, 0x2E, 0x06, 0xC7, 0xD9, 0x3B, 0xAA, 0xFA, 0x85, 0x90, 0xCE, 0x10, 0xC2, 0xCD, 0x41, 0x63,
|
||||
0xA1, 0x34, 0x30, 0x24, 0xC1, 0x93, 0xD5, 0xC3, 0x9E, 0xC4, 0xF6, 0x86, 0xB9, 0x03, 0x15, 0x7E,
|
||||
0x23, 0x43, 0xB4, 0x1B, 0x4E, 0x5D, 0xC4, 0x84, 0x6E, 0xF2, 0xCA, 0xFE, 0x9E, 0x9A, 0x8B, 0xF1,
|
||||
0x00, 0x0C, 0x76, 0x10, 0x91, 0xE2, 0x97, 0x8A, 0x40, 0x5F, 0x06, 0x1B, 0x00, 0x58, 0x52, 0xBB,
|
||||
0xAA, 0xA4, 0x73, 0x4E, 0xD0, 0xF3, 0x9D, 0x8C, 0xD0, 0x39, 0x8D, 0x3D, 0x67, 0x4D, 0xEA, 0x05,
|
||||
0xA4, 0x04, 0x9A, 0xEF, 0xAC, 0x9A, 0x56, 0x8F, 0x46, 0x37, 0xC2, 0x8A, 0x27, 0x71, 0xE1, 0x2F,
|
||||
0x3F, 0xC2, 0x9E, 0x77, 0x9E, 0xFD, 0xE2, 0x51, 0x13, 0x8C, 0xCA, 0x2B, 0xE3, 0x6E, 0x71, 0x8F,
|
||||
0x7B, 0x23, 0xB5, 0xD9, 0x59, 0xAA, 0xF1, 0xB2, 0x53, 0xD8, 0xBF, 0xDB, 0xEC, 0x1D, 0x99, 0xD8,
|
||||
0x41, 0x31, 0x75, 0xA4, 0x68, 0x34, 0xC8, 0x3A, 0x1F, 0xDF, 0xAF, 0xE9, 0xC7, 0xA8, 0xF2, 0x14,
|
||||
0x1A, 0xF8, 0xB1, 0x4B, 0x3B, 0x67, 0x40, 0x07, 0xD1, 0x27, 0xCE, 0xA4, 0x7A, 0x44, 0x1F, 0x16,
|
||||
0xF4, 0xE2, 0x26, 0x18, 0x09, 0x99, 0xC5, 0xB9, 0x39, 0x30, 0xEE, 0xC0, 0xEE, 0x9D, 0x74, 0x91,
|
||||
0xCE, 0x32, 0x2C, 0xEE, 0xE6, 0xED, 0xC3, 0x2C, 0x4D, 0xAB, 0x6A, 0x5C, 0x16, 0xDA, 0xD2, 0xDE,
|
||||
0x59, 0xBA, 0x9C, 0x96, 0x99, 0xC1, 0x15, 0x7E, 0x69, 0xD7, 0xEC, 0x6A, 0x55, 0xA2, 0x4F, 0xCD,
|
||||
0xF6, 0xA2, 0x7E, 0x6C, 0xD3, 0x6A, 0xF5, 0x60, 0x9E, 0xB3, 0x7D, 0xD5, 0xF9, 0x5F, 0x77, 0x0D,
|
||||
0x73, 0xC9, 0x51, 0x78, 0x15, 0xCC, 0x40, 0x42, 0xC2, 0xD9, 0x27, 0x0F, 0x4D, 0x00, 0x89, 0x7F,
|
||||
0x97, 0xA1, 0xED, 0xEC, 0x5E, 0xCA, 0xE7, 0x73, 0x6E, 0xF5, 0x0F, 0x24, 0x8A, 0x15, 0xC0, 0x77,
|
||||
0x05, 0xBB, 0x68, 0x76, 0xD6, 0xE4, 0x9C, 0x26, 0xEB, 0x35, 0x22, 0x7B, 0xA5, 0xE9, 0x1B, 0xB5,
|
||||
0x6F, 0x1F, 0xBF, 0x6F, 0xC5, 0x29, 0x52, 0x8F, 0x60, 0x04, 0xAF, 0xDF, 0x61, 0x7C, 0xEA, 0x09,
|
||||
0x6F, 0x5E, 0x19, 0x60, 0x9F, 0x0F, 0x99, 0x8A, 0x59, 0x07, 0x5E, 0x6B, 0xEC, 0x1E, 0xC0, 0x53,
|
||||
0x55, 0xB6, 0x0E, 0xB7, 0xEA, 0xCD, 0xA2, 0x46, 0x58, 0x07, 0x0C, 0xAF, 0x28, 0x83, 0x35, 0x75,
|
||||
0xF3, 0x6D, 0xDD, 0x69, 0x79, 0x5C, 0x29, 0x04, 0x84, 0x55, 0x0F, 0x6B, 0x47, 0xE2, 0x88, 0x6E,
|
||||
0x17, 0x4E, 0x5B, 0x0D, 0x97, 0x35, 0x0C, 0x13, 0x8C, 0xF3, 0xA3, 0xE0, 0x94, 0x61, 0x5C, 0x60,
|
||||
0x18, 0x87, 0x1A, 0x52, 0xDB, 0x90, 0xF8, 0x13, 0xB5, 0x42, 0xC2, 0x65, 0xFB, 0x13, 0x68, 0xE0,
|
||||
0xC4, 0xB0, 0xC9, 0x90, 0x69, 0x86, 0x8C, 0x94, 0xDC, 0x0B, 0xB3, 0xB4, 0x8C, 0xDE, 0x38, 0x9F,
|
||||
0xCF, 0x99, 0xA1, 0xD5, 0x8B, 0x7E, 0x27, 0xCD, 0xFA, 0x15, 0xC5, 0xE7, 0x75, 0x9A, 0x3D, 0x14,
|
||||
0xEB, 0x72, 0x4E, 0x1C, 0xE8, 0xE1, 0x5B, 0xB1, 0xAA, 0x1D, 0x3D, 0xE2, 0x44, 0x27, 0x52, 0x19,
|
||||
0x7A, 0x00, 0x4D, 0x1D, 0xD4, 0xDA, 0xC0, 0x67, 0x1C, 0x69, 0x03, 0x5B, 0x87, 0xA4, 0x18, 0xB1,
|
||||
0xC0, 0xA0, 0x4D, 0x52, 0x7A, 0xBD, 0x36, 0x71, 0xED, 0x77, 0xDA, 0xC0, 0x2D, 0xB4, 0xDA, 0x20,
|
||||
0xEF, 0xAC, 0x01, 0x3B, 0x64, 0x9B, 0xC6, 0xEB, 0x11, 0x9E, 0x61, 0x43, 0x30, 0xAF, 0x46, 0xAD,
|
||||
0x33, 0xC2, 0xF3, 0x33, 0x42, 0x77, 0xC3, 0x17, 0x03, 0xC2, 0x2E, 0xED, 0x20, 0xCC, 0xBD, 0xA6,
|
||||
0xD8, 0xAE, 0x49, 0xB4, 0x35, 0x71, 0x5A, 0x13, 0x99, 0x35, 0xC3, 0x99, 0x35, 0xED, 0xC2, 0xDA,
|
||||
0x54, 0x04, 0x20, 0xF8, 0xDA, 0x40, 0x23, 0xD6, 0x24, 0xF6, 0xDA, 0x80, 0x13, 0x6B, 0x03, 0x9E,
|
||||
0xAC, 0xC9, 0x85, 0x35, 0xF9, 0xB1, 0x26, 0x61, 0xD7, 0x24, 0xEC, 0x9A, 0xB1, 0xF1, 0x9A, 0x84,
|
||||
0x05, 0x50, 0x23, 0x41, 0x41, 0x90, 0x72, 0x4F, 0x31, 0x41, 0x28, 0x16, 0xC6, 0xEC, 0x1C, 0xED,
|
||||
0xD6, 0x79, 0xC0, 0x06, 0x76, 0xEB, 0x71, 0x35, 0x20, 0xF2, 0x62, 0xA0, 0x4A, 0xC5, 0x29, 0xC4,
|
||||
0x7C, 0x32, 0x02, 0xA1, 0x81, 0x2F, 0x24, 0x48, 0xDC, 0x48, 0xE2, 0x31, 0x4A, 0x62, 0xE1, 0xC2,
|
||||
0x59, 0x73, 0x00, 0xC7, 0x1D, 0x0C, 0x3B, 0xD7, 0xD3, 0x8D, 0x72, 0x33, 0x50, 0x9B, 0xD3, 0x32,
|
||||
0x69, 0x38, 0x33, 0xAC, 0x29, 0xCC, 0xBD, 0xEF, 0x5C, 0xAE, 0xE4, 0x05, 0x2E, 0x86, 0x52, 0x14,
|
||||
0x3D, 0xBC, 0xF8, 0x94, 0x86, 0x8E, 0x26, 0x05, 0x33, 0x47, 0x03, 0xB7, 0x63, 0x9C, 0xA5, 0xBC,
|
||||
0x98, 0x1B, 0x3F, 0x1B, 0xF7, 0x3E, 0xE9, 0xAC, 0x98, 0xAA, 0x09, 0x7C, 0x7E, 0xF0, 0x1A, 0xA6,
|
||||
0xE7, 0xFF, 0x7D, 0xEF, 0xF5, 0x4B, 0xF7, 0x6E, 0x0F, 0x7C, 0xBA, 0x78, 0x1D, 0xA2, 0x34, 0x97,
|
||||
0x77, 0x70, 0xAD, 0x72, 0x52, 0x46, 0x48, 0xA3, 0xC4, 0x4B, 0x93, 0x38, 0x8B, 0x7E, 0x4A, 0x31,
|
||||
0x01, 0xEE, 0x7C, 0x01, 0xF2, 0x55, 0x14, 0x77, 0x50, 0xD3, 0xF1, 0xC9, 0xCE, 0xD7, 0xCA, 0x4F,
|
||||
0x07, 0x0A, 0x59, 0x2E, 0x6A, 0x1D, 0x76, 0x8B, 0x96, 0xBB, 0xF9, 0x36, 0x7B, 0x77, 0xEC, 0xE2,
|
||||
0xD3, 0x13, 0xFB, 0x00, 0x3A, 0x63, 0xE2, 0xE9, 0xE1, 0x00, 0x61, 0xFC, 0xBA, 0x98, 0xD9, 0x25,
|
||||
0x77, 0xCA, 0x5D, 0x2F, 0xDA, 0xEE, 0x50, 0x4C, 0xC0, 0x96, 0x99, 0x0F, 0x3E, 0x54, 0x4C, 0xE7,
|
||||
0xCD, 0x6C, 0xB8, 0x37, 0x73, 0x06, 0xC2, 0x76, 0xF1, 0x0E, 0xD6, 0x29, 0x3E, 0xFE, 0x80, 0x29,
|
||||
0x1B, 0xF2, 0xB1, 0x69, 0x76, 0xA6, 0xD8, 0xD6, 0x1E, 0x3C, 0x2F, 0x85, 0x7B, 0xEB, 0xE0, 0x0D,
|
||||
0x2D, 0xF9, 0x4B, 0x41, 0x40, 0x59, 0x08, 0xBA, 0x82, 0xF2, 0x80, 0x58, 0x22, 0xAD, 0xE3, 0xED,
|
||||
0xFB, 0x36, 0x0F, 0xC4, 0x65, 0x15, 0x92, 0x3F, 0xDF, 0x75, 0x75, 0x92, 0x73, 0x92, 0x66, 0x43,
|
||||
0x52, 0x52, 0xDB, 0x3D, 0x58, 0x1A, 0x4C, 0x00, 0xA3, 0x0C, 0xCB, 0xD2, 0x80, 0x33, 0x08, 0xC4,
|
||||
0x24, 0x0C, 0x85, 0xE9, 0x41, 0x5E, 0x54, 0x7A, 0x66, 0x97, 0xCC, 0xB0, 0xA4, 0x04, 0x4E, 0x8E,
|
||||
0x73, 0x70, 0x95, 0xFE, 0x0D, 0x04, 0x26, 0xFE, 0xE7, 0x6E, 0x06, 0xE1, 0x7F, 0x30, 0x6C, 0xFE,
|
||||
0x2F, 0xB1, 0x76, 0x35, 0x0D, 0x5F, 0x3E, 0xC7, 0xC0, 0x64, 0xC5, 0xB9, 0x84, 0xCD, 0x7E, 0x5F,
|
||||
0x30, 0x5A, 0xC1, 0x4A, 0x4C, 0xAB, 0x5A, 0x3C, 0x11, 0xE3, 0xE9, 0x0E, 0xB1, 0x1F, 0x70, 0x38,
|
||||
0xEF, 0xB0, 0x2E, 0x3A, 0x5D, 0x30, 0x0A, 0x64, 0x81, 0x85, 0x81, 0x62, 0xD4, 0x78, 0xC3, 0x1A,
|
||||
0x58, 0xB0, 0xDC, 0x75, 0x86, 0x88, 0x30, 0xE8, 0x02, 0x44, 0xB5, 0x1A, 0x23, 0x61, 0x11, 0xF1,
|
||||
0xF4, 0xFB, 0xB0, 0x61, 0xA6, 0xE6, 0x0D, 0x18, 0x53, 0x22, 0xEC, 0x34, 0x1C, 0x4B, 0x2A, 0x27,
|
||||
0x79, 0xC4, 0xE7, 0x45, 0xAA, 0x42, 0x86, 0xA6, 0xE7, 0xDA, 0x05, 0xAA, 0x3A, 0x53, 0xFE, 0xFF,
|
||||
0xF0, 0x79, 0xE6, 0xC2, 0xD7, 0x04, 0x14, 0xF9, 0x43, 0xE3, 0xAA, 0x02, 0x76, 0x96, 0x2A, 0x86,
|
||||
0xB8, 0xCC, 0x13, 0xA9, 0x1C, 0x2C, 0x6A, 0x60, 0xFF, 0x9A, 0xFE, 0x3B, 0x9D, 0x69, 0x14, 0x93,
|
||||
0x3C, 0x49, 0xC9, 0x34, 0x8F, 0xB2, 0x86, 0x55, 0x0B, 0x46, 0xEF, 0xE9, 0x47, 0x24, 0xC5, 0xBF,
|
||||
0x36, 0x14, 0x4B, 0xD6, 0x83, 0xED, 0x52, 0xAD, 0x90, 0x4F, 0x01, 0x25, 0x34, 0xFE, 0x8C, 0x19,
|
||||
0x09, 0xFB, 0x69, 0x3A, 0x34, 0x56, 0xE5, 0x30, 0xB2, 0xA0, 0xAB, 0x4D, 0x4B, 0xA4, 0x1A, 0x2C,
|
||||
0x7C, 0x61, 0x69, 0xCD, 0x90, 0x6C, 0xC3, 0x08, 0xED, 0x82, 0xD9, 0x73, 0x41, 0xBE, 0x02, 0x89,
|
||||
0xA8, 0x15, 0xEF, 0x09, 0xC3, 0x9C, 0x96, 0x18, 0xA1, 0x99, 0x2C, 0x5F, 0x38, 0xC5, 0x53, 0x1B,
|
||||
0x06, 0xF2, 0x9A, 0xD8, 0x26, 0x3F, 0xE5, 0x76, 0xBE, 0x2C, 0xE7, 0x94, 0x8D, 0x71, 0x8E, 0xAB,
|
||||
0x26, 0xBC, 0x8E, 0x90, 0xEE, 0x8B, 0x37, 0x4B, 0x4A, 0xBF, 0xD9, 0x15, 0x51, 0xE6, 0x6C, 0xF4,
|
||||
0x30, 0x2F, 0xAF, 0xDE, 0xFC, 0x47, 0xA4, 0x7A, 0x25, 0x15, 0xB0, 0x16, 0x50, 0x30, 0x70, 0x52,
|
||||
0xC5, 0x8A, 0x02, 0xF3, 0x56, 0x6E, 0x37, 0xE1, 0xCD, 0x63, 0xC9, 0xCA, 0xB2, 0xD9, 0x84, 0xB0,
|
||||
0x14, 0xC2, 0x4C, 0x83, 0x35, 0x66, 0xA9, 0x7C, 0x76, 0x73, 0x92, 0x3D, 0xCC, 0x24, 0x38, 0x9B,
|
||||
0x3B, 0xA6, 0xCA, 0x15, 0x05, 0xA5, 0xF3, 0x9C, 0x15, 0xB2, 0x33, 0xEB, 0x8C, 0xD2, 0x71, 0x3D,
|
||||
0x5B, 0x18, 0xC8, 0x22, 0x93, 0xD5, 0xFC, 0xDB, 0x15, 0xCB, 0x25, 0x6F, 0xE5, 0xC6, 0x6C, 0xB8,
|
||||
0x81, 0xB9, 0xE2, 0x19, 0x83, 0xAB, 0xAE, 0xB9, 0x78, 0x95, 0x8B, 0x66, 0xCC, 0x3E, 0x6F, 0x13,
|
||||
0xBF, 0x5F, 0xC5, 0xD8, 0x4D, 0x1F, 0xB1, 0x24, 0x1E, 0x40, 0xB5, 0x1A, 0xBF, 0xE3, 0xBE, 0x7A,
|
||||
0x16, 0xC3, 0xD7, 0xDC, 0xC0, 0x91, 0x2B, 0x69, 0x9C, 0x4A, 0xD5, 0x7E, 0xC9, 0xBD, 0x97, 0xBC,
|
||||
0xFA, 0x43, 0x70, 0x21, 0xA9, 0xC3, 0x16, 0x1B, 0xFA, 0x2B, 0x57, 0x22, 0xB4, 0x39, 0xC2, 0x94,
|
||||
0x36, 0x78, 0x7C, 0xAB, 0xF7, 0x5C, 0x31, 0x66, 0xEF, 0xCD, 0x6E, 0x3C, 0x34, 0x3E, 0x8D, 0xF2,
|
||||
0x93, 0x63, 0x03, 0xD5, 0x60, 0x0C, 0x66, 0xCE, 0xA5, 0xC1, 0xB8, 0xAA, 0x00, 0xEF, 0x52, 0x93,
|
||||
0xD8, 0xF1, 0x34, 0xB4, 0xED, 0x29, 0x0B, 0x53, 0x37, 0xAF, 0xA8, 0xD1, 0x5D, 0x05, 0xB2, 0x08,
|
||||
0x02, 0x96, 0x15, 0x2C, 0x47, 0x6D, 0x59, 0x8E, 0x6A, 0x5C, 0x39, 0xAA, 0x90, 0x22, 0x69, 0xBE,
|
||||
0xAB, 0x49, 0x95, 0xCF, 0x02, 0x57, 0x9E, 0xC9, 0x5D, 0xED, 0xAA, 0xE6, 0xB0, 0x01, 0xF6, 0xFF,
|
||||
0xA8, 0xC2, 0x54, 0xB7, 0x93, 0xA5, 0x3A, 0x35, 0x97, 0xFF, 0x1F, 0x78, 0xDC, 0xD6, 0xF4, 0x17,
|
||||
0x0D, 0x7D, 0x2E, 0xE5, 0x2F, 0xCD, 0x2F, 0xD7, 0xE3, 0x2E, 0xB5, 0xDC, 0xF5, 0x61, 0xDF, 0x70,
|
||||
0xBC, 0xDE, 0x72, 0x2C, 0x92, 0xBE, 0x66, 0x9E, 0xF0, 0xBA, 0x9A, 0xE5, 0xE2, 0x85, 0x20, 0x1D,
|
||||
0xE3, 0x12, 0xF4, 0xFA, 0xAC, 0x0C, 0xB7, 0xF7, 0x6C, 0xC9, 0xCA, 0x7F, 0x89, 0x39, 0x75, 0x89,
|
||||
0xC9, 0xD3, 0x2B, 0x36, 0x13, 0x1F, 0x7E, 0x65, 0x17, 0xB8, 0x4B, 0x5D, 0xC2, 0xDC, 0x54, 0x65,
|
||||
0x34, 0xDC, 0xB0, 0x6E, 0x2A, 0x55, 0x4C, 0xC8, 0xDF, 0xF7, 0xC7, 0x1A, 0x29, 0xF3, 0x52, 0x22,
|
||||
0x17, 0x6C, 0x6F, 0x0D, 0xCE, 0xB7, 0x62, 0x65, 0xC9, 0x9D, 0x97, 0xB2, 0xF4, 0x6A, 0x55, 0xCD,
|
||||
0xB5, 0x14, 0xA9, 0xAA, 0xEC, 0x69, 0x9F, 0x2F, 0x8A, 0xC8, 0x5D, 0x62, 0x9B, 0xDF, 0xFB, 0xE1,
|
||||
0x9C, 0xE0, 0xA4, 0x0C, 0x0A, 0xA5, 0xBA, 0xE4, 0x1D, 0x3B, 0xD7, 0x0A, 0x41, 0x14, 0x4B, 0x66,
|
||||
0x5C, 0x31, 0x6F, 0x69, 0xB7, 0x2C, 0x2B, 0xAD, 0x46, 0x7D, 0x63, 0x7B, 0xF7, 0xD2, 0xFB, 0xD7,
|
||||
0x6D, 0xF3, 0x5E, 0xC6, 0xA8, 0xF4, 0x84, 0x5E, 0xE3, 0x7D, 0xA1, 0x97, 0x75, 0xF4, 0x4A, 0x9F,
|
||||
0xA1, 0x57, 0x7C, 0x19, 0x08, 0xBD, 0x92, 0x1D, 0xBD, 0x8A, 0x1D, 0xBD, 0x7E, 0xBB, 0xA3, 0x57,
|
||||
0xBC, 0x63, 0xCC, 0x5C, 0x73, 0xBC, 0x4E, 0xDF, 0x14, 0xFF, 0x1E, 0xF2, 0x16, 0xD1, 0xF9, 0x7B,
|
||||
0x15, 0x08, 0x79, 0x53, 0xBD, 0x40, 0x22, 0xD8, 0x22, 0x5B, 0x42, 0x4E, 0x7B, 0xBD, 0xBD, 0x67,
|
||||
0x20, 0xA5, 0x23, 0xE4, 0xEE, 0x9C, 0x34, 0x7E, 0xDD, 0xF6, 0x4D, 0xC8, 0x76, 0xE1, 0x6A, 0x7C,
|
||||
0x62, 0x94, 0xF4, 0x4B, 0xFF, 0xD4, 0x37, 0x25, 0x39, 0x2D, 0xD4, 0x6E, 0xD4, 0x5C, 0x25, 0xD4,
|
||||
0x7F, 0xC4, 0x9D, 0xD1, 0xA8, 0xFF, 0xBD, 0x51, 0x76, 0x50, 0x55, 0x23, 0x55, 0xAA, 0xEC, 0x85,
|
||||
0xCD, 0x1F, 0x32, 0xB8, 0xA0, 0x0C, 0xCC, 0xC8, 0x60, 0xD3, 0x33, 0xC4, 0xF5, 0x19, 0x8C, 0x79,
|
||||
0x06, 0x1E, 0x65, 0x44, 0x0C, 0x22, 0xA2, 0xE1, 0xF7, 0x34, 0x72, 0x31, 0x1D, 0xB0, 0x42, 0x35,
|
||||
0xA2, 0x71, 0x9B, 0x69, 0x9D, 0x41, 0xC4, 0x32, 0x98, 0xFF, 0x0C, 0x96, 0xE2, 0xA6, 0x7E, 0xDE,
|
||||
0x35, 0xEF, 0xEA, 0x36, 0xCC, 0x41, 0x3A, 0xA9, 0xA3, 0xC2, 0xFE, 0x0E, 0x0E, 0x01, 0x26, 0xE1,
|
||||
0x0F, 0x8F, 0x4F, 0x10, 0xD0, 0xF8, 0x88, 0x65, 0x3A, 0x50, 0xDC, 0xC2, 0x52, 0x8B, 0xDB, 0x95,
|
||||
0x07, 0x02, 0xC8, 0xB7, 0xFB, 0x98, 0x31, 0x28, 0x8F, 0xF6, 0x7C, 0x9E, 0xDE, 0x94, 0xCF, 0xB3,
|
||||
0x58, 0x43, 0x7F, 0x3C, 0x16, 0x91, 0x82, 0xF1, 0x9E, 0x44, 0xFC, 0x05, 0xB5, 0x2E, 0xA5, 0x58,
|
||||
0xF3, 0xDC, 0x70, 0x06, 0x0C, 0xDE, 0xA3, 0x1D, 0x90, 0xAF, 0x71, 0x2D, 0x5D, 0xA4, 0xB3, 0x0C,
|
||||
0x0B, 0xB6, 0x37, 0x46, 0x37, 0xD5, 0xBE, 0xF6, 0x2E, 0xD7, 0x11, 0x10, 0x26, 0xC1, 0xFE, 0x18,
|
||||
0x0B, 0x5A, 0x1C, 0x58, 0xD0, 0x8C, 0x16, 0x34, 0x13, 0xC5, 0xE7, 0x1A, 0xAF, 0x60, 0xC3, 0x9F,
|
||||
0xD6, 0xE0, 0xB7, 0x65, 0xF1, 0x44, 0x83, 0x13, 0x6A, 0xF0, 0x4B, 0xCD, 0xED, 0x0C, 0x6E, 0x47,
|
||||
0x06, 0x99, 0xF4, 0xD1, 0xE6, 0xEF, 0x10, 0x57, 0x62, 0x47, 0x2E, 0x76, 0x7B, 0xE9, 0xED, 0x8E,
|
||||
0x50, 0x26, 0x15, 0x53, 0xE4, 0x9A, 0x0F, 0xA4, 0x9C, 0x7E, 0xBE, 0xA8, 0x3D, 0xE3, 0x29, 0xA9,
|
||||
0x22, 0xB7, 0xCB, 0x61, 0x60, 0xE9, 0x2C, 0x27, 0x43, 0x02, 0xD6, 0x51, 0xA3, 0xE6, 0x4C, 0x9D,
|
||||
0xD4, 0x2C, 0x22, 0xEF, 0x0E, 0x65, 0x3A, 0xDE, 0x9E, 0x14, 0x51, 0x12, 0x6B, 0x0A, 0x6B, 0xFE,
|
||||
0xF4, 0xAB, 0xBB, 0xAF, 0x52, 0x6E, 0xE1, 0xC9, 0x0D, 0x6C, 0x70, 0x0A, 0x46, 0xB3, 0xE8, 0x99,
|
||||
0xA1, 0xE9, 0x57, 0x6C, 0x05, 0x5A, 0x05, 0x84, 0xAD, 0xCE, 0x7D, 0x9B, 0xB6, 0xB7, 0x2C, 0x36,
|
||||
0xA3, 0x7D, 0x3B, 0xA4, 0x34, 0x76, 0x17, 0x73, 0x41, 0x76, 0x3E, 0xD1, 0x58, 0xCE, 0x45, 0x46,
|
||||
0x77, 0x94, 0xC9, 0x30, 0x9B, 0x31, 0x75, 0xE7, 0x80, 0x37, 0x70, 0x67, 0x86, 0x51, 0xCF, 0x9A,
|
||||
0x44, 0x3D, 0x97, 0xB3, 0x2B, 0x09, 0x88, 0xCE, 0x96, 0x13, 0xB4, 0x98, 0x76, 0x9B, 0xE5, 0xF0,
|
||||
0x33, 0x66, 0xF8, 0x61, 0xCB, 0x08, 0x40, 0x84, 0x37, 0x18, 0x45, 0x72, 0x23, 0x4B, 0xD9, 0x2D,
|
||||
0xE3, 0x47, 0x58, 0x06, 0x31, 0xF9, 0x07, 0x39, 0x4F, 0x91, 0x12, 0xB2, 0x1C, 0x9D, 0x28, 0xF3,
|
||||
0x61, 0x9B, 0x85, 0xA0, 0x5B, 0xD0, 0x9D, 0x05, 0xED, 0x9A, 0x95, 0x04, 0xA6, 0xB7, 0x7E, 0x3F,
|
||||
0xF8, 0x1D, 0x66, 0xDE, 0xF6, 0xEA, 0x2B, 0xCC, 0x3D, 0x75, 0xEA, 0x09, 0x28, 0xB9, 0x69, 0x25,
|
||||
0xB7, 0x2B, 0x1A, 0x39, 0x77, 0x20, 0x57, 0x1B, 0x89, 0xCE, 0x11, 0x90, 0x2F, 0xC7, 0x53, 0x1B,
|
||||
0x97, 0x53, 0xFA, 0x72, 0x7A, 0x2F, 0x85, 0xA2, 0x82, 0xE8, 0xA7, 0x0C, 0x3E, 0xE3, 0x69, 0x39,
|
||||
0xFB, 0x93, 0xEC, 0x50, 0xBE, 0xEA, 0x13, 0xF8, 0x81, 0x8D, 0x94, 0x9B, 0x43, 0x12, 0x1E, 0xA1,
|
||||
0x0E, 0x06, 0x8D, 0x64, 0x6B, 0xA1, 0x44, 0x18, 0xAF, 0xA2, 0x18, 0xD3, 0x13, 0x47, 0xB1, 0xB0,
|
||||
0x5D, 0x28, 0xE6, 0xCB, 0xFF, 0x3A, 0xEC, 0x29, 0x76, 0x46, 0x5F, 0xA3, 0xE9, 0x66, 0x52, 0x49,
|
||||
0xC8, 0x37, 0x0B, 0xC5, 0xC2, 0x5D, 0x0C, 0xBC, 0x3F, 0xDF, 0xDA, 0x1F, 0xCF, 0xD1, 0x1D, 0xBF,
|
||||
0x4F, 0xCC, 0x2A, 0x76, 0xE3, 0xC9, 0x05, 0xBD, 0x76, 0xCD, 0x76, 0xF3, 0xF8, 0xC4, 0x9D, 0x40,
|
||||
0xA8, 0x8D, 0x8B, 0xA4, 0x39, 0x9E, 0x23, 0x93, 0xF9, 0x24, 0x62, 0x74, 0x6E, 0x94, 0xAF, 0x79,
|
||||
0xB8, 0x06, 0x35, 0x52, 0xC5, 0xF1, 0x31, 0x8F, 0xE4, 0x9A, 0x4A, 0x0A, 0x64, 0x3F, 0x60, 0x1A,
|
||||
0x10, 0x6F, 0xF9, 0xCB, 0xF3, 0x34, 0xE0, 0x42, 0x7D, 0x67, 0x90, 0x53, 0xC0, 0x5E, 0x0E, 0x3A,
|
||||
0xCE, 0x9F, 0xA7, 0x2B, 0xF0, 0xB4, 0x9F, 0xF3, 0x74, 0xC5, 0x93, 0xA3, 0x8D, 0xA3, 0x97, 0x5D,
|
||||
0xEA, 0x97, 0x10, 0xC8, 0x92, 0xEC, 0xC3, 0xCE, 0x89, 0xE4, 0xB3, 0xB7, 0x54, 0x09, 0x94, 0xFE,
|
||||
0x9C, 0x05, 0x88, 0x04, 0xBE, 0x28, 0x77, 0x27, 0xAF, 0x4F, 0xDD, 0x18, 0xCB, 0x7C, 0xB4, 0xE3,
|
||||
0xD1, 0x83, 0x2B, 0x51, 0x84, 0xC8, 0xFB, 0x11, 0x22, 0x7F, 0x1E, 0xE5, 0xE7, 0x27, 0xC8, 0x51,
|
||||
0xFE, 0x52, 0x55, 0xCC, 0xFB, 0xBD, 0x3B, 0xEA, 0xFF, 0x00, 0xD5, 0x5A, 0x3D, 0x8C, 0xDC, 0xD6,
|
||||
0xBD, 0x5A, 0x3D, 0x8C, 0xE5, 0x56, 0x15, 0xBE, 0xB6, 0x77, 0xD7, 0x12, 0x16, 0x72, 0x8A, 0x2C,
|
||||
0x44, 0x6C, 0xE1, 0x26, 0x55, 0x22, 0xE2, 0x36, 0xC5, 0x8E, 0xAF, 0xA3, 0x88, 0x3E, 0xC5, 0x4E,
|
||||
0x56, 0x62, 0x8A, 0xD0, 0x12, 0x09, 0x4D, 0x93, 0x82, 0x62, 0xE6, 0xD9, 0x90, 0x82, 0x22, 0x05,
|
||||
0x2B, 0xA1, 0x20, 0xA4, 0x34, 0x14, 0x09, 0x82, 0xB7, 0xC5, 0x36, 0x14, 0x20, 0xBD, 0x79, 0xB6,
|
||||
0x40, 0xA2, 0xA1, 0x88, 0x41, 0x42, 0x94, 0x38, 0x62, 0xB7, 0x48, 0x28, 0x62, 0x81, 0x23, 0xFC,
|
||||
0x36, 0xB6, 0x2F, 0xDF, 0x77, 0xAE, 0xFD, 0xDE, 0xCC, 0xEE, 0xEC, 0x6F, 0x56, 0xF8, 0xF9, 0x1E,
|
||||
0xCF, 0xF3, 0xBB, 0x3F, 0xE7, 0x9E, 0xFF, 0x73, 0xEE, 0x68, 0xA5, 0xCF, 0xF9, 0x24, 0xEA, 0x95,
|
||||
0xE9, 0xF3, 0x6D, 0x75, 0x41, 0xF1, 0xD2, 0xAA, 0x48, 0x8B, 0x3C, 0xC3, 0xB7, 0x55, 0x50, 0x4D,
|
||||
0xB7, 0x5F, 0xE2, 0x3E, 0xC2, 0xED, 0xF1, 0x7E, 0xD5, 0xCB, 0x95, 0xFA, 0xAD, 0x53, 0xA3, 0x73,
|
||||
0xEB, 0x11, 0x36, 0x5E, 0xA5, 0x94, 0xD3, 0x78, 0xA5, 0x52, 0x6E, 0xE3, 0x13, 0xD6, 0x01, 0xA0,
|
||||
0x57, 0x05, 0xB9, 0xFA, 0xDA, 0xCD, 0xBB, 0xB7, 0x3E, 0x4B, 0x43, 0x83, 0x2B, 0xBF, 0x78, 0xF0,
|
||||
0xB7, 0xC3, 0xFD, 0x54, 0x05, 0x79, 0x8C, 0x41, 0x61, 0x9A, 0x08, 0xDC, 0x03, 0x8C, 0xD4, 0x55,
|
||||
0xC0, 0x58, 0x1D, 0x61, 0x79, 0x74, 0x1C, 0x27, 0xA8, 0xF6, 0x54, 0x8B, 0xF7, 0x57, 0x55, 0x03,
|
||||
0x78, 0xE4, 0x10, 0xAE, 0x5C, 0xC2, 0xCE, 0xB5, 0x8B, 0x57, 0xF3, 0xE2, 0xCA, 0x41, 0xC3, 0x6A,
|
||||
0xD2, 0xAE, 0xDC, 0x50, 0xBE, 0x31, 0x55, 0x64, 0x4C, 0x93, 0x7D, 0xF0, 0xF3, 0xE6, 0x04, 0xEF,
|
||||
0xFA, 0xA9, 0x0D, 0x68, 0xD9, 0x7B, 0xB9, 0x8A, 0x4D, 0x8E, 0x3E, 0x9D, 0x52, 0x19, 0xFE, 0x08,
|
||||
0x4C, 0xA3, 0x82, 0x9F, 0x74, 0x18, 0xDB, 0xA3, 0xAD, 0xF8, 0x4C, 0x83, 0x1F, 0xF5, 0xE3, 0xC1,
|
||||
0xAD, 0x16, 0x68, 0x54, 0x19, 0x31, 0xB9, 0x92, 0xEF, 0xE6, 0x67, 0xB3, 0x97, 0x3B, 0x72, 0x07,
|
||||
0x2A, 0x34, 0x25, 0x90, 0x57, 0xF1, 0xEB, 0x35, 0x30, 0x56, 0x09, 0x10, 0x5D, 0x11, 0xF7, 0x86,
|
||||
0xD8, 0x1F, 0x01, 0xCD, 0x46, 0x1E, 0x24, 0x0C, 0x1E, 0x61, 0x2E, 0x8F, 0x48, 0x36, 0xD6, 0x26,
|
||||
0x97, 0x6E, 0xA8, 0x3D, 0xA7, 0x59, 0xE9, 0x5E, 0xBD, 0xE2, 0x36, 0xED, 0xBA, 0x50, 0xD1, 0x07,
|
||||
0x7F, 0x6D, 0xDA, 0x30, 0x0D, 0xCC, 0xBB, 0x7F, 0x69, 0xC2, 0xDC, 0xF9, 0x7D, 0xF8, 0x87, 0x86,
|
||||
0xC4, 0x8C, 0xDC, 0x9A, 0x13, 0x24, 0x9E, 0x3C, 0x8E, 0xEC, 0xA3, 0x93, 0x97, 0x6E, 0xE3, 0xB2,
|
||||
0x8B, 0x57, 0x39, 0x18, 0xA0, 0xF0, 0x57, 0x17, 0xA5, 0x44, 0x2B, 0x1B, 0xC9, 0x4D, 0x15, 0x75,
|
||||
0x82, 0xAB, 0x93, 0xCE, 0x58, 0x47, 0x86, 0x7F, 0x16, 0x06, 0x34, 0x74, 0xC6, 0x03, 0xE0, 0xE8,
|
||||
0xB5, 0x0E, 0xFA, 0x00, 0xCD, 0x7E, 0x07, 0xF9, 0xC6, 0xE9, 0x3C, 0xD2, 0x7F, 0x88, 0x48, 0x6D,
|
||||
0x5D, 0x70, 0xDB, 0xD1, 0xC0, 0x19, 0x32, 0xBE, 0x76, 0xCD, 0xA0, 0xBC, 0x3A, 0x68, 0xB3, 0xDC,
|
||||
0xAF, 0xA2, 0x32, 0xAA, 0xC3, 0x52, 0xA7, 0x7E, 0x07, 0x88, 0x59, 0x75, 0xAA, 0x49, 0x5B, 0x59,
|
||||
0xBE, 0x27, 0x0C, 0x7A, 0x15, 0x97, 0x4A, 0x8F, 0x2A, 0xDC, 0x5C, 0x37, 0x18, 0x61, 0x4C, 0x17,
|
||||
0xD6, 0xC0, 0x21, 0xF7, 0x7A, 0xE5, 0xBC, 0x41, 0xC6, 0xF2, 0x3A, 0x3A, 0x3A, 0x7A, 0x15, 0x22,
|
||||
0xB9, 0xC2, 0xC2, 0x8D, 0x5F, 0x85, 0x65, 0x94, 0xC7, 0x29, 0xA5, 0x74, 0x81, 0xCF, 0xC9, 0xA9,
|
||||
0x0F, 0xBF, 0xF3, 0x7D, 0x9C, 0x87, 0x65, 0x58, 0xF9, 0xB5, 0x4B, 0xA4, 0x8F, 0xD5, 0x85, 0x15,
|
||||
0xDE, 0x45, 0x69, 0x58, 0xFA, 0xB5, 0xD7, 0xB8, 0xDC, 0x54, 0x8F, 0xCF, 0x3C, 0x60, 0xFE, 0x9C,
|
||||
0xA8, 0x81, 0xEF, 0x5D, 0xAE, 0xD1, 0x04, 0x55, 0x98, 0x6B, 0x0C, 0x7D, 0xCE, 0x12, 0xC8, 0xAF,
|
||||
0x6D, 0xA3, 0x4C, 0xA1, 0xBD, 0xE8, 0x95, 0x17, 0xB3, 0x9F, 0xFD, 0x69, 0x2C, 0x20, 0x07, 0xDF,
|
||||
0x2A, 0xC6, 0x8F, 0x54, 0x88, 0x8D, 0x6B, 0x30, 0xF7, 0x04, 0x9B, 0xEC, 0xE3, 0xDC, 0x6B, 0xC3,
|
||||
0x4A, 0x68, 0xFF, 0x9C, 0x43, 0xA1, 0x57, 0x8F, 0x00, 0x19, 0xA6, 0x49, 0x05, 0x5C, 0xBE, 0xFE,
|
||||
0xC5, 0xFE, 0xA3, 0x46, 0x28, 0xE0, 0x13, 0x92, 0x08, 0x25, 0x89, 0x90, 0x40, 0xA0, 0x8E, 0x88,
|
||||
0x26, 0x39, 0x97, 0xE2, 0xDE, 0xDF, 0x3F, 0xA3, 0xAC, 0x0A, 0x2A, 0xAC, 0xB0, 0x3B, 0x45, 0xD9,
|
||||
0x23, 0x0B, 0x9D, 0x56, 0x98, 0xA9, 0xA8, 0xA1, 0x01, 0xB6, 0x13, 0x52, 0x88, 0x72, 0x6A, 0x99,
|
||||
0x48, 0x05, 0xF6, 0x2C, 0xDD, 0x14, 0xF8, 0xEC, 0x4E, 0x5D, 0xFD, 0x87, 0x74, 0xB5, 0x57, 0x0C,
|
||||
0x91, 0x52, 0xD0, 0x09, 0xE5, 0x51, 0x70, 0x82, 0xDA, 0xE9, 0xD9, 0xF9, 0x84, 0xBD, 0xC1, 0x8D,
|
||||
0x54, 0x98, 0xA2, 0x88, 0xAE, 0x05, 0x51, 0xFE, 0x30, 0x30, 0xF5, 0x93, 0x61, 0x32, 0x01, 0xA6,
|
||||
0x5A, 0x63, 0x52, 0xA0, 0xE5, 0x6D, 0xAE, 0x74, 0x5C, 0x08, 0x08, 0xC5, 0x22, 0xAD, 0x14, 0x11,
|
||||
0xEC, 0x04, 0x92, 0x58, 0xD4, 0xDE, 0x97, 0x75, 0xF4, 0xCF, 0x5A, 0xBF, 0x5E, 0x7F, 0x1D, 0x6F,
|
||||
0x9E, 0xE2, 0x16, 0x1A, 0x0E, 0x50, 0x7B, 0x53, 0xEA, 0x3B, 0x87, 0x4D, 0x02, 0xF9, 0xBF, 0x00,
|
||||
0x76, 0x4E, 0xF4, 0x98, 0x9A, 0x33, 0xD1, 0xC5, 0x6F, 0x2C, 0x3D, 0x34, 0xB0, 0x58, 0xE3, 0x15,
|
||||
0x5E, 0x07, 0xC4, 0x2A, 0xBF, 0xEF, 0x6B, 0xB1, 0x5C, 0x2E, 0x29, 0x28, 0x6C, 0x56, 0xE3, 0x8A,
|
||||
0x91, 0xCA, 0x86, 0x4D, 0x60, 0x99, 0x16, 0xFC, 0x71, 0x39, 0xB7, 0x12, 0x0D, 0x3A, 0xD9, 0x02,
|
||||
0xBE, 0x93, 0xC5, 0x03, 0xAC, 0xF4, 0x76, 0x81, 0xA5, 0xCC, 0xA0, 0xA6, 0xBD, 0xAB, 0x2B, 0x32,
|
||||
0x62, 0x14, 0x9D, 0x53, 0x13, 0x37, 0x4E, 0x81, 0x0C, 0x74, 0x71, 0x06, 0xB5, 0x4E, 0xD1, 0xAF,
|
||||
0xE0, 0x88, 0xC8, 0x8C, 0xC0, 0xF3, 0xB2, 0xA8, 0xBA, 0x4C, 0xEA, 0x11, 0x84, 0x04, 0x31, 0xC1,
|
||||
0x9E, 0xEC, 0xAE, 0xA7, 0x9D, 0x06, 0xF0, 0x2B, 0x80, 0x10, 0x56, 0x0F, 0x9C, 0xEE, 0xC9, 0xE6,
|
||||
0x9E, 0xE2, 0xD0, 0x5B, 0x1B, 0xE0, 0x57, 0x00, 0xB0, 0x27, 0xA2, 0xBE, 0xBD, 0xC8, 0x6E, 0x2B,
|
||||
0xE0, 0xA5, 0xE5, 0x72, 0xFC, 0xE5, 0x84, 0xF8, 0xFD, 0x00, 0xFB, 0xAD, 0x62, 0xA0, 0x12, 0xF3,
|
||||
0x39, 0xE1, 0x39, 0xB7, 0x64, 0x6E, 0x6F, 0xDF, 0x50, 0x99, 0xB5, 0xF2, 0xD5, 0x05, 0x15, 0x51,
|
||||
0x48, 0x3B, 0x75, 0x1F, 0x07, 0xD8, 0xDC, 0x2E, 0x05, 0x07, 0x36, 0xAF, 0x65, 0x15, 0xB5, 0x26,
|
||||
0xA6, 0xC5, 0xBE, 0x3C, 0xCB, 0xA2, 0xC3, 0x6F, 0x11, 0x41, 0x71, 0xE7, 0x9A, 0x72, 0x49, 0x34,
|
||||
0x0A, 0xB1, 0x48, 0xB0, 0xB0, 0x0C, 0xA6, 0x40, 0x59, 0x21, 0x56, 0xFA, 0x12, 0xE8, 0xA2, 0x63,
|
||||
0x18, 0x7A, 0xBD, 0x3E, 0xC0, 0x8E, 0x6A, 0xD0, 0x54, 0xE7, 0xA7, 0xC4, 0x35, 0x3A, 0x05, 0x20,
|
||||
0x90, 0x31, 0x8C, 0x08, 0x7E, 0x03, 0x6B, 0xD7, 0xCA, 0x1B, 0xFC, 0x9E, 0xFC, 0x68, 0x94, 0xB8,
|
||||
0x0D, 0x91, 0x45, 0xE1, 0xF1, 0x0E, 0xCC, 0xFC, 0xD8, 0x32, 0x94, 0x92, 0x33, 0x61, 0x53, 0x6F,
|
||||
0xB1, 0xC9, 0xE7, 0x6E, 0x8B, 0x59, 0x0C, 0x9C, 0x7B, 0xFA, 0x6E, 0x31, 0x8F, 0xB7, 0x7D, 0xD7,
|
||||
0x67, 0x64, 0x64, 0xD2, 0x42, 0xA8, 0x84, 0x43, 0x84, 0xFC, 0xCD, 0x75, 0xF0, 0xA8, 0x42, 0x27,
|
||||
0x41, 0xD7, 0x2A, 0xD1, 0xA4, 0x49, 0x0B, 0x0A, 0x14, 0x78, 0x39, 0x50, 0xAE, 0x0A, 0x78, 0x28,
|
||||
0x0C, 0x13, 0xE3, 0x01, 0xE5, 0xED, 0x82, 0x0E, 0xC3, 0x9C, 0x91, 0xEB, 0x08, 0x0D, 0x44, 0x1F,
|
||||
0xC3, 0x2D, 0x80, 0x52, 0xCA, 0x2C, 0x2D, 0x1C, 0x9C, 0x67, 0x84, 0xC4, 0xE6, 0x86, 0xB2, 0x86,
|
||||
0x5A, 0xAE, 0xC3, 0x81, 0xF6, 0x1F, 0x93, 0x06, 0x9B, 0xC3, 0x46, 0x45, 0x25, 0xF4, 0x00, 0x0A,
|
||||
0x22, 0xA2, 0x92, 0x90, 0xC5, 0x34, 0xD9, 0xA0, 0xBC, 0x00, 0x7E, 0x15, 0x77, 0x80, 0x49, 0x1D,
|
||||
0x93, 0x86, 0x5F, 0x1C, 0x18, 0x4E, 0x0A, 0xBE, 0x14, 0xDD, 0x4E, 0x94, 0x93, 0xFB, 0xE9, 0x07,
|
||||
0xC4, 0x2F, 0x38, 0x50, 0x88, 0x80, 0x0E, 0x38, 0x30, 0xB7, 0xF1, 0xEA, 0x1A, 0xDA, 0x05, 0x6B,
|
||||
0xF5, 0x52, 0xAD, 0xCE, 0xBB, 0x76, 0x16, 0xD1, 0x01, 0x5E, 0x71, 0x15, 0x57, 0x42, 0xD0, 0xFB,
|
||||
0xAE, 0x88, 0x76, 0x35, 0xE7, 0xE6, 0x8F, 0x61, 0x31, 0x3B, 0xB5, 0xDD, 0x9A, 0xF2, 0x29, 0x35,
|
||||
0x19, 0x9D, 0x01, 0xE9, 0x95, 0xD1, 0x23, 0x8C, 0xA7, 0xFF, 0xD0, 0xD5, 0xF4, 0xBB, 0x8A, 0x3B,
|
||||
0x11, 0x43, 0x7E, 0x8F, 0x44, 0x47, 0x0B, 0xE2, 0x18, 0x88, 0x3C, 0x48, 0x07, 0x3F, 0x3D, 0x85,
|
||||
0x18, 0x05, 0x45, 0xCF, 0xB6, 0xE1, 0xDE, 0x0B, 0x4A, 0xB5, 0xBC, 0xB7, 0xCB, 0x3B, 0xDB, 0x79,
|
||||
0x62, 0xEB, 0x6F, 0xCF, 0xFB, 0xD3, 0xD9, 0xAE, 0x16, 0x8C, 0x34, 0x39, 0x95, 0xF5, 0xD1, 0x2E,
|
||||
0xF9, 0xA5, 0xBB, 0x79, 0x82, 0x77, 0xCE, 0x2C, 0x76, 0xC9, 0xB4, 0xD4, 0x67, 0xA5, 0x3F, 0x2A,
|
||||
0xF2, 0x17, 0x45, 0xE0, 0xFC, 0xCA, 0x3A, 0x8A, 0xD3, 0x1E, 0x00, 0x0C, 0x85, 0x6D, 0x51, 0x7E,
|
||||
0x3B, 0x7D, 0xA5, 0xF9, 0x60, 0x1C, 0x36, 0xB5, 0xE7, 0x4F, 0x4D, 0x48, 0x07, 0x61, 0xA6, 0x36,
|
||||
0xCE, 0xBF, 0x39, 0xB7, 0x3E, 0x14, 0xD4, 0x0C, 0x95, 0xC6, 0x33, 0xEF, 0x1F, 0x52, 0x5D, 0x6B,
|
||||
0x04, 0x07, 0xD6, 0x96, 0x41, 0x18, 0x3A, 0x91, 0x79, 0x88, 0xFE, 0x45, 0xC8, 0xBF, 0x5F, 0x32,
|
||||
0xA0, 0x40, 0x78, 0x14, 0x82, 0xDD, 0x5D, 0x84, 0xE5, 0x11, 0x08, 0xC4, 0xE0, 0x4D, 0xA2, 0x8E,
|
||||
0x12, 0x30, 0x08, 0x8F, 0x3D, 0xF8, 0x34, 0x79, 0x34, 0x74, 0x86, 0xF6, 0xB1, 0xE2, 0xCB, 0x63,
|
||||
0xD5, 0xED, 0xA1, 0xCB, 0xA0, 0x60, 0xCC, 0x4F, 0x5C, 0xE3, 0xD5, 0xB1, 0xD2, 0x9F, 0x7F, 0xFC,
|
||||
0x79, 0x19, 0xA9, 0x70, 0xF4, 0x0C, 0x55, 0xE2, 0x0D, 0x7C, 0xB8, 0x49, 0xAA, 0x5A, 0x6B, 0x91,
|
||||
0x1E, 0xAD, 0xCB, 0x8F, 0x37, 0xDF, 0xB9, 0x95, 0x93, 0xD9, 0x46, 0x88, 0xFA, 0xB2, 0xF9, 0x70,
|
||||
0x2B, 0x93, 0x34, 0x11, 0xE4, 0xAA, 0x08, 0x90, 0xB0, 0xD9, 0xF9, 0x2F, 0xD5, 0xD9, 0xFB, 0x37,
|
||||
0xB5, 0xD5, 0xFB, 0x44, 0x84, 0xB4, 0x12, 0x0B, 0x5B, 0x9E, 0x88, 0x86, 0x2F, 0xC4, 0xE0, 0x58,
|
||||
0x7B, 0x07, 0x33, 0x02, 0x81, 0x26, 0xE9, 0xC4, 0x49, 0x42, 0x61, 0x61, 0xA0, 0xA1, 0x34, 0xD0,
|
||||
0x5B, 0xEF, 0xEE, 0xF2, 0x66, 0x4B, 0x62, 0x83, 0x04, 0xB6, 0x43, 0x54, 0x52, 0xD5, 0x39, 0x44,
|
||||
0xDB, 0xE1, 0xE9, 0x42, 0x26, 0xB4, 0x93, 0x17, 0xE6, 0x53, 0x5A, 0x20, 0x8C, 0x07, 0x60, 0x4C,
|
||||
0x03, 0xBD, 0xC4, 0x2F, 0x05, 0x81, 0x3E, 0x0B, 0xD6, 0x00, 0x30, 0x15, 0x66, 0x59, 0x48, 0xE7,
|
||||
0x94, 0xA0, 0xE5, 0x3B, 0x19, 0xA1, 0x53, 0x1A, 0x7B, 0xCE, 0x1A, 0x95, 0x33, 0x88, 0x09, 0x34,
|
||||
0xDF, 0x19, 0x35, 0x2E, 0x6F, 0x0D, 0x76, 0x84, 0x11, 0x4F, 0x62, 0xA3, 0x5C, 0xFE, 0x09, 0x7B,
|
||||
0xDE, 0x38, 0xE6, 0xB3, 0x5B, 0x95, 0x37, 0x28, 0x27, 0x0F, 0x49, 0x80, 0xB8, 0x54, 0xD6, 0x2F,
|
||||
0x5A, 0xDF, 0xB4, 0xDE, 0x3A, 0xB7, 0xE1, 0xAC, 0x53, 0xD8, 0xBD, 0x5B, 0xEF, 0x1C, 0x19, 0x87,
|
||||
0x69, 0x31, 0x8B, 0xA4, 0x68, 0xD0, 0xC9, 0x3A, 0xEF, 0x1F, 0x94, 0x34, 0xA8, 0x54, 0x79, 0x0A,
|
||||
0x0D, 0xFC, 0xD8, 0x99, 0x9D, 0x33, 0x6E, 0x83, 0xE8, 0x13, 0x67, 0x52, 0x3D, 0xA0, 0x0F, 0xF3,
|
||||
0x5A, 0x71, 0x13, 0xF4, 0xD8, 0xFD, 0xEC, 0xDC, 0x2C, 0x18, 0xB6, 0x60, 0xFB, 0x4E, 0xBA, 0x48,
|
||||
0x67, 0x19, 0x16, 0x36, 0xD3, 0xF6, 0x61, 0x96, 0xC6, 0x65, 0x31, 0xCC, 0x0B, 0x6D, 0xC4, 0xC0,
|
||||
0xD3, 0xE5, 0xD4, 0x4C, 0x00, 0xCE, 0xF1, 0x4B, 0xDB, 0x66, 0x96, 0xCB, 0x1C, 0x7D, 0x4A, 0xB6,
|
||||
0x87, 0xF5, 0x63, 0x1B, 0x97, 0xCB, 0x1B, 0xD3, 0x9C, 0xF5, 0xE3, 0xCE, 0xFF, 0xA4, 0x6B, 0xCC,
|
||||
0x6D, 0x72, 0x91, 0x34, 0xDB, 0x11, 0x09, 0x67, 0xEE, 0xDE, 0xEC, 0x3D, 0x48, 0xFC, 0x8B, 0x8C,
|
||||
0x60, 0x45, 0xDA, 0xAC, 0x70, 0x3F, 0x80, 0x73, 0xCB, 0xDF, 0x90, 0x28, 0x46, 0x00, 0xDF, 0x65,
|
||||
0xEC, 0xA2, 0xD9, 0x59, 0x93, 0x73, 0x9A, 0xAC, 0xD7, 0x08, 0xE0, 0x95, 0xA6, 0x6F, 0xD4, 0xAE,
|
||||
0xB9, 0xF3, 0xA6, 0x91, 0x10, 0x81, 0x7A, 0x04, 0x23, 0x78, 0xF1, 0x0D, 0x86, 0xA1, 0x8E, 0xF0,
|
||||
0xE6, 0xB1, 0x01, 0xF6, 0x79, 0x93, 0x19, 0x97, 0xB1, 0xE0, 0x89, 0xC6, 0xEE, 0x00, 0x72, 0x0A,
|
||||
0x78, 0x3B, 0x7F, 0x43, 0x97, 0xFA, 0x34, 0x77, 0x4B, 0x67, 0x49, 0x7F, 0x89, 0xB0, 0x3E, 0x7C,
|
||||
0x76, 0x77, 0x9C, 0xEF, 0x17, 0x0A, 0x01, 0x61, 0xD1, 0xC2, 0xDA, 0x91, 0x38, 0xA2, 0xDB, 0x99,
|
||||
0xD5, 0xD6, 0x9E, 0xCB, 0x8A, 0x27, 0xEF, 0xAD, 0x1F, 0x05, 0xA7, 0x7A, 0xC6, 0x96, 0x3D, 0xC3,
|
||||
0x87, 0x9E, 0xD4, 0xEE, 0x49, 0xFC, 0x91, 0x5A, 0xC1, 0xF0, 0x5B, 0x99, 0xF7, 0xA0, 0x81, 0x23,
|
||||
0xC3, 0xA6, 0x9E, 0x4C, 0xEB, 0xC9, 0x48, 0x49, 0xB1, 0x30, 0x0B, 0x03, 0x68, 0x9F, 0xF3, 0xB9,
|
||||
0x9C, 0x19, 0x5A, 0x3D, 0xEB, 0x77, 0x54, 0xAD, 0x1E, 0x53, 0x7C, 0x9E, 0xA4, 0x99, 0xD3, 0x62,
|
||||
0x9D, 0xD3, 0x23, 0x31, 0xD8, 0xF0, 0x24, 0x80, 0xF5, 0x25, 0x6C, 0x82, 0x03, 0xA7, 0x13, 0x29,
|
||||
0x7A, 0x7A, 0x00, 0x4D, 0x1D, 0xD4, 0xBA, 0x87, 0xCF, 0xD8, 0xD3, 0x3D, 0x6C, 0x1D, 0x72, 0x5F,
|
||||
0xC4, 0x02, 0x9D, 0xEE, 0xA3, 0xDC, 0x69, 0x75, 0x1F, 0x96, 0x6E, 0xA3, 0x7B, 0xB8, 0x85, 0x5A,
|
||||
0xF7, 0x48, 0x2F, 0x4B, 0xC0, 0x06, 0x49, 0x65, 0xEF, 0xB4, 0x08, 0xCF, 0xB0, 0x21, 0x98, 0xD7,
|
||||
0x5E, 0xAD, 0x12, 0xC2, 0xC5, 0x31, 0xA1, 0xBD, 0xE1, 0x8B, 0x01, 0x61, 0x97, 0xB6, 0x10, 0xE6,
|
||||
0x5E, 0x53, 0x6C, 0x57, 0x24, 0xDA, 0x8A, 0x38, 0xAD, 0x88, 0xCC, 0x0A, 0xC6, 0x4D, 0xAD, 0x68,
|
||||
0x17, 0x56, 0x7D, 0x41, 0x00, 0x82, 0xAF, 0x7A, 0x68, 0xC4, 0x8A, 0xC4, 0x5E, 0xF5, 0xE0, 0xC4,
|
||||
0xAA, 0x07, 0x4F, 0x56, 0xE4, 0xC2, 0x8A, 0xFC, 0x58, 0x91, 0xB0, 0x2B, 0x12, 0x76, 0xC5, 0xD8,
|
||||
0x78, 0x45, 0xC2, 0x02, 0xA8, 0x81, 0x20, 0x23, 0x88, 0xB9, 0xA7, 0x90, 0xC0, 0x17, 0x0B, 0xD3,
|
||||
0x6F, 0x1D, 0xED, 0xC6, 0x7A, 0xC0, 0x0A, 0x76, 0xEB, 0x4E, 0xD1, 0x29, 0xBF, 0x63, 0xA0, 0x4A,
|
||||
0xC5, 0xC9, 0xC4, 0x7C, 0x32, 0x02, 0xA1, 0x81, 0x27, 0x73, 0xC3, 0x6A, 0x2D, 0xF1, 0x22, 0xF5,
|
||||
0xCD, 0x6F, 0x6C, 0x38, 0xDB, 0x9F, 0x82, 0xC3, 0x16, 0xFA, 0x8D, 0xED, 0x69, 0x47, 0xD9, 0x19,
|
||||
0xA8, 0xCD, 0x71, 0x1E, 0x55, 0x9C, 0x19, 0xD6, 0x14, 0xE6, 0xDE, 0xB5, 0x2E, 0x57, 0xF2, 0x02,
|
||||
0x1B, 0x43, 0x29, 0x8A, 0x1E, 0x5E, 0x7C, 0x48, 0x43, 0x47, 0x93, 0x82, 0x99, 0x83, 0x8E, 0xDB,
|
||||
0xE9, 0xAD, 0xA5, 0x3C, 0x99, 0x1A, 0xFF, 0xEE, 0xED, 0xFB, 0xA8, 0x31, 0x62, 0xAA, 0x46, 0xF0,
|
||||
0xF9, 0xC6, 0x13, 0x98, 0x9E, 0xFF, 0xFB, 0xDE, 0xCB, 0x47, 0xEE, 0xDD, 0x9C, 0xF2, 0xE9, 0xE2,
|
||||
0x75, 0x88, 0xD2, 0x54, 0xC5, 0xC1, 0xB5, 0x4C, 0x49, 0x19, 0x21, 0x8D, 0x12, 0x2F, 0x4D, 0xE2,
|
||||
0xCC, 0xFA, 0x29, 0x35, 0x03, 0xB8, 0xF3, 0x19, 0xC8, 0x57, 0x51, 0xDC, 0x4E, 0x8D, 0xFB, 0x87,
|
||||
0x5B, 0x5F, 0x2B, 0x3F, 0x9D, 0x52, 0xC8, 0x7C, 0x56, 0x6B, 0xBF, 0x99, 0xB5, 0xDC, 0xCE, 0xB7,
|
||||
0xDE, 0xB9, 0x63, 0x1B, 0x9F, 0x1E, 0x9A, 0x1B, 0xD0, 0x99, 0x3E, 0x1C, 0x6F, 0x76, 0x10, 0xC6,
|
||||
0xE7, 0xC5, 0xCC, 0xCE, 0xB9, 0x53, 0x6A, 0x7B, 0xD1, 0x76, 0xFB, 0x62, 0x02, 0x36, 0xCC, 0x7C,
|
||||
0x98, 0x73, 0x32, 0x6B, 0xEF, 0x27, 0xC3, 0xBD, 0x9E, 0x52, 0x0B, 0xB6, 0x93, 0xE7, 0xB0, 0x4E,
|
||||
0xF6, 0xFE, 0x5B, 0x4C, 0xD9, 0x90, 0x8F, 0x8D, 0x93, 0x33, 0xC5, 0xB6, 0x76, 0xE0, 0x41, 0x29,
|
||||
0xDC, 0x33, 0x07, 0x4F, 0x69, 0xC9, 0x1F, 0x09, 0x3C, 0xCA, 0x82, 0xD7, 0x64, 0x94, 0x07, 0xC4,
|
||||
0x12, 0x71, 0x19, 0x6E, 0xDE, 0x34, 0xA9, 0x27, 0x2E, 0x2B, 0x93, 0xFC, 0xF9, 0x9A, 0x2D, 0x87,
|
||||
0x2C, 0x48, 0x9A, 0x35, 0x49, 0x49, 0x6D, 0x77, 0x60, 0x69, 0x30, 0x01, 0x8C, 0x32, 0x2C, 0x4B,
|
||||
0x05, 0xCE, 0x20, 0x10, 0x93, 0x30, 0x14, 0xA6, 0xA7, 0x85, 0x5D, 0x76, 0xFA, 0x6D, 0x32, 0xC3,
|
||||
0xCA, 0x11, 0x38, 0x39, 0x4C, 0xC1, 0x55, 0xFC, 0x63, 0x08, 0x4C, 0xF8, 0xC7, 0x66, 0x02, 0xFE,
|
||||
0xDF, 0x19, 0x36, 0xFF, 0x4B, 0xAC, 0x5D, 0x49, 0xC3, 0x97, 0x4E, 0x31, 0x30, 0x59, 0xB1, 0x90,
|
||||
0xB0, 0xD9, 0x6D, 0x33, 0x46, 0x2B, 0x58, 0x09, 0xB3, 0x21, 0xED, 0xF4, 0x1A, 0x96, 0x3B, 0x90,
|
||||
0xB9, 0xD5, 0xC0, 0x61, 0xD1, 0x60, 0x5D, 0xD6, 0x4F, 0x18, 0x05, 0xE2, 0xD9, 0x33, 0x50, 0x0C,
|
||||
0x2A, 0xA7, 0x5B, 0x01, 0x0B, 0x56, 0xB5, 0x8E, 0x11, 0x11, 0x7A, 0x8D, 0x87, 0xA8, 0x56, 0x63,
|
||||
0x24, 0x2C, 0x22, 0x9E, 0x6E, 0xEB, 0x57, 0x31, 0xA2, 0x43, 0xA7, 0xC3, 0x98, 0x1C, 0x61, 0x67,
|
||||
0xCF, 0xB1, 0xA4, 0x72, 0x94, 0x06, 0x7C, 0x9E, 0xC4, 0xCA, 0x67, 0x68, 0xBA, 0xD0, 0x36, 0x50,
|
||||
0xD5, 0x89, 0x72, 0xFF, 0xC3, 0xE7, 0xB1, 0x0D, 0x5F, 0x23, 0x50, 0xE4, 0xCB, 0xCA, 0x56, 0x05,
|
||||
0xCC, 0x24, 0x55, 0x0C, 0x71, 0x99, 0x27, 0x52, 0x39, 0x58, 0xF1, 0xD0, 0xAC, 0xC9, 0xC0, 0x7F,
|
||||
0xC7, 0x13, 0x8D, 0x42, 0x92, 0x27, 0xCA, 0x99, 0xE6, 0x51, 0xD6, 0xB0, 0x6A, 0xC6, 0xE8, 0x3D,
|
||||
0x7E, 0x97, 0xA4, 0xF8, 0x73, 0x45, 0xB1, 0x64, 0xD9, 0xD7, 0xCC, 0xD5, 0x0A, 0xF9, 0xCB, 0xA3,
|
||||
0x84, 0x86, 0x1F, 0x33, 0x23, 0x61, 0x3F, 0x4D, 0x87, 0xC6, 0xE2, 0x1B, 0x46, 0x66, 0x74, 0xB5,
|
||||
0x71, 0x8E, 0x54, 0x83, 0xF5, 0x2D, 0x2C, 0xAD, 0x19, 0x92, 0xAD, 0x19, 0xA1, 0x9D, 0xB0, 0xF6,
|
||||
0x92, 0x91, 0xAF, 0x40, 0x22, 0xA8, 0xC5, 0x7B, 0xC2, 0x30, 0xC7, 0x39, 0x46, 0x68, 0x16, 0x5C,
|
||||
0x4E, 0xAC, 0xE2, 0xA9, 0x35, 0x03, 0x79, 0x4D, 0x6C, 0xA3, 0x9F, 0x72, 0x3B, 0x9F, 0xE7, 0x53,
|
||||
0xCA, 0xC6, 0x38, 0x67, 0x4A, 0x97, 0x9F, 0x00, 0xEC, 0x8A, 0x73, 0xD9, 0x94, 0x0A, 0xAC, 0xB7,
|
||||
0x45, 0x94, 0x29, 0x1B, 0x9D, 0x03, 0x27, 0x29, 0x9F, 0x3E, 0xFD, 0x8F, 0x48, 0xF5, 0x72, 0x2A,
|
||||
0x60, 0x29, 0x20, 0x63, 0xE0, 0xA4, 0xB2, 0x25, 0x05, 0xE6, 0x99, 0xDC, 0x76, 0xC2, 0xCB, 0xFB,
|
||||
0x92, 0x95, 0x25, 0x93, 0x09, 0x61, 0x29, 0x84, 0x99, 0x06, 0x4B, 0xC9, 0x52, 0xE0, 0x6C, 0xA6,
|
||||
0x24, 0xBB, 0x9B, 0x48, 0x70, 0x3C, 0x75, 0x8C, 0xA7, 0x4A, 0x85, 0x74, 0x9E, 0xB2, 0x42, 0x76,
|
||||
0x66, 0x39, 0x51, 0x3A, 0xAE, 0x26, 0x0B, 0x03, 0x59, 0x64, 0xB2, 0x9A, 0xBE, 0x54, 0xB0, 0x5C,
|
||||
0xF2, 0x4C, 0x6E, 0xCC, 0x86, 0x1B, 0x98, 0x2B, 0x1E, 0x25, 0x88, 0x20, 0x2A, 0x1B, 0xAF, 0x72,
|
||||
0xD1, 0x84, 0xD9, 0xE7, 0x55, 0xE2, 0xF7, 0xBB, 0x10, 0xBB, 0x69, 0x03, 0x56, 0xBE, 0x3D, 0xA8,
|
||||
0x56, 0xE5, 0x36, 0xDC, 0x57, 0xCB, 0x9A, 0xF7, 0x8A, 0x1B, 0xD8, 0xE3, 0xB6, 0xE5, 0x34, 0x21,
|
||||
0x4C, 0xB7, 0xB9, 0xF7, 0x9C, 0x57, 0xBF, 0x0D, 0x2E, 0x44, 0xA5, 0x5F, 0x63, 0x43, 0x3F, 0x54,
|
||||
0x36, 0x41, 0x4D, 0x11, 0xA6, 0xD4, 0xDE, 0x9D, 0x2B, 0xAD, 0x23, 0x05, 0xBD, 0x53, 0xDE, 0xEC,
|
||||
0xD2, 0xCD, 0xDE, 0xA5, 0x51, 0xBE, 0xBB, 0xDF, 0x43, 0x35, 0x18, 0x83, 0xF5, 0x0B, 0x69, 0x30,
|
||||
0xAE, 0xCA, 0xC3, 0xBB, 0xB8, 0x8F, 0xCC, 0x70, 0xE4, 0x9B, 0xFA, 0x88, 0x85, 0xA9, 0xCB, 0xE7,
|
||||
0xD4, 0xE8, 0xCE, 0x03, 0x49, 0x00, 0x01, 0x4B, 0x32, 0x96, 0xA3, 0x36, 0x2C, 0x47, 0x55, 0xB6,
|
||||
0x1C, 0x95, 0xF1, 0x57, 0xFD, 0xD0, 0xEA, 0xA9, 0xFC, 0x80, 0x2E, 0x52, 0xB7, 0x29, 0x39, 0xAC,
|
||||
0x83, 0xFD, 0xDF, 0x2B, 0x30, 0xD5, 0xD5, 0x68, 0xAE, 0x4E, 0x4D, 0x55, 0xFE, 0x1B, 0x0E, 0xB7,
|
||||
0x35, 0x7E, 0xB7, 0xA2, 0xCF, 0xA5, 0xFC, 0xC5, 0xE9, 0xD9, 0x7A, 0xDC, 0x99, 0x96, 0xDA, 0x3E,
|
||||
0xEC, 0xEB, 0x0F, 0x17, 0x6B, 0x8E, 0x45, 0xD2, 0x57, 0x4D, 0x13, 0x5E, 0x54, 0x73, 0x05, 0xEB,
|
||||
0x61, 0x20, 0x1E, 0xC2, 0x1C, 0xF4, 0xFA, 0x38, 0xF7, 0x37, 0xD7, 0x4D, 0xCE, 0x02, 0x7F, 0x8E,
|
||||
0x39, 0x75, 0x8E, 0xC9, 0xE3, 0x73, 0x36, 0x13, 0x9E, 0xFE, 0xCA, 0x2E, 0xAC, 0x3A, 0xE5, 0x30,
|
||||
0x37, 0x45, 0x1E, 0x74, 0x97, 0x8C, 0x9D, 0x4A, 0x65, 0x23, 0xF2, 0xF7, 0xDD, 0xE9, 0x45, 0xCC,
|
||||
0xBC, 0x94, 0xC8, 0x49, 0x8D, 0x58, 0x7C, 0x2B, 0x56, 0x5E, 0x6F, 0xAB, 0xCF, 0x00, 0xCB, 0x65,
|
||||
0x31, 0xD5, 0x52, 0xA4, 0xAA, 0xB2, 0xA3, 0x7D, 0x3A, 0x2B, 0x22, 0x77, 0x89, 0x6D, 0xBE, 0xF6,
|
||||
0x83, 0x29, 0xC1, 0x89, 0x19, 0x14, 0x4A, 0x75, 0xC9, 0xD9, 0xB7, 0xAE, 0x15, 0x82, 0x28, 0x96,
|
||||
0x6C, 0x2A, 0x05, 0xCF, 0xED, 0x8A, 0x61, 0xA5, 0xB5, 0x57, 0xDF, 0xD8, 0x5C, 0x3B, 0xF3, 0xFE,
|
||||
0x49, 0xDB, 0xB4, 0x97, 0x21, 0xC8, 0x1D, 0xA1, 0xD7, 0x70, 0x20, 0xF4, 0x32, 0x96, 0x5E, 0xF1,
|
||||
0x7D, 0xF4, 0x0A, 0xCF, 0x02, 0xA1, 0x57, 0xB4, 0xA5, 0x57, 0xB6, 0xA5, 0xD7, 0x3F, 0xB6, 0xF4,
|
||||
0x0A, 0xB7, 0x8C, 0x89, 0x25, 0xD7, 0x26, 0x0B, 0x8D, 0x0A, 0x3F, 0x85, 0xBC, 0x05, 0x74, 0xFE,
|
||||
0x4E, 0x01, 0x42, 0x5E, 0x56, 0x0F, 0x91, 0x08, 0xB6, 0xC0, 0xE4, 0x90, 0xD3, 0x56, 0x6F, 0xAE,
|
||||
0xF7, 0x90, 0xD2, 0x01, 0x72, 0xB7, 0x20, 0x8D, 0x9F, 0xB4, 0xBD, 0x00, 0xD9, 0xCE, 0x6C, 0x8D,
|
||||
0x4F, 0x8C, 0x92, 0x7E, 0xE4, 0x47, 0xBD, 0x20, 0xC9, 0x69, 0xA6, 0xB6, 0xA3, 0xA6, 0x2A, 0xA1,
|
||||
0xFE, 0x0A, 0x77, 0x42, 0xA3, 0xFE, 0xAB, 0x5E, 0x99, 0x4E, 0x15, 0x95, 0x54, 0xA9, 0x92, 0x87,
|
||||
0x36, 0xB7, 0x4B, 0xE0, 0x82, 0x12, 0x30, 0x23, 0x81, 0x4D, 0x4F, 0x10, 0xD7, 0x27, 0x30, 0xE6,
|
||||
0x09, 0x78, 0x94, 0x10, 0x31, 0x88, 0x88, 0x86, 0xDF, 0xD3, 0xC8, 0xC5, 0xB4, 0xC7, 0x0A, 0xD5,
|
||||
0x80, 0xC6, 0x6D, 0xC6, 0x65, 0x02, 0x11, 0xE3, 0xC1, 0x42, 0x02, 0x4B, 0x71, 0x59, 0x3F, 0xE8,
|
||||
0x9A, 0x76, 0x75, 0x15, 0xE6, 0x20, 0x1E, 0xD5, 0x5E, 0x66, 0x3E, 0xE1, 0x99, 0xC4, 0xE6, 0xE0,
|
||||
0xCB, 0x3B, 0x87, 0x08, 0x68, 0x5C, 0xC4, 0x32, 0x0D, 0x28, 0x6E, 0x60, 0xA9, 0xC5, 0xED, 0xCA,
|
||||
0x03, 0x01, 0xE4, 0xB3, 0x7D, 0x4C, 0x18, 0xE4, 0x7B, 0x3B, 0x3E, 0x8F, 0x4F, 0xCB, 0xE7, 0xB9,
|
||||
0x3E, 0x9D, 0x53, 0xAF, 0x5A, 0xE5, 0x0D, 0xD7, 0x25, 0xE2, 0xCF, 0xA8, 0x75, 0x31, 0xC5, 0x9A,
|
||||
0xC7, 0x83, 0x13, 0x60, 0xF0, 0x1E, 0x6C, 0x81, 0x7C, 0x0D, 0x4B, 0xE9, 0x22, 0x9D, 0x65, 0x98,
|
||||
0xB7, 0xB9, 0x34, 0xD8, 0xA9, 0x76, 0xB5, 0x77, 0xB9, 0xF6, 0x80, 0x30, 0x09, 0xF6, 0x55, 0x2C,
|
||||
0x68, 0x76, 0xCA, 0x82, 0x26, 0xB4, 0xA0, 0x89, 0x28, 0x3E, 0xD7, 0x78, 0x0C, 0x1B, 0x7E, 0xAF,
|
||||
0x06, 0x3F, 0x2B, 0x8B, 0x27, 0x1A, 0x1C, 0x51, 0x83, 0x1F, 0x69, 0x6E, 0x27, 0x70, 0x35, 0xE8,
|
||||
0x91, 0x49, 0xEF, 0xAD, 0x7F, 0x81, 0xB8, 0x12, 0x3B, 0xB2, 0xB1, 0xDB, 0x23, 0x6F, 0x7B, 0x84,
|
||||
0x32, 0xAA, 0x90, 0x22, 0x57, 0xBD, 0x25, 0xE5, 0xF4, 0xC5, 0xAC, 0xF6, 0x8C, 0xA7, 0xA4, 0x8A,
|
||||
0x5C, 0xDB, 0x33, 0x3F, 0x87, 0xC4, 0x8F, 0xA6, 0x90, 0x80, 0x75, 0xD4, 0xA0, 0x3A, 0x56, 0x87,
|
||||
0x25, 0x8B, 0xC8, 0xDB, 0x43, 0x99, 0x86, 0xB7, 0x23, 0x45, 0x94, 0xC8, 0xF4, 0x99, 0xE9, 0xBF,
|
||||
0xF9, 0xC5, 0xB5, 0xC7, 0x29, 0xB7, 0xF0, 0xE4, 0x06, 0x36, 0x38, 0x06, 0xA3, 0x59, 0xF4, 0x4C,
|
||||
0xD0, 0xF4, 0x63, 0xB6, 0x0C, 0xAD, 0x00, 0xC2, 0x46, 0xA7, 0xAE, 0x89, 0xEB, 0x2B, 0x06, 0x9B,
|
||||
0xD1, 0xAE, 0xE9, 0x62, 0x1A, 0xBB, 0x93, 0xA9, 0x20, 0x6B, 0x19, 0x4E, 0x2B, 0xB8, 0x20, 0xBD,
|
||||
0x06, 0x86, 0x36, 0xB0, 0x81, 0x08, 0xB3, 0x19, 0x53, 0x37, 0x16, 0x38, 0x1D, 0x77, 0xD6, 0x33,
|
||||
0xEA, 0x59, 0x91, 0xA8, 0x0B, 0x39, 0xBB, 0x92, 0x80, 0xE8, 0x78, 0x3E, 0x41, 0x0B, 0x69, 0xB7,
|
||||
0x59, 0x0E, 0x3F, 0x66, 0x86, 0xEF, 0xD7, 0x8C, 0x00, 0x44, 0x78, 0xBD, 0x41, 0x24, 0x37, 0x30,
|
||||
0x94, 0xDD, 0x3C, 0xBC, 0x85, 0x65, 0x10, 0x93, 0xBF, 0x95, 0xF2, 0x14, 0x29, 0x22, 0xCB, 0xD1,
|
||||
0x89, 0x32, 0xEF, 0xD7, 0x89, 0x0F, 0xBA, 0x79, 0xCD, 0xB1, 0x57, 0xAF, 0x58, 0x49, 0x60, 0x7A,
|
||||
0xEB, 0xB6, 0x9D, 0xDB, 0x60, 0xE6, 0x4D, 0xAB, 0xBE, 0xC0, 0xDC, 0x63, 0xA3, 0xEE, 0x82, 0x92,
|
||||
0xEB, 0x5A, 0x72, 0xBB, 0xAC, 0x92, 0x73, 0x07, 0x72, 0xB5, 0x92, 0xE8, 0x1C, 0x01, 0xF9, 0x7C,
|
||||
0x3C, 0xB5, 0xB6, 0x39, 0xA5, 0x2B, 0x87, 0xF4, 0x52, 0x28, 0xCA, 0x88, 0x7E, 0xCC, 0xE0, 0x33,
|
||||
0x1C, 0xE7, 0xB3, 0x3F, 0xC9, 0x0E, 0xE5, 0xAB, 0x3E, 0xE4, 0x11, 0xA6, 0x94, 0x9B, 0x7D, 0x12,
|
||||
0x1E, 0xA1, 0x0E, 0x06, 0x0D, 0x64, 0x6B, 0xA6, 0x44, 0x18, 0xCF, 0xA3, 0x98, 0x1C, 0xEF, 0x0A,
|
||||
0xC5, 0x10, 0x32, 0x4D, 0x14, 0x73, 0xE5, 0x5F, 0x1A, 0x76, 0x14, 0x3B, 0xA6, 0xAF, 0xD1, 0x74,
|
||||
0x33, 0xB1, 0x24, 0xE4, 0xEB, 0x99, 0x62, 0xFE, 0x36, 0x06, 0x96, 0xF3, 0x2D, 0xBF, 0x3E, 0x73,
|
||||
0xC8, 0x45, 0x77, 0xFC, 0x26, 0x31, 0x2B, 0xD8, 0x8D, 0x27, 0x17, 0xF4, 0xDA, 0x25, 0xDB, 0xE5,
|
||||
0xFD, 0xC3, 0xDD, 0x99, 0xAB, 0x63, 0xFF, 0x2D, 0x41, 0x46, 0x46, 0xD3, 0x49, 0xC4, 0x60, 0xDD,
|
||||
0x28, 0x5F, 0x43, 0x7B, 0xE4, 0xBC, 0x2C, 0xDB, 0xDF, 0x4F, 0x59, 0x56, 0x2D, 0xA4, 0x40, 0xF6,
|
||||
0x7D, 0xA6, 0x01, 0xE1, 0xC6, 0x9E, 0x70, 0x9D, 0x7F, 0x9F, 0xA8, 0x97, 0x3B, 0x39, 0x05, 0x6C,
|
||||
0xE5, 0xA0, 0x63, 0xF1, 0x20, 0x5D, 0x81, 0xA7, 0xBD, 0xCD, 0xD3, 0x15, 0x47, 0x8E, 0x36, 0xF6,
|
||||
0x1E, 0x75, 0xA9, 0x5F, 0x43, 0x20, 0x73, 0xB2, 0x0F, 0x3B, 0x27, 0x92, 0xF7, 0xDF, 0x52, 0x25,
|
||||
0x50, 0xFA, 0x36, 0x0B, 0x10, 0x11, 0x7C, 0x51, 0x6A, 0x4F, 0x5E, 0xEF, 0xB9, 0x31, 0x96, 0xF9,
|
||||
0x68, 0xC3, 0xA3, 0x07, 0x5B, 0xA2, 0xF0, 0x91, 0xF7, 0x23, 0x44, 0xBE, 0x1D, 0xA4, 0x8B, 0x43,
|
||||
0xE4, 0x28, 0xDF, 0xE3, 0x31, 0xB6, 0x1C, 0xCB, 0xFD, 0x0F, 0xD5, 0x5A, 0x3D, 0x8C, 0xDC, 0xD6,
|
||||
0x11, 0x1E, 0x92, 0xD2, 0xD1, 0x10, 0x81, 0x50, 0x85, 0x64, 0x04, 0x16, 0x2C, 0xBA, 0x48, 0x11,
|
||||
0x0B, 0xB6, 0xD8, 0xBA, 0xB8, 0x5B, 0xD2, 0x48, 0x91, 0x22, 0x85, 0x55, 0xE8, 0x2C, 0x20, 0x2E,
|
||||
0x54, 0xC7, 0x41, 0x70, 0x29, 0x5C, 0xA8, 0x58, 0x2D, 0xE9, 0x2A, 0x65, 0xAE, 0x71, 0x95, 0x46,
|
||||
@@ -2225,85 +2225,85 @@ static const EpdGlyph notosans_16_regularGlyphs[] = {
|
||||
{ 9, 4, 172, 1, 11, 9, 898 }, // -
|
||||
{ 5, 6, 143, 2, 5, 8, 907 }, // .
|
||||
{ 13, 24, 198, 0, 24, 78, 915 }, // /
|
||||
{ 17, 26, 305, 1, 25, 111, 993 }, // 0
|
||||
{ 10, 24, 305, 2, 24, 60, 1104 }, // 1
|
||||
{ 17, 25, 305, 1, 25, 107, 1164 }, // 2
|
||||
{ 17, 26, 305, 1, 25, 111, 1271 }, // 3
|
||||
{ 19, 24, 305, 0, 24, 114, 1382 }, // 4
|
||||
{ 16, 25, 305, 2, 24, 100, 1496 }, // 5
|
||||
{ 17, 26, 305, 1, 25, 111, 1596 }, // 6
|
||||
{ 17, 24, 305, 1, 24, 102, 1707 }, // 7
|
||||
{ 17, 26, 305, 1, 25, 111, 1809 }, // 8
|
||||
{ 17, 26, 305, 1, 25, 111, 1920 }, // 9
|
||||
{ 5, 20, 143, 2, 19, 25, 2031 }, // :
|
||||
{ 6, 24, 143, 1, 19, 36, 2056 }, // ;
|
||||
{ 17, 18, 305, 1, 21, 77, 2092 }, // <
|
||||
{ 17, 10, 305, 1, 17, 43, 2169 }, // =
|
||||
{ 17, 18, 305, 1, 21, 77, 2212 }, // >
|
||||
{ 14, 26, 231, 0, 25, 91, 2289 }, // ?
|
||||
{ 28, 27, 479, 1, 24, 189, 2380 }, // @
|
||||
{ 22, 24, 341, 0, 24, 132, 2569 }, // A
|
||||
{ 17, 24, 347, 3, 24, 102, 2701 }, // B
|
||||
{ 19, 26, 337, 2, 25, 124, 2803 }, // C
|
||||
{ 20, 24, 389, 3, 24, 120, 2927 }, // D
|
||||
{ 14, 24, 296, 3, 24, 84, 3047 }, // E
|
||||
{ 14, 24, 277, 3, 24, 84, 3131 }, // F
|
||||
{ 20, 26, 388, 2, 25, 130, 3215 }, // G
|
||||
{ 19, 24, 395, 3, 24, 114, 3345 }, // H
|
||||
{ 9, 24, 181, 1, 24, 54, 3459 }, // I
|
||||
{ 10, 31, 146, -3, 24, 78, 3513 }, // J
|
||||
{ 18, 24, 330, 3, 24, 108, 3591 }, // K
|
||||
{ 14, 24, 279, 3, 24, 84, 3699 }, // L
|
||||
{ 24, 24, 484, 3, 24, 144, 3783 }, // M
|
||||
{ 20, 24, 405, 3, 24, 120, 3927 }, // N
|
||||
{ 22, 26, 416, 2, 25, 143, 4047 }, // O
|
||||
{ 16, 24, 323, 3, 24, 96, 4190 }, // P
|
||||
{ 22, 31, 416, 2, 25, 171, 4286 }, // Q
|
||||
{ 18, 24, 332, 3, 24, 108, 4457 }, // R
|
||||
{ 16, 26, 293, 1, 25, 104, 4565 }, // S
|
||||
{ 19, 24, 296, 0, 24, 114, 4669 }, // T
|
||||
{ 19, 25, 390, 3, 24, 119, 4783 }, // U
|
||||
{ 20, 24, 320, 0, 24, 120, 4902 }, // V
|
||||
{ 31, 24, 496, 0, 24, 186, 5022 }, // W
|
||||
{ 20, 24, 312, 0, 24, 120, 5208 }, // X
|
||||
{ 19, 24, 302, 0, 24, 114, 5328 }, // Y
|
||||
{ 17, 24, 305, 1, 24, 102, 5442 }, // Z
|
||||
{ 9, 30, 175, 2, 24, 68, 5544 }, // [
|
||||
{ 13, 24, 198, 0, 24, 78, 5612 }, // <backslash>
|
||||
{ 9, 30, 175, 0, 24, 68, 5690 }, // ]
|
||||
{ 17, 16, 305, 1, 24, 68, 5758 }, // ^
|
||||
{ 16, 3, 237, -1, -3, 12, 5826 }, // _
|
||||
{ 8, 6, 150, 1, 26, 12, 5838 }, // `
|
||||
{ 15, 20, 299, 1, 19, 75, 5850 }, // a
|
||||
{ 17, 27, 328, 2, 26, 115, 5925 }, // b
|
||||
{ 14, 20, 256, 1, 19, 70, 6040 }, // c
|
||||
{ 17, 27, 328, 1, 26, 115, 6110 }, // d
|
||||
{ 17, 20, 301, 1, 19, 85, 6225 }, // e
|
||||
{ 13, 26, 183, 0, 26, 85, 6310 }, // f
|
||||
{ 17, 27, 328, 1, 19, 115, 6395 }, // g
|
||||
{ 16, 26, 330, 2, 26, 104, 6510 }, // h
|
||||
{ 5, 25, 138, 2, 25, 32, 6614 }, // i
|
||||
{ 9, 33, 138, -2, 25, 75, 6646 }, // j
|
||||
{ 16, 26, 285, 2, 26, 104, 6721 }, // k
|
||||
{ 4, 26, 138, 2, 26, 26, 6825 }, // l
|
||||
{ 27, 19, 499, 2, 19, 129, 6851 }, // m
|
||||
{ 16, 19, 330, 2, 19, 76, 6980 }, // n
|
||||
{ 18, 20, 323, 1, 19, 90, 7056 }, // o
|
||||
{ 17, 27, 328, 2, 19, 115, 7146 }, // p
|
||||
{ 17, 27, 328, 1, 19, 115, 7261 }, // q
|
||||
{ 12, 19, 220, 2, 19, 57, 7376 }, // r
|
||||
{ 14, 20, 255, 1, 19, 70, 7433 }, // s
|
||||
{ 12, 23, 193, 0, 22, 69, 7503 }, // t
|
||||
{ 16, 19, 330, 2, 18, 76, 7572 }, // u
|
||||
{ 17, 18, 271, 0, 18, 77, 7648 }, // v
|
||||
{ 26, 18, 419, 0, 18, 117, 7725 }, // w
|
||||
{ 18, 18, 282, 0, 18, 81, 7842 }, // x
|
||||
{ 17, 26, 272, 0, 18, 111, 7923 }, // y
|
||||
{ 14, 18, 251, 1, 18, 63, 8034 }, // z
|
||||
{ 12, 30, 203, 0, 24, 90, 8097 }, // {
|
||||
{ 4, 35, 294, 7, 26, 35, 8187 }, // |
|
||||
{ 11, 30, 203, 1, 24, 83, 8222 }, // }
|
||||
{ 17, 5, 305, 1, 14, 22, 8305 }, // ~
|
||||
{ 17, 26, 311, 1, 25, 111, 993 }, // 0
|
||||
{ 10, 24, 235, 0, 24, 60, 1104 }, // 1
|
||||
{ 16, 25, 296, 1, 25, 100, 1164 }, // 2
|
||||
{ 17, 26, 305, 1, 25, 111, 1264 }, // 3
|
||||
{ 19, 24, 305, 0, 24, 114, 1375 }, // 4
|
||||
{ 16, 25, 305, 2, 24, 100, 1489 }, // 5
|
||||
{ 17, 26, 305, 1, 25, 111, 1589 }, // 6
|
||||
{ 17, 24, 272, 0, 24, 102, 1700 }, // 7
|
||||
{ 17, 26, 314, 1, 25, 111, 1802 }, // 8
|
||||
{ 17, 26, 305, 1, 25, 111, 1913 }, // 9
|
||||
{ 5, 20, 143, 2, 19, 25, 2024 }, // :
|
||||
{ 6, 24, 143, 1, 19, 36, 2049 }, // ;
|
||||
{ 17, 18, 305, 1, 21, 77, 2085 }, // <
|
||||
{ 17, 10, 305, 1, 17, 43, 2162 }, // =
|
||||
{ 17, 18, 305, 1, 21, 77, 2205 }, // >
|
||||
{ 14, 26, 231, 0, 25, 91, 2282 }, // ?
|
||||
{ 28, 27, 479, 1, 24, 189, 2373 }, // @
|
||||
{ 22, 24, 341, 0, 24, 132, 2562 }, // A
|
||||
{ 17, 24, 347, 3, 24, 102, 2694 }, // B
|
||||
{ 19, 26, 337, 2, 25, 124, 2796 }, // C
|
||||
{ 20, 24, 389, 3, 24, 120, 2920 }, // D
|
||||
{ 14, 24, 296, 3, 24, 84, 3040 }, // E
|
||||
{ 14, 24, 277, 3, 24, 84, 3124 }, // F
|
||||
{ 20, 26, 388, 2, 25, 130, 3208 }, // G
|
||||
{ 19, 24, 395, 3, 24, 114, 3338 }, // H
|
||||
{ 9, 24, 181, 1, 24, 54, 3452 }, // I
|
||||
{ 10, 31, 146, -3, 24, 78, 3506 }, // J
|
||||
{ 18, 24, 330, 3, 24, 108, 3584 }, // K
|
||||
{ 14, 24, 279, 3, 24, 84, 3692 }, // L
|
||||
{ 24, 24, 484, 3, 24, 144, 3776 }, // M
|
||||
{ 20, 24, 405, 3, 24, 120, 3920 }, // N
|
||||
{ 22, 26, 416, 2, 25, 143, 4040 }, // O
|
||||
{ 16, 24, 323, 3, 24, 96, 4183 }, // P
|
||||
{ 22, 31, 416, 2, 25, 171, 4279 }, // Q
|
||||
{ 18, 24, 332, 3, 24, 108, 4450 }, // R
|
||||
{ 16, 26, 293, 1, 25, 104, 4558 }, // S
|
||||
{ 19, 24, 296, 0, 24, 114, 4662 }, // T
|
||||
{ 19, 25, 390, 3, 24, 119, 4776 }, // U
|
||||
{ 20, 24, 320, 0, 24, 120, 4895 }, // V
|
||||
{ 31, 24, 496, 0, 24, 186, 5015 }, // W
|
||||
{ 20, 24, 312, 0, 24, 120, 5201 }, // X
|
||||
{ 19, 24, 302, 0, 24, 114, 5321 }, // Y
|
||||
{ 17, 24, 305, 1, 24, 102, 5435 }, // Z
|
||||
{ 9, 30, 175, 2, 24, 68, 5537 }, // [
|
||||
{ 13, 24, 198, 0, 24, 78, 5605 }, // <backslash>
|
||||
{ 9, 30, 175, 0, 24, 68, 5683 }, // ]
|
||||
{ 17, 16, 305, 1, 24, 68, 5751 }, // ^
|
||||
{ 16, 3, 237, -1, -3, 12, 5819 }, // _
|
||||
{ 8, 6, 150, 1, 26, 12, 5831 }, // `
|
||||
{ 15, 20, 299, 1, 19, 75, 5843 }, // a
|
||||
{ 17, 27, 328, 2, 26, 115, 5918 }, // b
|
||||
{ 14, 20, 256, 1, 19, 70, 6033 }, // c
|
||||
{ 17, 27, 328, 1, 26, 115, 6103 }, // d
|
||||
{ 17, 20, 301, 1, 19, 85, 6218 }, // e
|
||||
{ 13, 26, 183, 0, 26, 85, 6303 }, // f
|
||||
{ 17, 27, 328, 1, 19, 115, 6388 }, // g
|
||||
{ 16, 26, 330, 2, 26, 104, 6503 }, // h
|
||||
{ 5, 25, 138, 2, 25, 32, 6607 }, // i
|
||||
{ 9, 33, 138, -2, 25, 75, 6639 }, // j
|
||||
{ 16, 26, 285, 2, 26, 104, 6714 }, // k
|
||||
{ 4, 26, 138, 2, 26, 26, 6818 }, // l
|
||||
{ 27, 19, 499, 2, 19, 129, 6844 }, // m
|
||||
{ 16, 19, 330, 2, 19, 76, 6973 }, // n
|
||||
{ 18, 20, 323, 1, 19, 90, 7049 }, // o
|
||||
{ 17, 27, 328, 2, 19, 115, 7139 }, // p
|
||||
{ 17, 27, 328, 1, 19, 115, 7254 }, // q
|
||||
{ 12, 19, 220, 2, 19, 57, 7369 }, // r
|
||||
{ 14, 20, 255, 1, 19, 70, 7426 }, // s
|
||||
{ 12, 23, 193, 0, 22, 69, 7496 }, // t
|
||||
{ 16, 19, 330, 2, 18, 76, 7565 }, // u
|
||||
{ 17, 18, 271, 0, 18, 77, 7641 }, // v
|
||||
{ 26, 18, 419, 0, 18, 117, 7718 }, // w
|
||||
{ 18, 18, 282, 0, 18, 81, 7835 }, // x
|
||||
{ 17, 26, 272, 0, 18, 111, 7916 }, // y
|
||||
{ 14, 18, 251, 1, 18, 63, 8027 }, // z
|
||||
{ 12, 30, 203, 0, 24, 90, 8090 }, // {
|
||||
{ 4, 35, 294, 7, 26, 35, 8180 }, // |
|
||||
{ 11, 30, 203, 1, 24, 83, 8215 }, // }
|
||||
{ 17, 5, 305, 1, 14, 22, 8298 }, // ~
|
||||
{ 0, 0, 139, 0, 0, 0, 0 }, // U+00A0
|
||||
{ 5, 26, 143, 2, 19, 33, 0 }, // U+00A1
|
||||
{ 14, 26, 305, 3, 25, 91, 33 }, // U+00A2
|
||||
@@ -3303,7 +3303,7 @@ static const EpdUnicodeInterval notosans_16_regularIntervals[] = {
|
||||
};
|
||||
|
||||
static const EpdFontGroup notosans_16_regularGroups[] = {
|
||||
{ 0, 3626, 9206, 97, 0 },
|
||||
{ 0, 3626, 9181, 97, 0 },
|
||||
{ 3626, 3331, 11053, 96, 97 },
|
||||
{ 6957, 4095, 16723, 128, 193 },
|
||||
{ 11052, 4104, 15196, 96, 321 },
|
||||
|
||||
+2802
-2801
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user