Add STR_TEXT_AA_FULL and STR_TEXT_AA_FAST translation strings across all supported languages. Also includes a complete retranslation of the Slovak language file.
Add --compress flag to font generation command and regenerate the font bitmap data with compression enabled. This reduces the bitmap array size while maintaining the same 2-bit mode and character intervals.
Replace solid black rendering of anti-aliased font edges with period-2 dithering patterns. Full coverage pixels render as solid ink, while partial coverage pixels use checkerboard patterns (50% for dark grey, 25% for light grey) matching fillRectDither behavior. This prevents edge pixels from bolding to black on BW displays and produces proper anti-aliasing for 2-bit fonts.
## Summary
* **What is the goal of this PR?** Fixes#2402. On the X3, the "Time to
Sleep"
picker's 5-minute side buttons were inverted (left increased, right
decreased)
and the on-screen legend didn't match the physical buttons.
* **What changes are included?**
* Flip the large-step (±5 min) direction on X3 so the left side button
decreases and the right increases, matching the layout. X4 is unchanged.
* Add a device-specific step-hint string
(`STR_SLEEP_TIMER_STEP_HINT_X3`)
selected via `gpio.deviceIsX3()`. X3 shows `+/-: 1 min Side buttons: 5
min`;
X4 keeps the original `Left/Right: 1 min Up/Down: 5 min`.
* Same fix applied to both slider pickers that read the raw up/down side
buttons: the Time-to-Sleep picker and the "Go to %" picker
(`EpubReaderPercentSelectionActivity`), each with its own X3 hint string
(`STR_SLEEP_TIMER_STEP_HINT_X3`, `STR_PERCENT_STEP_HINT_X3`).
## Additional Context
* **Root cause:** the X3's side buttons sit one on each edge of the
screen
(power on top), whereas the X4 has a vertical up/down rocker on the
right
edge. So `BTN_UP` is physically the *left* button on X3 but the *top*
button
on X4. The picker mapped up→+5 / down→−5 unconditionally, which reads
naturally on the X4 rocker but inverted on the X3's left/right buttons.
The
static legend ("Up/Down: 5 min") had the same X3-only mismatch.
* The codebase already detects the device at runtime and handles this
layout
difference elsewhere (e.g. `LyraTheme::drawSideButtonHints` draws "Up on
left,
Down on right" for X3), so this reuses the same `gpio.deviceIsX3()`
signal.
* Swept the codebase for other side-button sliders:
`ClockOffsetActivity` already
uses the `Next/Previous` (`ButtonNavigator`) abstraction, which resolves
to
left=decrement / right=increment on X3, so it needs no change. List/page
navigation (Wi-Fi, KOReader sync, BMP viewer) and the keyboard cursor
are also
unaffected. The two slider pickers above were the only ones using raw
up/down.
* **X4 is untouched** — same direction, same legend wording as before.
* **Tested on X3 hardware:** left side button now decreases, right
increases, and
the legend matches. X4 not yet tested on device (no unit on hand); its
behaviour and strings are unchanged from master.
* **Translations:** the new `STR_SLEEP_TIMER_STEP_HINT_X3` and
`STR_PERCENT_STEP_HINT_X3` were added to all 26 languages, but the
non-English
ones are AI-generated and would benefit from a native-speaker pass —
particularly Hebrew (RTL ordering with a leading `+/-`), Kazakh, and
Vietnamese.
---
### AI Usage
Did you use AI tools to help write this code? _**YES**_
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This PR moves us from the xteink openx4 SDK to the freeink sdk from
https://freeink.org. Out of the box there are NO changes needed in the
firmware to support this swap, it all magically works as is. However as
we support more than just the x3/x4 devices, this sdk allows us to pass
env vars into the build commands to include support for other devices.
As support for new hardware such as touch screens and bluetooth are
added the xteink builds decide at compile time if the libraries are used
or not. For example right now the freeinkui and icons libraries are in
the platform.io file but as they are not used anywhere, they won't be
included in the final build. Once the touch branch and sd themes branch
are merged in this sdk is required for them to function correctly. All
the docs for freeink are available at freeink.org/docs. x4/x3 is a
single binary build unlike other devices that will build unique binaries
for each device. Eventually we will want to remove a lot of the manual
isx3 type stuff from our firmware and go through the boardsupport api
the sdk provides as it will generalize everything into one common system
that any device can support. The upcoming touch branch does a lot of
this for us but this initial PR is JUST to get the sdk swapped over
without any code changes to show seamless integration without any
regressions.
## Improvements
* Moved the File Manager breadcrumb into the Contents card header for a
cleaner, more consistent interface.
* Updated the Wireless Transfer section of the User Guide with clearer
instructions.
* Battery status bar indicator no longer changes position when adding or
removing bookmarks.
## Performance
* Optimized path normalization for faster file handling.
* Significantly improved bookmark rendering by removing unnecessary
XPath lookups.
* Optimized dithered rectangle drawing (fillRectDither) using a
byte-aligned rendering implementation, improving display performance on
supported devices.
## Bug Fixes
* Fixed an issue where the Inverted Orientation label was incorrectly
combined with the Color Filter label for Geman localization.
* Fixed excessive ghosting on the X3 cover screen during sleep
## Summary
* **What is the goal of this PR?**
Replace the pixel-by-pixel `fillRectDither` implementation with a new
byte-aligned `fillRectImpl` that eliminates per-pixel
`rotateCoordinates` calls and Read-Modify-Write bitwise loops, yielding
a significant rendering speedup on ESP32 E-ink framebuffers.
Ported from @rhythmerc's crosspoint-reader fork commit 27ea625.
* **What changes are included?**
- **`GfxRenderer.cpp` — `fillRectDither` refactor:** The existing
`if/else if` chain is replaced with a `switch` statement that delegates
each `Color` case to the new `fillRectImpl<Color>()` template,
eliminating runtime branching.
- **`GfxRenderer.cpp` — new `fillRectImpl<C>()` template:** Core of the
optimization. Key behaviors:
- Clips the rectangle in logical space upfront.
- Rotates only **2 opposing corner points** (top-left and bottom-right)
into physical framebuffer space instead of rotating every pixel
individually.
- Derives physical-space `byteStart`/`byteEnd` and precomputes
`headMask` / `tailMask` for MSB-first partial-byte boundaries,
performing RMW only on the edge bytes.
- **Solid fills (`Black` / `White`):** Uses `memset` for all interior
full-byte runs per row — no per-pixel writes.
- **Dithered fills (`LightGray` / `DarkGray`):** Precomputes both parity
variants of `blackMask` (even/odd `py`) **outside** the row loop,
eliminating the previously re-evaluated 8-bit construction loop on every
physical row. Interior full bytes are then written with a single
`memset(whiteMask)`.
- Uses `if constexpr` throughout to dispatch on `Color` at compile time,
generating zero runtime branches per template instantiation.
- **`GfxRenderer.h`:** Declares the new private `fillRectImpl<Color>()`
template method with an explanatory doc-comment.
- **Explicit template instantiations** added for all four active `Color`
variants (`Black`, `White`, `LightGray`, `DarkGray`).
## Additional Context
* **Performance:** The primary motivation is ESP32 E-ink framebuffer
performance. The old path called `rotateCoordinates` and did a full RMW
for every single pixel in the rectangle. The new path calls
`rotateCoordinates` exactly **twice** per fill regardless of rectangle
size, then operates at byte granularity — a complexity reduction from
O(W×H) coordinate transforms to O(1).
* **Dither correctness:** The `blackMask` precomputation relies on the
dither pattern having period 2 in both logical X and Y, which makes the
per-row byte pattern repeat with period 2 in `py`. Reviewers should
verify the `lxBase`/`lyBase` derivations for all four orientations
(`Portrait`, `PortraitInverted`, `LandscapeClockwise`,
`LandscapeCounterClockwise`) match the inverse of `rotateCoordinates`.
* **Edge case — single-byte rows:** When `byteStart == byteEnd`, the
head and tail masks are ANDed together into a single `rectMask` to avoid
double-masking the same byte. This path should be tested with narrow
rectangles (width < 8px).
* **No behavioral change for `Color::Clear`:** The `Clear` case exits
early via `if constexpr` and is a no-op, matching the original behavior.
---
### AI Usage
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 >**_
---------
Co-authored-by: Ryan Mercado <rmercado@firstdollar.com>
Force display resync on X3 when HALF refresh is requested to clear prior content before rendering. Add grayscale preconditioning for X3's UC81xx controller to even out single-pixel dithering artifacts that appear as speckle with its turbo BW waveform.
## Summary
fix#2414
### Root Cause
`updateBookmarkFlag()` was introduced in commit 1db1442 and is executed
on every render (every page turn). The function calls:
`ProgressMapper::toSavedProgress()`
→ `ChapterXPathResolver::findXPathForProgress()`
This path decompresses the current EPUB section content twice:
1. To count visible characters.
2. To resolve the corresponding XPath.
For larger sections (e.g. ~133 KB decompressed content), this adds
approximately **1 second of I/O overhead per page turn**, with the cost
increasing as chapter size grows.
### Fix
`updateBookmarkFlag()` only needs to determine whether a bookmark falls
within the currently displayed page range.
The required information is already available during rendering:
* `currentPage`
* `section->pageCount`
* `currentSpineIndex`
Instead of converting the current location to a saved progress object
(and resolving an XPath), the implementation now computes the current
page's progress range directly and compares bookmark percentages against
that range.
This is effectively the same percentage-based matching logic already
used as a fallback in `bookmarkMatchesProgress()` when XPath matching is
unavailable.
---
### AI Usage
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? _**< PARTIALLY >**_
## Summary
* **What is the goal of this PR?**
* Fix the “Inverted” translation so reader orientation and color/filter
inversion can use separate labels.
* **What changes are included?**
* Adds `STR_ORIENTATION_INVERTED` for the inverted portrait orientation
option.
* Updates the reader orientation setting to use
`STR_ORIENTATION_INVERTED` instead of reusing `STR_INVERTED`.
* Leaves `STR_INVERTED` for the sleep cover filter and tilt page-turn
mode
* Adds the new orientation string across all 26 locale YAML files.
## Additional Context
* The original issue was found by a user in German, where `STR_INVERTED`
was translated as `Hochformat 180°`, which made sense for orientation
but not for color filters.
* This is a UI-label-only change. It does not change persisted
orientation values or settings behavior.
* Reviewer note: non-English wording may still benefit from
native-speaker review, especially for the new orientation-specific
labels and to verify the interchangeable usage between inverted color
and inverted tilt page turn direction.
---
### AI Usage
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 >**_
(#2334)
Introduces a new display method that prepares the framebuffer as a base
frame for grayscale overlays. On X3 panels, this uses the OEM
differential base waveform (AA-pre-BW) without forcing a resync. Other
panels fall back to normal display with configurable refresh mode.
Dependent upon matching SDK commit to work
## Summary
This PR enhances the EPUB reader's bookmark system with two
complementary improvements: a per-page bookmark indicator icon and
toggle behavior on the existing long-press action.
---
### What Changed
**Bookmark Toggle (was: add-only)**
The long-press Confirm action now toggles bookmarks rather than always
adding. `addBookmark()` checks whether a bookmark with the same xpath
already exists in the in-memory cache:
- If found → removes it and shows "Bookmark removed."
- If not found → adds it and shows "Bookmark added."
A new `STR_BOOKMARK_REMOVED` translation string was added to support the
removal message.
**Bookmark Icon Indicator**
A `BookmarkIcon` is now drawn at the top-right corner of the page
whenever the current page has a bookmark. `updateBookmarkFlag()` is
called at render time to determine whether the current page is
bookmarked.
**In-Memory Bookmark Cache**
Bookmarks are now loaded into `cachedBookmarks` on `onEnter()` rather
than being re-read from disk on every toggle. All subsequent add/remove
operations work against this cache and flush to disk, avoiding redundant
file reads on each bookmark action.
**Faster bookmarks list**
Previously, calculating "page X/Y" for each entry required decompressing
the entire spine item. We now persist `si`/`pc`/`pp` (spine index, page
count, and page progress) in the bookmark JSON when saving, and restore
them when loading. This avoids the expensive `toCrossPoint()` loop in
`onEnter()`, significantly reducing the cost of initializing the
bookmarks list.
---
### Files Changed
- `EpubReaderActivity.cpp` — `addBookmark()` toggle logic,
`updateBookmarkFlag()` (new), icon rendering in `renderContents()`,
cache initialization in `onEnter()`
- `EpubReaderActivity.h` — new fields: `currentPageBookmarked`,
`bookmarkRemoved`, `cachedBookmarks`; new method declaration
`updateBookmarkFlag()`
---
### AI Usage
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? _**< PARTIALLY >**_
---------
Co-authored-by: Julia Nguyen <julia@uxj.io>
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Fixes footnote links landing in start section instead of the specific
foot note target.
* **What changes are included?**
ChapterHtmlSlimParser.cpp: call flushPendingAnchor() before overwriting
pendingAnchorId. First id got lost due to consecutive non-block elements
carry ids, the first id was lost and the reader had no page to jump to,
so it defaulted to page 0.
## Additional Context
Tested with the epubs attached to to #2336. Need to clear .crosspoint/
cache after flashing so the anchor map gets rebuilt with the fix.
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
---
### AI Usage
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?
Partially assisted with writing documentation.
## Summary
* **What is the goal of this PR?**
* Make manual clock sync work even when the device is not already
connected to Wi-Fi, so the user can start the sync flow directly from
settings instead of being blocked by connection state.
* **What changes are included?**
* `ClockSyncActivity` now launches the normal Wi-Fi selection flow
before syncing when the device is offline.
* After Wi-Fi selection succeeds, clock sync resumes automatically and
performs the existing forced NTP sync.
* If clock sync had to bring Wi-Fi up, the activity disconnects and uses
the existing silent restart cleanup path to avoid leaving the device in
a fragmented post-Wi-Fi heap state.
* The completion UI now only advertises Back, matching the updated input
handling.
## Additional Context
* Existing behavior is unchanged when Wi-Fi is already connected: the
activity syncs immediately.
* Cancelling Wi-Fi selection exits the clock sync flow.
---
### AI Usage
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 >**_
## Summary
* Adds a live font preview pane to the font selection screen so users
can see how a font looks before committing to it.
* Changes
* A preview pane occupying the top 30% of the font selection screen,
rendering sample pangram text in the previewed font
* A two-step confirm flow: first press (enter button) previews the font,
second press selects it
* Back restores the original font settings, so browsing has no side
effects
* Layout dimensions cached in `onEnter()` to avoid redundant
recalculation between `loop()` and `render()`
## Additional Context
* Preview sample text is hardcoded English; didn't want to use AI for
translation as I would not be able to verify the output in most
languages...
* The preview pane reduces visible list height; this is compensated by
passing the reserved height into `getNumberOfItemsPerPage`
---
### AI Usage
Did you use AI tools to help write this code? **PARTIALLY**
AI use for assisting in coding and in writing the PR description.
## Summary
* **What is the goal of this PR?**
Fixes#2275. A book could get stuck reopening on an old page, with
progress no
longer saving and neither "Delete Book Cache" nor "Clear Reading Cache"
able to
fix it. Root cause: `progress.bin` was written truncate-in-place, so an
interrupted write (power loss, or a crash mid-SPI during sleep) left it
with a
broken FAT cluster chain that the firmware could neither rewrite nor
delete —
recovery required `fsck`/manual deletion on a host PC.
Confirmed in the SDK: `SDCardManager::openFileForWrite` opens with
`O_RDWR | O_CREAT | O_TRUNC`, so the canonical file is zeroed before the
few
progress bytes are rewritten — exactly the window that corrupts the FAT
chain.
* **What changes are included?**
* New shared helper `ProgressFile::writeAtomic()`
(`src/activities/reader/ProgressFile.h`): writes progress to
`progress.bin.tmp`, flushes and closes it, then `remove`s the old
`progress.bin` and `rename`s the temp into place. An interrupted write
now
only ever damages the throwaway temp; the canonical file is never torn.
* All three readers route their progress saves through the helper:
EPUB (`EpubReaderUtils.h`), `TxtReaderActivity`, `XtcReaderActivity` —
they
all shared the identical vulnerable pattern.
* Minor: `EpubReaderUtils::saveProgress` now takes `const Epub&` (clears
a
cppcheck `constParameterReference` finding).
## Additional Context
* **Crash-safe, not metadata-atomic.** On FAT the replace is `remove` +
`rename`
(two directory ops; SdFat's `rename` won't overwrite, hence
remove-first). A
crash between them leaves *neither* file, which reads as "no saved
progress" on
next launch — a harmless reset to an old page, never a
corrupt/unclearable file.
The guarantee is that `progress.bin` is never half-written.
* **Prevents, does not repair.** This stops new corruption on healthy
cards. It
cannot fix an already-corrupted `progress.bin` (removing it may itself
fail at
the FAT level) — those still need `fsck`/manual deletion, as in the
issue's
workaround.
* **Known follow-up (out of scope here):** a crash *while writing the
temp* can
leave an orphan `progress.bin.tmp`. It's harmless and self-healing (the
next
save overwrites it, and it never blocks reading progress), but a
boot-time
orphan-`.tmp` cleanup would be a tidy follow-up.
* **Focus areas for review:** the close-before-rename ordering in
`ProgressFile.h` and the remove-before-rename rationale.
## Verification
* `./bin/clang-format-fix` — clean
* `pio check --fail-on-defect low --fail-on-defect medium
--fail-on-defect high` — no defects
* `pio run` — SUCCESS (RAM 30.9%, Flash 78.8%; footprint essentially
unchanged)
* Tested on a **Xteink X4** device: open book, turn pages, sleep/exit,
reopen —
progress now restores to the navigated page across all three readers
(EPUB / TXT / XTC).
---
### AI Usage
Did you use AI tools to help write this code? _**YES**_
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
- Hold the menu (Confirm) button for ≥1s while reading an EPUB to launch
the existing `KOReaderSyncActivity` directly — replaces the three-step
path (open reader menu → scroll to Sync → confirm) with a single
gesture.
- Reuses `ReaderUtils::GO_HOME_MS` (same 1s threshold used by long-press
Back) and the existing `KOREADER_STORE.hasCredentials()` guard.
- Adds a Controls picker **"Long-press Menu"** (`longPressMenuFunction`,
default **Bookmark**) that **cycles through the available functions**
bound to the long-press gesture: `KOSync → Disabled → Bookmark`. The
field name and `LONG_PRESS_MENU_FUNCTION` enum are intentionally general
so future actions (dictionary lookup, table of contents, etc.) can be
appended without another schema migration. The setting is **not** a
binary toggle.
- Existing menu Sync entry still works — both call sites share one
extracted helper (`launchKOReaderSync`); no logic duplication.
- Short-press Confirm release is gated on duration so the reader menu
does not also open after a long press that *acts*, mirroring the
existing long-press Back pattern.
- **No-credentials fall-through:** `launchKOReaderSync()` now returns
whether it acted. When the function is set to KOSync but no KOReader
credentials are stored, the long-press is a no-op that **falls through
to open the reader menu** — so the menu stays reachable instead of the
hold silently swallowing the gesture. The release is only suppressed
when sync actually launched or surfaced a save error.
## Test plan
- [x] `pio run` succeeds clean for the `default` ESP32-C3 environment.
- [x] On-device, value **KOSync**, valid KOReader credentials:
long-press Confirm ≥1s → sync screen launches; get + update progress
return HTTP 200; release does **not** also open the reader menu; returns
to the same page.
- [x] On-device, value **KOSync**, **no** credentials: long-press
Confirm falls through and **opens the reader menu** (regression fix);
short-press also opens the menu.
- [x] On-device: menu → Sync still launches the same screen (shared
helper) and syncs (200/200).
- [x] On-device, value **Bookmark**: long-press drops a bookmark and
does **not** also open the menu.
- [x] On-device, value **Disabled**: long-press Confirm opens the menu
on release; no sync, no bookmark.
- [x] On-device: long-press Back still goes to the file browser
(unchanged path).
- [x] Heap: epub is released before the TLS handshake (frees ~16 KB);
min free heap stayed ~84 KB during sync, well above the safe floor. No
panics/OOM across the session.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
This PR adds the [Libre Baskerville
](https://github.com/impallari/Libre-Baskerville) font family to the
fonts available for download in CrossPoint.
## Additional Context
Baskerville is a classic font family, widely available in some form or
the other in most ereaders and widely used in publishing and in the
academy.
Libre Baskerville is a libre (SIL Open Font License v1) implementation
of it, which makes it possible for CrossPoint to ship it without issues.

---
### AI Usage
Did you use AI tools to help write this code? _**NO**_
## Summary
Non-functional cleanup: `ChapterHtmlSlimParser::` prefix is redundant
when calling `applyDirectionToEntry` within `ChapterHtmlSlimParser`
methods.
---
### AI Usage
Did you use AI tools to help write this code? _**NO**_
## Summary
* **What is the goal of this PR?** Prevent heap memory exhaustion caused
by thousands of machine-generated anchor IDs injected by epub converters
like Kobo KePub.
* Fix#2292.
* **What changes are included?**
* **Element-type filter (Layer 1):** Introduced the
`isNonNavigableInlineElement()` function in `ChapterHtmlSlimParser.cpp`
to automatically skip recording IDs on `<span>` elements, as they are
purely inline wrappers used for tracking and lack navigable meaning.
* **Hard cap (Layer 2):** Added the `MAX_ANCHORS_PER_CHAPTER = 1024`
constant to act as a fallback safety net against unbounded heap growth
from unknown future ID-injection patterns on non-span elements.
* **TOC Safety net:** Ensured that IDs matching known Table of Contents
(TOC) entries explicitly bypass both the `<span>` filter and the
1024-anchor cap, guaranteeing that chapter page-break and core
navigation logic are never compromised.
---
### AI Usage
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? _**< PARTIALLY >**_
## Summary
* **What is the goal of this PR?** Update project documentation to match
the current master implementation for cache formats, i18n, file
transfer/web server workflows, SD-card fonts, and root user-facing docs.
* **What changes are included?** Refreshes `book.bin`/`section.bin` docs
for v6/v25, updates File Transfer/Calibre/WebDAV/API docs, documents 24
UI languages and JSON language persistence, updates root
README/USER_GUIDE cache and network details, and syncs the tracked
CLAUDE skill doc cache-version notes.
## Additional Context
* Docs-only change. Verified with `git diff --check origin/master..HEAD`
and stale-reference greps for old cache versions, removed i18n APIs, old
WiFi screen wording, and raw `Serial.printf` examples. No firmware build
was run.
---
### AI Usage
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? _**NO**_
## Summary
`std::map` heap-allocates each node. Avoid using it for wifi SSID
dedupe, and instead just dedupe in-place with the existing `networks`
vector. Removed dead `ipAddress` member in `WifiNetworkInfo` struct.
---
### AI Usage
Did you use AI tools to help write this code? _**PARTIALLY**_
## Summary
* **What is the goal of this PR?**
* Fix EPUBs where internal file references are written in URL-style
escaped form, like spaces appearing as `%20`, so the reader can find the
right files instead of treating those references as missing.
* **What changes are included?**
* Added a small shared helper that converts those escaped EPUB-internal
paths back into their normal filenames before we try to look them up.
* Applied that cleanup step across the EPUB parsing flow wherever we
resolve internal references, including cover images, manifest items, TOC
links, spine entries, and inline HTML images.
* Kept the change narrowly focused on EPUB-internal asset resolution
rather than changing broader URL or networking behavior.
## Additional Context
* The user-facing bug here is that some books package their internal
filenames in an escaped form, so a file like `Chapter 1.xhtml` may be
referenced more like `Chapter%201.xhtml`. The reader was treating that
escaped text as the literal filename, which means otherwise-valid books
could lose images, covers, or chapter targets because the lookup no
longer matched the real file inside the EPUB.
* Risk is intentionally low. The helper only rewrites valid `%XX` escape
sequences and leaves malformed input alone, so it should improve
compatibility with escaped filenames without broadening the parser’s
behavior in unrelated cases.
## Local Testing Performed
* This was tested on my device with the user-provided optimized epub
that was not rendering images within the text prior to this fix (cover
image and chapter headers were rendering fine):
[orv_main_baseline.epub.zip](https://github.com/user-attachments/files/28529545/orv_main_baseline.epub.zip)
* This was also tested by the user with a local build and the affected
epub and confirmed to be working
## Steps for Testing
* Try to open the affected epub (linked above) or any epub that has
similar percent-encoding on a build prior to this fix.
* Apply this fix, clear book cache, and re-open the affected book.
* Images should render properly.
---
### AI Usage
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 >**_
## Summary
Use `std::string_view` and case-insensitive comparisons to avoid string
allocations during CSS parsing.
**Hot path:** `resolveStyle` (called per HTML start tag during chapter
rendering) now does zero heap allocations. Previously it allocated a
normalized tag string, a vector of class strings, and a composite key
per class. For a chapter with ~2000 tags × 2 classes each, that's ~12
000 small short-lived allocations eliminated per page render — primarily
a heap-fragmentation win on the ESP32-C3's ~380KB RAM.
**Cold path:** CSS load no longer allocates per-rule selector vectors or
per-token strings; `splitOnChar`/`splitWhitespace` are gone, replaced
with callback-based tokenization (`forEachDelimitedToken`).
**Behavioral notes:**
- The selector `unordered_map` now uses an ASCII-case-insensitive
hash/equal. Selectors are stored with their original case rather than
pre-lowercased; the observable lookup result is unchanged.
- `stripTrailingImportant` is now case-insensitive (per CSS spec;
previously matched only lowercase `!important`).
**Cache compatibility:** `CSS_CACHE_VERSION` unchanged. Old caches
(lowercase selectors) load correctly under the new lookup; new caches
will contain verbatim-case selectors — both forms work.
---
### AI Usage
Did you use AI tools to help write this code? _**PARTIALLY**_
## Summary
This PR optimizes the text rendering process by skipping underline style
calculations and measurements during the initial font cache scanning
phase. This prevents excessive and unnecessary SD card reads on pages
with heavy use of underlines (e.g., Table of Contents pages).
### **The Problem**
During the first rendering pass (the font cache scan pass used to
collect text for prewarming), `GfxRenderer::drawText()` early-returns
after recording text as expected. However, `TextBlock::render()`
continued past this point to execute the underline decoration logic.
Because underline calculation calls `getTextWidth()` and
`getTextAdvanceX()`, it triggered immediate glyph lookups via
`EpdFont::getGlyph()`. Since the SD card font had not been prewarmed yet
at this stage, the lookups fell back to the `glyphMissHandler`,
resulting in hundreds of individual, slow SD card reads into a limited
8-slot ring buffer.
### **The Fix**
1. **Exposed Scan State:** Added `GfxRenderer::isFontCacheScanning()` to
safely check if the font cache manager is currently in
text-collection/scan mode.
2. **Bypassed Underline Logic:** Modified `TextBlock::render()` to check
this state and skip underline measurement and drawing entirely while
scanning is active.
> [!NOTE]
> The text itself is still properly captured for prewarming via
`drawText()`. The underlines will be safely and efficiently calculated
and drawn during the actual render pass after the fonts have been
completely prewarmed.
---
### AI Usage
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 >**_
## Summary
* **What is the goal of this PR?**
* This PR ports over Crossink's handling of large EPUBs that helps
prevent crashes during open after the book metadata cache is built.
* **What changes are included?**
* Removes the post-indexing ZIP-wide CSS discovery pass that built an
in-memory map of every ZIP entry.
* Reuses `content.opf` parsing to collect declared CSS files without
writing spine entries again.
* Temporarily releases the loaded book metadata cache while rebuilding
CSS for cached books.
* Parses CSS before reloading `book.bin` after a fresh cache build,
leaving more heap available during CSS rule parsing.
## Additional Context
* User reported their EPUB opening fine on Crossink but would crash on
Crosspoint. Verified this claim on my own devices.
* The crash this addresses happened after `book.bin` was successfully
built, when CSS discovery allocated a large `unordered_map` for ~3k EPUB
ZIP entries.
* Tradeoff: CSS files not declared in `content.opf` are no longer
discovered by scanning the full ZIP. This avoids the high-risk memory
allocation but improperly formatted EPUBs (ones that don't declare their
CSS styles in `content.opf` will render without styling and fallback to
inline styles.
* User provided epub that was crashing prior to this change:
https://www.mediafire.com/file/g57ea4mj13iunvh/Quang+%C3%82m+Chi+Ngo%E1%BA%A1i+-+Nh%C4%A9+C%C4%83n.epub/file
---
### AI Usage
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 >**_
## Summary
* Fix the issue described in #2176 where bookmark percentages are always
0%
* Also fixed the page number rendering to +1 because they are 0 index
based, but should be rendered starting from 1
---
### AI Usage
Did you use AI tools to help write this code? _**< NO >**_
## Summary
* **What is the goal of this PR?**
Mainly fixes#2196, but also allows users to set a sleep time of 1~30
min or to never sleep.
* **What changes are included?**
1. Revert changes from #1948 and #2137 (Sorry @Uri-Tauber)
2. Cherrypick changes from
https://github.com/uxjulia/CrossInk/commit/0cb91c3dd1c63f0559d4392fadc4a8340af5bdde
and
https://github.com/uxjulia/CrossInk/commit/4e6bd634b6ede51ff1a7107ba3e8efcd152bc6ee
for a timeout interval picker UI, the custom sleep time feature, and
migrate the existing setting in v1.3.0, `sleepTimeout`, to
`sleepTimeoutMinutes` (Thanks! @uxjulia)
3. Add a "Never" at the far right of the timeout interval picker
---
### AI Usage
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? _**PARTIALLY**_
---------
Co-authored-by: Julia <julia@uxj.io>
## Summary
* **What is the goal of this PR?**
Improve the X3 clock UTC offset picker so it is clearer which part of
the offset will change when the user presses up/down.
* **What changes are included?**
Replaces the single underlined `UTC +/- H:MM` string with separate
editable fields for the sign, hour, and minutes.
The editable fields are spaced out, outlined, and the currently selected
field is highlighted with a light gray background.
## Additional Context
* This is a render-only UI change in `ClockOffsetActivity`; it does not
change how UTC offsets are stored, clamped, saved, or applied.
* The selected field still cycles with Confirm, and up/down still
adjusts the active sign/hour/minute value.
Screenshot of **current design** showing the sign (+/-) highlighted:
<img width="264" height="396" alt="old utc offset"
src="https://github.com/user-attachments/assets/c88d69ea-089c-47da-b5b3-ed0ebda5b112"
/>
Screenshot of **new design** showing the sign (+/-) highlighted:
<img width="264" height="396" alt="utc offset fix"
src="https://github.com/user-attachments/assets/f51da8b3-9aab-4f48-a9d1-21c828c01499"
/>
---
### AI Usage
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 >**_
Chapter selection previously only used the spine index, causing
navigation to always land on page 0 of the spine item. Sub-chapters that
share a spine file but differ by anchor (e.g. `chapter.xhtml#sec2`) were
silently ignored. Now the TOC anchor is passed through `ChapterResult`
and applied via the existing `pendingAnchor` mechanism.
## Summary
* This PR implements navigation to sub-chapters which didn't work
correctly previously. If a sub-chapter of the current top-level chapter
was selected, nothing happened. If a sub-chapter of another top-level
chapter was selected, reader switched to the beginning of the top-level
chapter.
* In addition, chapters now always start from a new page. This fixes
anchor to page calculation for the cases when the actual chapter content
doesn't fit on the page where the corresponding ToC anchor was found.
## Additional Context
* I might misuse `pendingAnchor` here which was previously used for
footnote navigation, please double check. I'm open to suggestions for
improvements.
* Note that the chapter selected by default when
`EpubReaderChapterSelectionActivity` opens is still wrong. I'm going to
fix this separately. This PR addresses only navigation to the selected
chapter.
* I tested this PR on my X4 and verified that navigation to a different
sub-chapter works correctly, both inside and outside the current spine.
* Some of the changes were borrowed from
https://github.com/crosspoint-reader/crosspoint-reader/pull/1455
---
### AI Usage
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? _**PARTIALLY**_
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Tiled grayscale rendering to drop the storeBwBuffer peak largest
contiguous free block (the value that actually drives OOM on the C3)
from ~114 KB to ~82-90 KB.
This renders each grayscale plane band-by-band into a small (~8 KB)
scratch and
streams each band straight to controller RAM (community-sdk
writeGrayscalePlaneStrip), leaving the BW framebuffer intact. No save,
no
restore; controller RAM is re-synced for the next differential turn
directly
from the live framebuffer.
Three writers honor the active band target so per-band re-rendering
stays cheap
and correct:
- drawPixel (text) redirects writes to the band scratch and clips to it.
- renderCharImpl skips glyphs whose physical y-extent is outside the
band before
the bitmap decode (glyphIntersectsStrip), so the per-band re-render
doesn't
pay N x glyph decode.
- DirectPixelWriter (images) writes the band scratch via getWriteTarget
instead
of the framebuffer. Without this, image pixels wrote the live BW frame
directly and cleanup re-synced that corruption, leaving thin outlines
after
navigating away from an image.
Controller specifics live in the SDK (X4 setRamArea windowing, X3 PTL);
the
reader checks supportsStripGrayscale() and is otherwise
controller-agnostic.
Measured on hardware (X4 and X3, text and images, visually correct):
- Grayscale scratch ~8 KB vs ~50 KB save; largest contiguous free block
held at
full size during grayscale instead of dropping ~25-32 KB.
- X4 text page about +25 ms/page; X3 page time is dominated by its
intrinsic
grayscale refresh, not tiling.
Depends on community-sdk #13 (the writeGrayscalePlaneStrip API). The
submodule
bump here points at that branch, so until #13 merges the submodule won't
resolve
from upstream and CI will fail there; keeping this a draft until then.
Will
rebase onto master and re-point the submodule to the merged SDK commit
once #13
lands.
Did you use AI tools to help write this code? partial
## Summary
Minor cleanup flagged by newer gcc:
- Removed unused variables
- Removed unimplemented function declaration
- `static` -> `inline` to avoid per-TU duplication
---
### AI Usage
Did you use AI tools to help write this code? _**NO**_
## Summary
* **What is the goal of this PR?** fixes#2132.
---
### AI Usage
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? _**< NO >**_
2026-05-25 09:51:52 -05:00
273 changed files with 20592 additions and 57984 deletions
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.
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.
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
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.
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
@@ -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).
@@ -127,7 +127,7 @@ These flags in `platformio.ini` fundamentally affect firmware behavior:
// No file.close() needed — DESTRUCTOR_CLOSES_FILE=1 handles it at scope exit
}
```
**Usage**: See example above. Uses `FsFile` (SdFat), NOT Arduino `File`. Do NOT add `file.close()` for local variables (see DESTRUCTOR_CLOSES_FILE above).
**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 includes `<HalStorage.h>`, which transparently `using FsFile = HalFile;`; every method call (read, write, seek, close) takes the mutex. `HalFile`'s destructor also takes the mutex before letting the underlying SdFat `FsFile` close.
- **Never** call into `SdFat` / `SdSpiCard` / `FsBaseFile` / `SDCardManager`directly. **Never** define `HAL_STORAGE_IMPL` outside `HalStorage.cpp`; that disables the `FsFile -> HalFile` typedef and you'll get a raw SdFat handle that bypasses the mutex.
- If you're storing a raw `FsFile` in a place that won't transitively include `<HalStorage.h>` (rare), include the header explicitly so the typedef applies.
-`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.
---
@@ -458,7 +457,6 @@ void onExit() { /* free: vTaskDelete, free buffer, close member FsFiles */ Act
**All fonts are loaded as global static objects** at firmware startup:
**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 {
@@ -920,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.
Removing `/.crosspoint` clears all cached metadata and forces a full regeneration on next open. Note: the cache isn't cleared automatically when you delete a book, and moving a file to a new path resets its reading progress.
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).
@@ -248,9 +245,7 @@ One of the best things about open source is that anyone can take the code in a d
- [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.
- [crosspoint-reader (jpirnay)](https://github.com/jpirnay/crosspoint-reader) — Faster integration of functionality. Tracks upstream PRs and integrates the good ones ahead of the official merge.
-~~[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.
@@ -260,6 +255,8 @@ One of the best things about open source is that anyone can take the code in a d
- [crosspoint-reader-papers3](https://github.com/juicecultus/crosspoint-reader-papers3) — Crosspoint port for M5Stack Paper S3.
- [t5s3-reader](https://github.com/ShallowGreen123/t5s3-reader) — Crosspoint port for LilyGo T5 ePaper S3 / T5S3 4.7-inch e-paper device.
**Note:** Many of these features will make their way into CrossPoint over time. We maintain a slower pace to ensure rock-solid stability and squash bugs before they reach your device.
Want to build your own device? Be sure to check out the [de-link](https://github.com/iandchasse/de-link) project.
@@ -11,7 +11,7 @@ Focus Reading is a reading aid that bolds the first portion of each word, guidin
1. Open **Settings > Reader**
2. Toggle **Focus Reading** on
Toggling the setting will trigger a re-index of your current book, the same as when changing font settings. Once indexing is complete, page turns proceed as normal. No changes are made to your EPUB files.
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.
Loaded 20 of 273 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.