Temporarily release framebuffer memory during section pagination to reduce heap pressure. The framebuffer can be released before memory-intensive operations and restored afterward, allowing BLE stack startup threshold to be lowered from 80KB to 70KB. Implements RAII-style FrameBufferBuildLoan class to ensure proper cleanup and automatic restart on restoration failure.
Add missingInclude suppression to prevent false positives on fresh CI checkouts where include paths are unresolved. Add inline suppression for constVariableReference warning where cppcheck cannot detect mutations without full include path resolution.
Reduce BLE stack memory usage from ~68KB to ~52KB by disabling unused NimBLE roles (peripheral, broadcaster), limiting connections to 1, and reducing buffer counts for ACL, HCI events, and ATT entries. Remove unused cloud components (esp_insights, esp_rainmaker) that require unavailable server certificates. These changes prevent stack collision with the render shed, eliminating restart flaps on the reader device.
Replace per-page buffer free+realloc pattern with capacity tracking that only reallocates when needed size exceeds current capacity. This prevents heap fragmentation from non-coalescing holes that occurred when each page's freed block rarely fit the next page's allocation. After a few page turns, capacities converge on the book's maximum and page turns stop touching the allocator entirely.
Prevent OOM aborts by shedding BLE stack when free heap drops below 24KB before rendering. Lower BLE start threshold from 100KB to 80KB to match steady-state heap levels (~84KB). Reserve vector capacity upfront in page parsing to avoid reallocation crashes on fragmented heaps. Gate background section builds on heap availability.
The popup lingered until the next page turn. E-ink has no free timers --
clearing costs a refresh whenever it happens -- so request the redraw
right away: the popup shows for the ~2 s page re-render, then clears.
Field session: after a recovery teardown the heap settles at ~72 KB --
below the 100 KB start floor -- and never recovers on its own (the
defrag silent-restart only fires when a build FAILS, and builds succeed
now). BT stayed paused forever, the menu toggle claimed ON, and toggling
off/on changed nothing.
- Menu label tells the truth: ON / PAUSED (enabled but stack down) / OFF
(new STR_STATE_PAUSED string)
- Toggling BT on below the heap floor silent-restarts into the current
book: the fresh boot's ~118 KB passes the gate and BT auto-starts on
resume. Explicit user intent is the right trigger for the defrag.
- Hoist the floor to bleinput::kStartMinFreeHeap, shared by the
lifecycle gate and the toggle
The 70 KB restart floor was arithmetic nonsense: NimBLE takes ~57 KB, so
a restart at 70 KB free left ~13 KB -- below the 40 KB build pre-flight
-- so the next chapter build instantly re-entered recovery and tore BLE
back down. Field symptom: endless 'BT Connecting...' popup + redraw loop.
- Single conservative floor: 100 KB (57 KB stack + 40 KB build headroom)
- 30 s cool-down after any recovery teardown before the lifecycle may
restart BLE, so a marginal heap can never flap; the 'BT paused (low
memory)' popup shows instead and reading continues without the remote
Layout code (line-break DP arrays, CSS lookups, glyph buffers) allocates
via std::vector/std::string and abort()s on OOM under -fno-exceptions --
it cannot fail cleanly mid-build. Field crashes (X4, BLE resident):
builds entered at ~11 KB free and aborted in ParsedText::
computeLineBreaks; an inline BLE restart after recovery re-starved the
render and abort()ed in FontCacheManager's scanText_.reserve at ~8 KB.
- BUILD_MIN_FREE_HEAP (40 KB): pre-flight before the build; below the
floor go straight to recovery instead of attempting a doomed build
- Recovery no longer restarts NimBLE inline; the lifecycle stays paused
until the render completes (scoped unpause guard), then the main-loop
lifecycle restarts BLE behind its own heap gate
- EpubReaderActivity: when a section build fails even after the BLE
teardown/retry, silentRestartToReader() as last-resort heap defrag,
guarded by bootWasSilentRestart() to prevent reboot loops
- main/SilentRestart.h: expose bootWasSilentRestart()
- platformio.ini: enable FREEINK_BLE_HID_REPORT_DEBUG in env:default
Introduces a global pause flag to temporarily block BLE stack auto-start during large memory allocations (e.g., in EPUB rendering). Also adds auto-restart logic for BLE scanning when device list is empty, and extensive debug logging for BLE scan lifecycle troubleshooting.
## 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>
## 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 >**_
Introduces a virtual method allowing activities to indicate whether they need the BLE stack to remain active. This enables selective teardown of Bluetooth to free heap memory, with the Bluetooth settings screen being able to override this for pairing and scanning functionality.
When building an EPUB section fails with Bluetooth enabled, temporarily stop the BLE stack to free up memory (~16KB), retry the build, then restart BLE. This works around memory fragmentation caused by the NimBLE stack that prevents allocating the large contiguous buffer needed for inflate/deflate operations. The recovery only runs once per uncached chapter since chapters are cached afterwards.
Integrate BLE keyboard host functionality for page-turner remotes. Adds device pairing, button mapping, preset configurations for Free2/Free3 remotes, and persistent storage of mappings. Migrates from open-x4-sdk to freeink-sdk submodule which includes the BleKeyboardHost library. Implements CPU frequency locking during BLE operations to prevent watchdog timeouts.
(#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?**
* 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>