Provides a weak definition of _btLibraryInUse to fix linker errors when using BLE controller-only builds with NimBLE-Arduino host. The arduino-esp32 core references this symbol but only defines it when IDF host stacks are enabled. This shim ensures the symbol is always available without conflicts.
The framebuffer loan provides a large contiguous block for inflate but cannot satisfy small allocations during CSS parsing and layout. Including it in the heap check made the threshold effectively always-false (48KB > 40KB floor), allowing builds to run at ~3KB free heap and crash in sscanf when BLE is connected. Now checks only general heap so BLE recovery runs before the build instead of crashing during it.
Replace single contiguous miniBitmap allocation with fixed 4KB chunks to allow large font page bitmaps (20-40KB) to fit in fragmented heap. Each glyph is kept whole within one chunk by padding to next chunk boundary when needed. Falls back to overflow path if glyph exceeds chunk size or max chunks reached. Mirrors existing image .pxc slot chunking strategy.
Prevents bold-italic glyphs from rendering blank under heap pressure (when BLE is resident). Pre-allocates a 1KB emergency buffer at font load time when heap is healthy. When per-glyph malloc fails during overflow, falls back to this buffer to render the glyph directly instead of returning null. The emergency buffer bypasses the cache ring and is overwritten on each use, which is safe since getGlyphBitmap() consumes bitmaps immediately.
Replace hardcoded path lookup with a multi-strategy search that tries PlatformIO's package API first, then falls back to globbing common package directories. This handles cases where the framework package name or location varies across installations.
Add pre-build script to patch arduino-esp32 core header, converting extern declaration to weak definition. This resolves linker errors when using NimBLE-Arduino host with ESP-IDF BLE controller only (no IDF host stack). The weak definition allows NimBLE translation units to provide the symbol while still allowing the core's strong definition to take precedence when an IDF host stack is enabled.
Previously a single scan buffer was used for all text styles (regular, bold, italic, bold-italic), causing the font decompressor to allocate full-page buffers for each style during prewarming. This led to OOM failures when the BLE stack was resident, causing bold/italic glyphs to render blank.
Now each style maintains its own scan buffer, sized only for the text actually rendered in that style. This keeps secondary style buffers small and prevents allocation failures.
## Summary
* **What is the goal of this PR?** Fix a reproducible crash: opening the
web UI's Settings page reliably crashes the device (silent reboot to
Home) under real-world conditions.
* **What changes are included?** `SettingsPage.html` fired three
unawaited `fetch()` calls on page load (`/api/settings`, `/api/wifi`,
`/api/opds`), opening simultaneous connections to the device's web
server. That server (Arduino's `WebServer`) handles one client at a
time; a genuinely concurrent second/third connection can stall a
response mid-stream for multiple seconds — long enough to trip the
firmware's watchdogs (300ms interrupt WDT / 5s task WDT, both
panic-on-timeout in this project's config). This PR makes the three
initial loads run sequentially via a small async IIFE instead of firing
them concurrently. Same end state for the page, no visible behavior
change.
## Scope Check
CrossPoint is intentionally narrow. See
[SCOPE.md](../blob/master/SCOPE.md) and
[ROADMAP.md](../blob/master/ROADMAP.md).
- [x] I have read SCOPE.md and ROADMAP.md.
- [x] This PR is **not** a new built-in theme.
- [x] This PR is **not** a new external network connector.
- [x] This PR is **not** an interactive app, writing tool,
RSS/news/browser, media playback, or PDF feature.
- [x] This is a bug fix for a reproducible crash, not new functionality.
- [x] Does not touch `freeink-sdk/`, `lib/hal/`, the bootloader, OTA, or
recovery code.
## Testing performed
Verified live on an X4 (driven via the `CMD:WIFI` serial debug command
from the companion PR, since I didn't have physical access to the device
for part of this investigation):
- Root-caused via a temporary per-entry heap/timing log in
`handleGetSettings()` (not included in this diff): under concurrent
load, individual `sendContent()` calls stalled 1-3+ seconds between
simple, static settings entries that should serialize in well under a
millisecond each.
- Reproduced the crash directly: firing `/api/settings`, `/api/wifi`,
and `/api/opds` concurrently (mirroring the page's old JS behavior)
crashed the device in 2/2 attempts. The companion PR (watchdog reset
detection) let the second crash actually get logged: reset reason
`ESP_RST_TASK_WDT`, panic confirmed.
- After the fix: same three endpoints, hit sequentially (matching the
new JS), succeeded cleanly across dozens of repeats (single-shot and a
60-request hammer loop) with no crashes and stable free-heap.
- Confirmed the live device served the updated, gzip-compressed
generated header after a normal `pio run` (source HTML →
`SettingsPageHtml.generated.h` via the existing `scripts/build_html.py`
build step).
## Additional Context
Memory/flash impact: none — same JS logic, just sequenced instead of
concurrent. No new allocations, no change to the generated header's size
beyond the few added bytes of source text.
Risk: low. This only changes the *order* three existing fetches happen
in on page load; no new endpoints, no changed request/response shapes.
---
### AI Usage
YES — diagnosed and fixed by Claude (Sonnet 5), including live
reproduction and verification against real hardware.
Ties bookmarks to the actual text on the page rather than to a page number. Page numbers shift whenever the book gets re-laid-out — changing font size, margins, line spacing, or rotating the screen — which could previously land you on the wrong spot.
This is a followup of #2805, making bookmarks use the same mechanism. Several minor improvments ride along.
needsIndex() opens .idx and .qidx and re-reads the sidecar header on every
call, and DictionaryWordSelectActivity called it once per tapped word rather
than once per dictionary open. A lookup therefore cost 4 SD opens and 2
std::string path temporaries, not the 2 and 0 the previous change set out to
achieve.
Ask needsIndex() once, inside the same guarded block that opens the
dictionary, and feed the build result back so a successful build stops
re-triggering while a failed one still retries on the next word.
Reimplement needsIndex() on openSession() so the "is the sidecar usable?"
rule lives in one place rather than two that have to be kept in step. This
is behaviour-preserving for everything buildIndex() can produce: it always
writes at least one sample (entry 0) and removes the file on failure, so
sampleCount == 0 means absent, stale or corrupt, all of which rebuild. The
one divergence is an improvement -- a corrupt sidecar with a valid magic
previously reported fresh and then full-scanned every lookup forever, and
now rebuilds once.
Also make the open()-time length bound a compile-time constant instead of
two runtime strlen() calls, and correct the buildPath() comment, which
claimed a lookup allocates nothing at all -- cleanWord(), stemVariants() and
the matched headword all still do.