SdFat's SdSpiCard tracks SPI bus state with an unsynchronized
m_spiActive bool. When two tasks call into SdFat concurrently they can
confuse that state machine, ending with one task calling
SPIClass::endTransaction() against a paramLock the other task holds.
That trips FreeRTOS's xTaskPriorityDisinherit assert (tasks.c:5156,
pxTCB == pxCurrentTCBs[0]) and panics the system.
HalStorage already serialized every explicit method call via
storageMutex, but HalFile's destructor was `= default`, which let the
underlying SdFat FsFile destructor run close() outside any lock
(DESTRUCTOR_CLOSES_FILE=1). Any task that destructed a HalFile while
another task was mid-SD-op would race the unsynchronized state.
Move the locking discipline into HalFile::Impl::~Impl: an explicit
close() under StorageLock, then the FsFile member destructor's redundant
close() is a no-op. HalFile's special members can stay = default.
Switch storageMutex to xSemaphoreCreateRecursiveMutex so openFileForRead
and openFileForWrite can hold the lock while assigning to a HalFile&
out-param whose prior Impl needs locked teardown. Priority inheritance
still applies to recursive mutexes.
Also documented the no-bypass rule in CLAUDE.md: never call SdFat /
SdSpiCard / FsBaseFile / SDCardManager directly, never define
HAL_STORAGE_IMPL outside HalStorage.cpp.
Addresses my admittedly synthetic repro for #2047
Did you use AI tools to help write this code? partial
With both OtaUpdater and HttpDownloader on esp_http_client
(https://github.com/crosspoint-reader/crosspoint-reader/pull/2074,
https://github.com/crosspoint-reader/crosspoint-reader/pull/2075),
checkForUpdate no longer needs its own client and event handler to fetch
the release JSON. It streams the response straight into
ReleaseJsonParser through a new HttpDownloader::fetchUrl(url,
DataCallback) overload, dropping the duplicate esp_http_client setup,
the HTTP_EVENT_ON_DATA handler, and the totalBytesReceived file global.
DataCallback hands body chunks to a callback without buffering. The
naive alternative, collecting the ~32KB JSON into a std::string, aborts
under -fno-exceptions: the growing allocation collides with the TLS
session's heap mid-fetch and operator new calls abort().
The OTA install path stays on esp_https_ota (flash-write streaming),
which has no HttpDownloader equivalent.
HttpDownloader.h must precede the lwip (esp_http_client) headers in
OtaUpdater.cpp, or Arduino/SdFat macros collide with lwip.
Did you use AI tools to help write this code? partial
Based on the learnings from
https://github.com/crosspoint-reader/crosspoint-reader/pull/2074 , I
wanted to bring the same buffer savings to the rest of our HTTP Client
stack. That being said, HttpDownloader (fonts/OPDS) used the Arduino
HTTPClient.
HttpDownloader was the last consumer of the Arduino HTTPClient +
NetworkClientSecure stack. OtaUpdater already runs on esp_http_client,
so this drops the parallel HTTP/TLS implementation. It also fixes a
class of OPDS/font download failures: HTTPClient's setTimeout is uint16
and truncates, and its short per-read deadline killed slow or chunked
responses (the -11 / incomplete-data errors).
What changed:
- Rewrote fetchUrl/downloadToFile around esp_http_client with a
streaming open() -> fetch_headers() -> read() loop, manual redirect
following, and is_complete_data_received() as the completeness gate.
Body bytes go straight to the sink (OPDS parser stream, std::string, or
file), so nothing buffers the payload.
- HTTPS is now verified against the CA bundle instead of
NetworkClientSecure::setInsecure(). esp-tls is built with
CONFIG_ESP_TLS_INSECURE off, so an unverified handshake can't be set up
anyway; the model is public servers over verified https and local
servers over plain http (transport is chosen from the URL scheme).
** Self-signed https servers are no longer supported, by design. **
- timeout_ms is 60s; esp_http_client's timeout is uint32, so unlike
HTTPClient it doesn't silently truncate.
- HTTP buffers are 4096 (rx) / 1024 (tx). 4096 holds real OPDS server
headers; the GitHub release CDN sends more and logs a non-fatal
truncation warning, but the headers we read (Location, Content-Length)
come first and survive.
- Removed the now-unused UrlUtils::isHttpsUrl and a stale HTTPClient
comment in FontDownloadActivity.
Validated on device: OPDS browse and a 3.4 MB book download over
verified https, GitHub font downloads (crc-checked), redirect handling
matching curl, and slow/erroring servers surfaced correctly.
Did you use AI tools to help write this code? partial
## Summary
### **What is the goal of this PR?**
This fixes an unintended settings side effect when cycling the `Sleep
Screen` option through `Quick Resume`.
Previously, selecting `Sleep Screen = Quick Resume` globally forced
`Quick Resume on Timeout = ON` and left it enabled even after the user
toggled `Sleep Screen` to another option within the same settings
session. Now the auto-enable behavior is scoped to the Settings screen
session:
- If `Quick Resume on Timeout` was already `ON` when entering Settings,
it stays `ON`.
- If it was `OFF`, selecting `Sleep Screen = Quick Resume` temporarily
turns it `ON`.
- If the user then switches away from `Quick Resume`, it turns back
`OFF`.
### **What changes are included?**
- Removes the global logic that permanently forced `Quick Resume on
Timeout` to `ON` whenever `Sleep Screen` was set to `Quick Resume`, even
if it was just due to toggling through the options.
- Adds Settings-screen session tracking so `Quick Resume on Timeout` is
only auto-enabled while the user has `Sleep Screen = Quick Resume`.
- Restores `Quick Resume on Timeout` back to `OFF` when the user
switches away, but only if it was `OFF` when they entered Settings.
- Preserves existing `ON` timeout preferences.
- Same behavior applies to the web settings
## Additional Context
- Tested this on device and via the settings UI
---
### 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 >**_
Unify the two splash-skip signals (RTC silent-reboot flag, SD
seamless-sleep flag) into one BootResume enum driving a single switch.
Storage unchanged; behavior-preserving apart from the fix.
Holding power to sleep from a WiFi activity (Font Download, OPDS, web
server, Calibre, KOReader sync) rebooted to home instead of sleeping.
goToSleep() runs the outgoing activity's onExit(), and those activities
call silentRestart() to clear heap fragmentation, so the heap-defrag
reboot fired before deep sleep could start.
enterDeepSleep() now latches deepSleepInProgress before goToSleep();
silentRestart()/silentRestartToReader() no-op while it's set. Deep sleep
is a full chip reset on wake, so it already clears the fragmentation the
reboot existed for.
Did you use AI tools to help write this code? partial
## Summary
**What is the goal of this PR?**
Adds a clearer Quick Resume sleep-screen flow. The previous “Page as
Sleep Screen” behavior is now exposed as a dedicated `Sleep Screen >
Quick Resume `option, with the timeout-only behavior controlled by a
renamed `Quick Resume on Timeout `setting.
**What changes are included?**
- Adds `Quick Resume` as a new `Sleep Screen` option.
- Renames the old `Page as Sleep Screen` setting to `Quick Resume on
Timeout`.
- Changes that setting’s choices from `Never / After Timeout / Always`
to `OFF / ON`.
- Makes `Quick Resume on Timeout = ON` equivalent to the old `After
Timeout` behavior.
- Makes `Sleep Screen > Quick Resume` equivalent to the old `Always`
behavior.
- Automatically forces `Quick Resume on Timeout` to `ON` when `Sleep
Screen` is set to `Quick Resume`.
- Renames internal setting references from `seamlessSleepScreen` to
`quickResumeSleepScreen`.
- Updates translations for the renamed setting label.
**Additional Context**
- This is mostly a settings/labeling restructure around existing
behavior, not a new rendering path.
- The runtime quick-resume behavior still uses the existing saved
framebuffer / last-screen sleep flow.
- Review focus areas:
- Sleep entry behavior from manual sleep vs timeout sleep.
- The automatic dependency where selecting `Sleep Screen > Quick Resume`
sets `Quick Resume on Timeout` to `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? _**< YES >**_
---
**New `Quick Resume` option for `Sleep Screen` will automatically set
`Quick Resume on Timeout` to `ON`**:
<img width="480" height="800" alt="quick resume"
src="https://github.com/user-attachments/assets/94c553fd-a122-47a8-add9-f29694f55566"
/>
**Example where a different sleep screen setting like `Cover` can be
used in combination with the `Quick Resume on Timeout` setting**:
<img width="480" height="800" alt="cover + quick resume"
src="https://github.com/user-attachments/assets/dd18ce18-230b-4b78-808b-ac85f5e7d5d8"
/>
## Summary
Previously `ScreenshotUtil` used physical display size to draw a border
around the screen contents. Because of this, in landscape orientation
the border was shown as a broken square. This PR changes border drawing
to use logical screen size instead of a physical display size to take
orientation into account.
## Additional Context
* Tested on X4 in all 4 reading orientations. Behavior is now correct,
however it doesn't look perfect on my X4: the border is much closer to
the physical top side of the display than to the other sides. This might
be related to assembly variation during manufacturing, but it might as
well be related to the way a eink controller is connected to the display
(controller supports bigger display sizes, so an offset may be present).
---
### 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
* **What is the goal of this PR?** Prefer epub format over kepub or
other formats offered from an OPDS server
* **What changes are included?**
## Additional Context
Should address #1419
---
### 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 >**_
---------
Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
## Summary
After a silent reboot, there was a small window where the esp32 would
listen for button presses but the full refresh would hold the event
loop. This gave a UX experience where the silent reboot had completed to
the home screen, a user taps select (at any time during the process),
and they find themselves unexpectedly in a book.
## Additional Context
This must land after
https://github.com/crosspoint-reader/community-sdk/pull/11 and will need
the submodule SHA changes included in.
---
### 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
OTA install streams the full 5.8MB image over a multi-minute TLS session
while wifi/LWIP already holds the big internal arena. Measured on
device, the arena bottomed out at ~7.7KB free with the largest
contiguous block down to ~2.4KB; for 80% of the download there wasn't
even a contiguous 8KB block. It finishes on a clean heap but tips into
OOM for anyone carrying more pre-OTA fragmentation.
Two avoidable drains, both in OtaUpdater:
- The esp_http_client RX/TX buffers were 8192/8192 on both the version
check and the install. RX only has to hold response headers (bodies
stream through the parser / OTA writer) and TX only carries our GET, so
trim both to 4096/1024. 4096 still fits the github->CDN redirect
headers; the 512 IDF default truncates them, which is why they got
oversized in the first place.
- installUpdate fired the progress callback every ~100ms perform
iteration, waking the render task on every tick. Its framebuffer work
fights the TLS session for the same arena, and epd can't really repaint
faster than a percent anyway. Throttle it to whole-percent changes.
On device, combined: floor 7.7KB -> 19KB, worstcase contiguous block
2.4KB -> 21KB, zero sub 8KB iterations across the whole download.
KOReaderSyncClient already uses small buffers; HttpDownloader is on the
Arduino HTTPClient stack with no equivalent knob, so neither changed.
Did you use AI tools to help write this code? partial, heap-exploration
assisted by Claude.
setTimeout(0) on the serial could trigger a subtle but obnoxious
underflow.
Eat a milli, save a reset button.
Did you use AI tools to help write this code? no
<img width="605" height="454" alt="image"
src="https://github.com/user-attachments/assets/bfd84afe-3b58-436e-9a5d-539af3ec3d4e"
/>
Actualized "Last" sleep screen setting from previous PRs, rebranded as a
~~`Seamless Sleep`~~ `Page as Sleep Screen` option with more
improvements.
https://github.com/user-attachments/assets/59029ba6-007e-4841-abfa-f680d7e98b79
---
New option: `Page as Sleep Screen` - `Never (default)`, `After Timeout`,
`Always`
When enabled, it seamlessly sleeps on timeout or power off, making a
fast refresh for the moon icon. When waking up, we still show the last
page, instead of the boot screen, making it fully seamless.
I tried different icons such as "refresh arrow" and others, but they
looked not as nice as 3 simple dots.
With this mode, the device turns off 4 seconds faster. And has 6 seconds
less delay when turning back on. Much more responsive.
Previously, even a 10-minute timeout sometimes wasn't enough, and I'd
worry about seeing the book cover. It's now easier to use a shorter
sleep timeout: if I get distracted during a reading session but don't
want to stop, the new screen is much more inviting to come back to.
---
Did you use AI tools to help write this code? _**PARTIALLY**_.
---
Test v1.3.0 firmware.bin file
[download](https://github.com/user-attachments/files/28015193/firmware.zip)
Based on PRs #410 and #495Closes#400, #1649
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary
Update the `/delete` endpoint reference to match the current web server
handler.
## Details
The endpoint docs still described a `type` form field and the old
`Deleted successfully` response. The handler now accepts either `path`
for a single item or `paths` as a JSON array for multi-delete, infers
file versus folder from the SD card entry, and returns `All items
deleted successfully` when all deletes complete.
This updates the curl examples, parameter table, success response, and
error response list to match the implemented behavior.
## Validation
- Compared the documented parameters and response strings with
`CrossPointWebServer::handleDelete()`
- `git diff --check`
When the device went into deep sleep while plugged into USB, a
power-button press would occasionally not wake it. The display held its
last frame, the chip stayed in deep sleep, and recovery required an
unplug + reset + hold-power cycle. On battery the symptom never surfaced
because the power button physically re-energises the chip.
Two peripherals were holding power domains alive across the deep sleep
boundary and interfering with the configured GPIO wake on the power
button:
1. HWCDC. Once Serial is initialized, the USB Serial/JTAG peripheral
keeps its power domain configured even with TX timeout at zero and no
host draining. Tear it down with Serial.end() in
HalPowerManager::startDeepSleep, gated by ENABLE_SERIAL_LOG to match the
Serial.begin site. This hit me if I was charging off my computer.
2. WiFi. enterDeepSleep had no WiFi teardown, so sleeping from any
network-using activity left the modem domain alive. Call
WiFi.disconnect(true) + WiFi.mode(WIFI_OFF) when WiFi is active. Wake
from deep sleep is effectively a chip reset, so no WiFi state needs to
survive. While this doesn't cause higher power drain, it apparently was
causing issues where I'd occasionally have the chip hang on sleep
transition from a wifi activity.
Confirmed on device.
Did you use AI tools to help write this code? partial
EIGHT_BIT_GRAYSCALE decode of a 3-component progressive JPEG calls
JPEGDecodeMCU_P with MCU_SKIP for Cb and Cr after every Y MCU. The
existing safe-pMCU patch redirects the wild pointer to &sMCUs[0] but
leaves the DC store unguarded, so each chroma skip overwrites the
just-decoded Y DC with the chroma DC predictor. Output reads sMCUs[0],
gets the trailing Cr DC (~0), and renders an all-black image.
Add `if (iMCU >= 0)` guards to the two pMCU[0] writes (main DC store and
successive-approximation update). The pointer redirect stays as the AC
wild-pointer defense; the new guards stop the silent corruption at
sMCUs[0]. The two fixes are independent and both required.
fixes the progressive 8bit grayscale jpeg regression in 1.3.0
Did you use AI tools to help write this code? partial
## Summary
* **What is the goal of this PR?** Small UX improvement to the Home
screen by preserving the last selected cursor position when returning to
it.
It supersedes #985 and #1103, which are both significantly outdated and
hundreds of commits behind master.
---
### AI Usage
Did you use AI tools to help write this code? _**< YES >**_
On-device repro showed the cover snapshot pinning ~52KB of contiguous
heap (cloning the full 48KB framebuffer with malloc overhead). MaxAlloc
on Home was 61KB; nothing was leaving headroom for HTTPS, which needs
30-50KB contiguous for the mbedTLS handshake.
Add region-aware framebuffer helpers to GfxRenderer that translate a
logical rect through rotateCoordinates and copy only the byte range that
contains the rotated rect. HomeActivity records the tile rect it passes
to drawRecentBookCover and caches only that subregion.
Measured on device (X3, Portrait):
Idle on Home | Free 102K -> 139K | MaxAlloc 61K -> 115K
Mid-EPUB-read | Free 81K -> 134K | MaxAlloc 70K -> 115K
Cover cache | ~52K -> ~16K (per allocation)
Works in all four orientations because the bounds helper samples the
four logical corners through the existing rotation, so the cached byte
range always covers the pixels the theme could have drawn into.
Savings will vary with theme, but should be significant across all.
The "logs only flow if you unplug and replug the USB cable at the right
moment" symptom traced to two interacting problems with the ESP32-C3 USB
Serial/JTAG controller (HWCDC):
1. Serial.begin was gated on gpio.isUsbConnected(). That check sampled
USB state at one specific microsecond during boot. If USB enumeration on
the host hadn't completed by that moment (common after a reset that
auto- reconnects a moment later), Serial was never initialized and
stayed dead until the next boot where the timing happened to win.
2. HWCDC writes block for up to the configured TX timeout (default 250
ms) when the host has the port open but isn't actively draining — a
state the macOS USB CDC stack enters intermittently after reconnect. The
firmware then appears to hang on logging until a USB unplug+replug
cycles the peripheral and flushes the TX FIFO.
Fix: move the Serial init to the very top of setup() with a 250 ms stall
before Serial.begin (lets the USB peripheral power-on and host
enumeration complete on cold boot), and call logSerial.setTxTimeoutMs(0)
so writes drop bytes harmlessly when the host is slow instead of
stalling the firmware. Both warm reboot and cold power-on now produce
logs immediately.
Did you use AI tools to help write this code? partial
## Summary
Add ability to long press 'confirm' on a book in the recent books list
to be prompted to remove it from the list.
---
### 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, Claude*
---------
PR #1908 silent-restarts on exit from any wifi-using activity to defuse
LWIP/mbedTLS heap fragmentation, but two of the wifi-using paths slipped
through that audit:
KOReaderAuthActivity (Settings -> KOReader sync -> Authenticate)
OtaUpdateActivity (Settings -> Check for update, back-out paths)
Both used WiFi.disconnect + WiFi.mode(WIFI_OFF) on exit and returned
control to Settings, leaving ~50KB of contiguous heap stranded for the
rest of the session.
Mirror the FontDownloadActivity pattern: if WiFi was activated,
disconnect and silentRestart. OTA's success path is unchanged:
SHUTTING_DOWN already calls plain ESP.restart() so the new firmware
boots normally; only the cancel/fail/no-update back-out paths now go
through silentRestart().
Did you use AI tools to help write this code? partial
Pulls in community-sdk PR #9, which clears inGrayscaleMode inside
cleanupGrayscaleBuffers() after the restored BW frame is written back
into RED RAM. Without this, the next BW page turn would still see the
flag set and trigger a redundant grayscaleRevert() refresh, producing
visible ghosting on the X4 with text anti-aliasing enabled.
Regression introduced by SDK commit 0a8ada2 (factory LUT grayscale
support), which removed a redundant inGrayscaleMode guard in
grayscaleRevert() and so caused the cleanup to actually run for the
first time.
Bypassing rules to avoid this going stale and all nightly builds being broken for x4 users
## Summary
* **What is the goal of this PR?**
Fix EPUB optimization when XHTML image references are URL-encoded.
* **What changes are included?**
The optimizer already converts image files to `.jpg`, but XHTML files
could still reference the original URL-encoded image path, for example:
```html
<img src="images/wensday%201%20full%202.png">
````
The optimized EPUB then contained the converted file:
```text
images/wensday 1 full 2.jpg
```
but the XHTML still pointed to the old `.png`, so CrossPoint failed to
extract/render the image.
The issue was that the previous replacement logic matched only the plain
filename form, such as:
```text
wensday 1 full 2.png
```
but not the URL-encoded form:
```text
wensday%201%20full%202.png
```
This PR updates XHTML image `src` attributes through the existing
DOMParser pass by decoding and resolving the image path before matching
it against renamed images.
After this fix, the optimized EPUB correctly rewrites the XHTML image
reference to the generated `.jpg`, and the image renders correctly.
---
### 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**_
---
Please let me know if you have questions,
Thank you!
## Summary
* **What is the goal of this PR?**
Fix incorrect power button long-press detection during shutdown/wake
verification by introducing dedicated power button timing logic.
* **What changes are included?**
* Added getPowerButtonHeldTime() to HalGPIO as a wrapper over input
manager logic
* Replaced generic getHeldTime() usage with power-button-specific timing
in verifyPowerButtonWakeup()
* Ensures shutdown/wake decision is based only on actual power button
hold duration, not any-button timing
* Minor header update for new API exposure in HalGPIO.h
## Additional Context
This fixes a bug where holding another button while briefly pressing the
power button could incorrectly trigger shutdown behavior due to shared
timing state (getHeldTime()).
The change isolates power button timing to prevent cross-button
interference and makes shutdown logic reliable during multi-button
interactions.
No behavioral changes are expected outside of power-button handling
logic.
**Dependencies**
- SDK PR: https://github.com/crosspoint-reader/community-sdk/pull/3
This PR requires the `community-sdk` submodule to be updated after the
SDK change is merged.
- Fixes: #1881
---
### AI Usage
Did you use AI tools to help write this code? _**PARTIALLY**_
## Summary
* Add swedish hyphenation using scripts/update_hypenation.sh
* Add hyphenation test data using the Swedish translation of Andy Weir's
Project Hail Mary
---
### 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
* **What is the goal of this PR?**
Improve the UI based on feedback from someone on discord
> Downloading ALL fonts feature.
> 1.1 Disable sleep when downloading, in my case went directly to sleep
just right after downloading.
> 1.2 It would be great to have and overall progress indicator as we
only have the indication of each font family
> 1.3 Any cancel or pause function might come in handy in case battery
is running out and then resume or retry with pending fonts
* **What changes are included?**
- Now the UI can show overall progress across every file being
downloaded in the batch, not just progress inside the current family.
- Extended `HttpDownloader::downloadToFile()` to accept a cancel flag
and abort the download.
- Rendered a cancel button in the font download UI while a download is
in progress.
- `preventAutoSleep()` in `FontDownloadActivity.h` now returns true for
`state_ == COMPLETE` and `state_ == ERROR` in addition to
`LOADING_MANIFEST` and `DOWNLOADING`
## Additional Context
Not very satisfied with how `HttpDownloader.cpp` is right now, might try
to refactor it after v1.3.0
---
### 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 the Italian translation.
* **What changes are included?** Took the latest `english.yaml` as
reference and updated `italian.yaml` accordingly, translating new
strings and revising existing ones where needed. Specific changes can be
inspected from the diff.
## Additional Context
* Nothing special to flag — happy to adjust any wording the reviewer
disagrees with.
---
### 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**_ — Claude
provided a first-pass draft; I revised and rewrote a substantial portion
by hand.
WiFi/LWIP/netif teardown scatters long-lived allocations across the
heap, leaving ~50KB of contiguous space unrecoverable without a reboot.
Reboot the SoC on exit from any wifi-using activity to guarantee a clean
heap. An RTC_NOINIT flag survives the reboot and tells setup() to skip
the boot splash and route the user back where they came from:
- File transfer / Calibre / OPDS / Font download -> home
- KOReader sync -> currently-open EPUB
Activities check WiFi.getMode() before rebooting, so backing out of the
network mode menu without joining doesn't trigger a cycle. KOSync also
esp_wifi_stop()s after the sync result so the radio is off while the
user reads it; full teardown happens at the reboot.
## Additional Context
The silent reboot skips the booting splash screen - it visibly looks
like a screen refresh. This does cause a disconnection/reconnection blip
for developers actively pulling logs over serial, but `pio device
monitor` and the like successfully reconnect and feed in the early boot
serial.
as an example:
```
[256676] [DBG] [ACT] Exiting activity: KOReaderSync
[256706] [DBG] [MAIN] Silent restart (target=reader)
ESP-ROM:esp32c3-api1-20210207
Build:Feb 7 2021
rst:0xc (RTC_SW_CPU_RST),boot:0xf (SPI_FAST_FLASH_BOOT)
Saved PC:0x403872bc
SPIWP:0xee
mode:DIO, clock div:1
load:0x3fcd72a0,len:0x990
load:0x403cbf10,len:0xac8
load:0x403ce710,len:0x4d28
entry 0x403cbf10
[22] [INF] [MAIN] Hardware detect: X4
[29] [SD] SD card detected
[43] [DBG] [CPS] Settings loaded from file
[58] [DBG] [KRS] Loaded KOReader credentials for user: jeremydk
[69] [DBG] [OPS] Loaded 1 OPDS servers from file
[69] [DBG] [UI] Using Lyra theme
[70] [DBG] [MAIN] Starting CrossPoint version 1.2.0-dev-detached-bde75787
...
[203] [DBG] [ACT] Entering activity: Reader
[211] [DBG] [EBP] Loading ePub: /Halting State - Charles Stross.epub
[221] [DBG] [BMC] Loaded cache data: 51 spine, 41 TOC entries
[246] [DBG] [CSS] Loaded 41 rules from cache
[247] [DBG] [EBP] Loaded ePub: /Halting State - Charles Stross.epub
```
---
### 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
As noted in
[#1680](https://github.com/crosspoint-reader/crosspoint-reader/discussions/1680#discussioncomment-16661106),
the README hasn't been updated in a while and has fallen behind the
actual firmware. This PR brings it up to date.
Beyond the feature list, I added a section acknowledging community forks
worth knowing about. I also took some deliberate editorial choices
around how CrossPoint is framed — I think it has the potential to be
more than just "an alternative Xteink firmware", and the wording
reflects that.
A note on process: I wrote the bulk of the text myself, but used AI
tools to scan the codebase and catch features I might have missed, and
to clean up my English (I'm fluent but not a native speaker). If any
line reads as unnatural or AI-sounding, please flag it — I'd rather fix
it than leave it.
---
One thing outside the scope of this PR: I think the cover photo could
use a refresh, ideally replaced with a small gallery showing different
CrossPoint screens. If you have a professional camera and an Xteink
device and want to help with that, let me know.
---------
Co-authored-by: Zach Nelson <zach@zdnelson.com>
Document the Xteink Unlocker tool requirement for third-party purchased
xteink units that ship with USB flashing locked. Include warnings about
bricking risks when flashing unsupported firmwares (e.g. Papyrix) on
locked devices, as they may permanently lock the device with no recovery
path.
## Summary
SD card font fixes:
- `TxtReaderActivity` needs to call `renderer.ensureSdCardFontReady` to
build the advance lookup table to support rendering with SD card fonts.
This revealed that `TxtReaderActivity` was inconsistently performing
layout with `getTextWidth`, when the renderer actually uses
`getTextAdvanceX`, which can lead to minor inconsistencies in alignment.
- Avoid allocating one big `allText` string in
`ParsedText::layoutAndExtractLines`. Instead, pass the vector of word
strings directly to `SdCardFont::buildAdvanceTable`, where the algorithm
just needs to iterate codepoints anyway.
---
### 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: Justin Mitchell <justin@jmitch.com>
## Summary
Deleted two unused header files containing binary icon data.
---
### 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
Pared down version of #1418.
Following up on b5df6cb2b5. Added
lib/Memory/Memory.h with:
- `makeUniqueNoThrow<T>` a `nothrow` wrapper for `std::make_unique` that
return `nullptr` on OOM instead of calling `abort()` (the behavior of
bare `new` with `-fno-exceptions`)
- `ScopedCleanup` a helper to call a cleanup lambda on scope exit.
These utilities help to write code that handles OOM scenarios
gracefully, and consistently cleans up resources on scope exit.
JpegToBmpConverter.cpp has been converted to use these utilities. Other
files can be converted later.
This will simplify some of the SD card font resource management in
#1327.
---
### 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
* **What is the goal of this PR?** Fix regression of
Calibre-Web-Automated authentication
* **What changes are included?** Trivial fix using ESP HTTP Client
primitives
## Additional Context
Calibre-Web-Automated KOSync stopped working after refactoring HTTP
client to use `ESP HTTP Client`, resulting in a _Server error (try again
later)_ error.
---
### AI Usage
Did you use AI tools to help write this code? _**NO**_
---------
Co-authored-by: drbourbon <fabio@MacBook-Air-di-Fabio.local>
## Summary
First of several changes to decouple and clean up SD card fonts
integration. This change eliminates SdCardFontGlobals.h:
- Simply declare the `extern SdCardFontSystem sdFontSystem` in
SdCardFontSystem.h.
- `ActivityManager::goToReader` should not care about loading SD card
fonts. Instead do the same work in `ReaderActivity::onEnter`, after the
previous activity has exited and after ReaderActivity has validated the
file path.
---
### 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
Relative includes can hide inappropriate dependency relationships. In
this case, I found that lib/KOReaderSync/KOReaderCredentialStore.cpp was
dependent on src/JsonSettingsIO.h -- a lib -> app dependency, the
opposite direction dependencies should flow in this project.
This change replaces all relative includes with root-relative includes,
and corrects the KOReaderCredentialStore dependency by moving its JSON
settings serialization local to the KOReaderSync library.
---
### 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
- Holding the navigation button in **Settings → Reader → Font family**
now advances the selection by a full visible page instead of one item at
a time.
- Same fix applied to **Settings → Language**, which had the same
one-item-only behavior.
- Mirrors the pattern already used in font download (ceb3fed) and
chapter selection screens.
## Test plan
- [ ] Settings → Reader → Font family: tap moves by one item; hold jumps
a page (wraps at ends)
- [ ] Settings → Language: tap moves by one item; hold jumps a page
(wraps at ends)
## Summary
- Fixes#1928 by having the prewarm and advance table functions resolve
fallback styles
---
### AI Usage
Did you use AI tools to help write this code? YES - Codex
---------
Co-authored-by: Uri Tauber <uritaube@gmail.com>
## Summary
* **What is the goal of this PR?** fix#1956.
## Additional Context
[NixOS-and-Flakes-Book_ch3_p7_4pct_130044.bmp](https://github.com/user-attachments/files/27648392/NixOS-and-Flakes-Book_ch3_p7_4pct_130044.bmp)
---
### 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 README link to the KOReader Sync quick setup section in the
user guide.
* Update the fragment from the old 3.6.5 anchor to the current 3.6.7
anchor.
## Additional Context
The README pointed to `#365-koreader-sync-quick-setup`, but
`USER_GUIDE.md` defines this section as `3.6.7 KOReader Sync Quick
Setup`, with the matching anchor `#367-koreader-sync-quick-setup`.
Verified with `rg` that the README now points to the existing heading.
PlatformIO checks were not run because this is a Markdown-only link fix
and `pio` is not installed in this environment.
## Summary
* **What is the goal of this PR?**
Separate the font manager's combined `Download / Update All` action into
separate `Download All` and `Update All` rows
* **What changes are included?**
- Updated multiple i18n translation files to add `STR_UPDATE_ALL` and
adjust `STR_DOWNLOAD_ALL` text.
- Added separate handlers: `downloadAll()` for fonts that have been
installed and `updateAll()` for fonts with updates.
- `Download All` and `Update All` are only shown when applicable.
---
### 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
Bind `prev` / `next` functionality to the left and right buttons in BMP
Viewer and adds labels
<img width="718" height="953" alt="image"
src="https://github.com/user-attachments/assets/c6dac14e-14f5-4cbf-9298-772cfc479f33"
/>
## Additional Context
* 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? _**NO**_
Reverts crosspoint-reader/crosspoint-reader#1912
This was meant to be more robust with partial SD card fonts, but causes
trouble for folks using custom font sizes. We need a better approach to
decouple numeric font sizes from S/M/L/XL settings.
## Summary
* Rename the hyphenation update script to match the documented
`update_hyphenation.sh` name.
* Update the hyphenation trie documentation command example to use the
renamed script.
## Additional Context
The prose in `docs/hyphenation-trie-format.md` referred to
`update_hyphenation.sh`, but the command example and file on disk used
`update_hypenation.sh`. This made the documented script name
inconsistent with the runnable command.
Verified with `rg` that no references to the misspelled script name
remain. Also ran `bash -n scripts/update_hyphenation.sh` to syntax-check
the renamed script without downloading trie files.
## Summary
Consolidate the script help/startup fixes so standalone helper commands
can show usage without requiring runtime-only dependencies or generating
files.
## Details
Several helper scripts imported optional packages, evaluated newer
annotations, or started generation before users could inspect CLI usage.
On a fresh checkout, this made common help paths fail on missing
packages such as `cairosvg`, `Pillow`, `freetype-py`, `fontTools`,
`pyphen`, `pyserial`, or on Python 3.9 annotation evaluation. A few
generators also treated `--help` as a normal output argument and wrote
files instead of printing usage.
This change keeps runtime dependencies on the code paths that need them,
handles lightweight help/list commands first, and adds explicit `--help`
handling for generator scripts that previously started work.
## Validation
- `python3 <script> --help` across tracked Python scripts, excluding
`scripts/patch_jpegdec.py` because it is a PlatformIO pre-build hook
rather than a standalone CLI
- `python3 scripts/convert_icon.py` still exits with usage for missing
required arguments
- `python3 lib/EpdFont/scripts/fontconvert_sdcard.py --list-presets`
- `python3 -m py_compile` for the changed scripts
## Summary
Micro fix Ukrainian translation
---
### 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**_
Co-authored-by: KymAndriy <test@notamail.ua>
## Summary
Update Ukrainian translation
### AI Usage
Did you use AI tools to help write this code? _** PARTIALLY **_
---------
Co-authored-by: Kym_Adnriy <kym_andr@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
## Summary
SD font binaries are published in their own repo, this updates the docs
to have the proper informatoin.
---
### 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, claude**
## Summary
The paths in the sd font documentation were never updated to match the
code's use of `/fonts` and `/.fonts`. This fixes the paths in the docs.
### 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, Claude**
## Summary
* **What is the goal of this PR?**
1. Adds font family deletion support to the font download activity so
users can remove installed font families directly from the download
list.
2. Address an issue where a font family manually deleted in the file
browser by user will show as "update" in the `FontDownloadActivity`
* **What changes are included?**
* Updated button hint label to show `Delete` when deletion is available.
* Added confirmation before deleting a selected installed font family.
* Refreshed font registry in `fetchAndParseManifest` (for the second
goal above)
---
### 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
This PR introduces **Focus Reading**, a generic implementation of
artificial fixation points (similar to Bionic Reading) designed to
improve reading speed and focus by bolding the initial characters of
words. This is achieved by dynamically bolding characters during
indexing.
<img width="500" alt="Focus Reading on X3"
src="https://github.com/user-attachments/assets/94a632a5-82da-47be-957c-538b35bf84d9"
/>
### Implementation Details
#### Core Text Engine (`ParsedText`)
- Modified `ParsedText::addWord` to implement a custom bolding
algorithm. It uses a 45% ratio for bolding, with a minimum of 1
character and a maximum of 9.
- UTF-8 Safety: Integrated `utf8NextCodepoint` to ensure character
counting and string slicing occur at safe byte boundaries, preventing
corruption of multi-byte characters (e.g., accented letters or smart
quotes).
- Intelligent Tokenization: This correctly identifies and separates
"word" characters (letters, apostrophes, hyphens) from "non-word"
characters (numbers, brackets, smart quotes).
- Formatting Preservation: The logic ensures that punctuation is not
"stolen" for the bolding count and that existing styles (like italics or
underlines) are preserved across the bold/regular split.
- Processing at indexing stage reduces CPU load at render-time and
ensures layout/fit is unaffected.
- Split details are tracked with `wordIsFocusSuffix`. After splitting
and layout, suffixes are merged back into their preceding word entries
to prevent a doubling of RAM usage.
#### Settings and UI
- Version Management: Bumped `SECTION_FILE_VERSION` to `21`
- Global Settings: Added `focusReadingEnabled` to `CrossPointSettings`.
- User Interface: Added a new toggle in the "Reader" section of the
settings menu, positioned after the "Embedded Style" option.
- Localization: Added the `STR_FOCUS_READING` string
#### Plumbing
- Plumbed the `focusReadingEnabled` boolean through
`EpubReaderActivity`, `Section`, and `ChapterHtmlSlimParser` to ensure
the user's setting reaches the `ParsedText` constructor during chapter
indexing.
## Additional Context
### Files Changed
- `lib/Epub/Epub/ParsedText.h / .cpp`: Core fixation logic and UTF-8
tokenization.
- `lib/Epub/Epub/Section.h / .cpp`: Cache header updates and
invalidation logic.
- `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h / .cpp`: Plumbing the
setting to text blocks.
- `src/CrossPointSettings.h`: Data persistence for the new setting.
- `src/SettingsList.h`: UI toggle implementation.
- `src/activities/reader/EpubReaderActivity.cpp`: Handling settings
changes during reading sessions.
- `lib/I18n/translations/*.yaml`: UI strings.
---
### 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
#1910 caused a massive performance degradation in the way rasterized
glyph buffers were handled during pixel iteration. This change restores
the original performance characteristics so the font generation job
finishes in a reasonable amount of time.
---
### 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
One of the recent font conversion script changes obliterated
performance. Add a verbose mode to build-sd-fonts.py to help diagnose
problems. Changed the CI process to use verbose mode.
---
### 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?** (e.g., Implements the new feature for
file uploading.)
* **What changes are included?**
## Additional Context
* 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? _**< YES | PARTIALLY | NO
>**_
Add SdCardFontFamilyInfo::pickClosestSize(targetPtSize) and have the
manager and SdCardFontSystem drive size selection from a target point
size derived from the user's font-size enum (SMALL=12, MEDIUM=14,
LARGE=16, EXTRA_LARGE=18) rather than indexing the family's sorted size
list by enum ordinal.
The ordinal-slot approach mis-selects whenever a family doesn't ship the
canonical {12,14,16,18} set: a family with only [10,14,18] would map
SMALL/MEDIUM/LARGE/EXTRA_LARGE to 10/14/18/18 — fine for SMALL but
arbitrary for the rest. Closest-pt always picks the on-disk file nearest
to the user-intended point size, with a deterministic smaller-pt
tie-break.
No change for canonical-sized families.
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* **What changes are included?**
## Additional Context
* 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? _**< YES | PARTIALLY | NO
>**_
## Summary
**What is the goal of this PR?**
* Provide a better default weight of Bitter font for improved rendering
**What changes are included?**
* Changes the default weight for Bitter font that is defined in the
custom font catalog to medium (500) from regular (400) for improved
rendering on e-ink, especially when font anti-aliasing is turned on
## Additional Context
* Bitter font at regular weight can become washed out when AA is on due
to some of the thinner stems and posts of the font. Bumping the weight
up to medium makes it sturdier and holds up better when AA is turned 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? _**< NO >**_
Fontconvert_sdcard.py:
* Move face.set_char_size() before any load_glyph() call. The validation
loop runs load_glyph() with FT_LOAD_RENDER, which renders at the
*active* size — calling it before set_char_size() wastes work at the
default size and triggers Invalid_Size_Handle on some fonts.
* Walk bitmap.buffer using bitmap.pitch with negative-pitch handling.
The previous linear iteration assumed pitch == width and a top-down
layout, which silently corrupts output for any padded or bottom-up
bitmap FreeType returns.
* Fix operator-precedence bug in 2-bit tail-padding: px << (4 - … % 4) *
2 evaluated as (px << (4 - … % 4)) * 2 due to << binding tighter than *.
Add the missing outer parens.
* Drop SMP codepoints (> U+FFFF) from kern and ligature tables before
packing — the binary format uses uint16/uint32 codepoint fields and
would raise struct.error otherwise.
* Skip GPOS kern subtables that aren't lookup type 2 (PairPos).
_extract_pairpos_subtable assumes Type-2 layout; cursive attachment and
other types reachable through the kern feature crash inside it.
Fontconvert.py:
* Force UTF-8 stdout so `python fontconvert.py … > foo.h` on Windows
doesn't emit UTF-16 / replacement characters in the generated header.
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* **What changes are included?**
## Additional Context
* 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? _**< YES | PARTIALLY | NO
>**_
instantiateVariableFont() returns a *new* TTFont with the requested axes
pinned; the existing call discards that return and proceeds to
font.save() the original (still-variable) font. The "static instance"
written to disk is therefore identical to the source variable font and
the requested axis values are silently ignored.
For sd-fonts.yaml entries that use the `variable: {wght: 400}` form
(e.g. Bitter, Lora) this means every weight ends up rasterised from the
same default-weight glyphs — bold and regular look the same.
Capture the return value, and pass two diagnostically useful kwargs
while we're touching the call:
* updateFontNames=True — rewrite the name table so the saved TTF reports
its actual weight/style instead of retaining the source variable-font
names.
* optimize=False — skip the gvar interpolation optimisation;
fully pinning every axis drops gvar anyway,
so the work would be wasted.
The atomic-write scaffolding around the call is left untouched.
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* **What changes are included?**
## Additional Context
* 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? _**< YES | PARTIALLY | NO
>**_
Add GROUP_MAX_UNCOMPRESSED_BYTES=65536 on top of the existing script-
based grouping. When adding the next glyph would push a group past the
cap, the group is closed and a new one started with the same script ID.
Without the cap, the size of a script group is bounded only by however
many glyphs the font supplies in that block. Dense scripts (CJK,
Vietnamese precomposed, user-supplied fonts with large Unicode blocks)
can produce a single group whose uncompressed size exceeds what fits in
the embedded decompressor's transient malloc on the ESP32-C3, which
manifests as a runtime allocation failure instead of a build error.
The 64 KB ceiling is large enough to hold any single built-in script
group with headroom and small enough to be a comfortable transient
allocation on-device.
The check uses byte-aligned size (4-pixel-aligned row stride × height),
which is what the decompressor actually consumes — not the packed
on-disk length. A defensive guard rejects any single glyph whose own
size exceeds the cap with a clear error pointing at the offending
codepoint.
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* **What changes are included?**
## Additional Context
* 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? _**< YES | PARTIALLY | NO
>**_
## Summary
* Add missing swedish translations
---
### 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
* Previous `fontconvert_sdcard.py` requires that the intervals are named
(i.e. listed in `INTERVAL_PRESETS`, ex. "latin1"), this change will also
allow them to be unnamed (ex. "(0x2100-0x214F)") for a more flexible
usage.
---
### 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
* **What is the goal of this PR?**
Add end-to-end integrity verification for downloaded font files by
including CRC32 checksums in the font manifest and validating downloaded
`.cpfont` files on device.
* **What changes are included?**
- `generate-font-manifest.py`: compute and include `crc32` for each
`.cpfont` asset in the generated `fonts.json` manifest.
- `FontDownloadActivity.h`: extend manifest file metadata with `crc32`
and declare checksum helper.
- `FontDownloadActivity.cpp`: parse `crc32` from manifest, compute CRC32
of downloaded files using `esp_rom_crc32_le`.
---
### 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
Removes duplicate "Download Fonts" menu entry and adds complete
navigation support to the font download activity.
## Problem
"Download Fonts" was appearing in **both** Reader settings and System
settings, creating a confusing duplicate menu entry.
## Changes
### 1. Remove Duplicate Menu Entry
- Removed `STR_DOWNLOAD_FONTS` from `systemSettings` in
`SettingsActivity.cpp`
- "Download Fonts" now only appears in **Reader settings**, positioned
right after the font family setting
- Rationale: Font settings logically belong together in the Reader
category
### 2. Navigation Improvements
- **Single-item navigation**: Replaced manual bounds checking with
`ButtonNavigator::nextIndex()` and `previousIndex()` methods
- Navigation now wraps from last item to first (and vice versa)
- **Page navigation**: Added continuous navigation handlers
- Long-pressing up/down buttons now navigates by page
- Uses `UITheme::getNumberOfItemsPerPage()` for consistent behavior
## Files Changed
- `src/activities/settings/SettingsActivity.cpp`: Removed duplicate
entry
- `src/activities/settings/FontDownloadActivity.cpp`: Navigation
improvements
## Testing
- Build: ✅ Compiled successfully
- Device testing: Recommended before merge
## Summary
* Fix missing requirement (needed by
lib/EpdFont/scripts/build-sd-fonts.py)
* Add a root requirements.txt to simplify installation
---
### 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
**Goal:** Fix bidirectional KOSync position matching between CrossPoint
and KOReader so that syncing in either direction lands on the correct
page with character-level accuracy.
**Changes included:**
**Download — `toCrossPoint` (server XPath → CrossPoint page)**
- **XPath ancestry mode for structured elements**: The previous
`ParagraphStreamer` only tracked `<p>` elements. Replaced with a full
ancestor-walking mode that correctly resolves XPaths pointing into
`<li>`, `<ul>`, and other structured elements. Char offset within the
target element is bounded to the matched element's content only.
- **Slash-in-attribute-value corrupts depth tracking**:
`processByteInTag()` treated every `/` byte as a self-closing tag
marker, including `/` inside quoted attribute values (e.g.
`xmlns="http://..."`, `src="Links/image.jpg"`). This drove `htmlDepth`
to 0 prematurely, causing the ancestry search to exit far short of the
target paragraph. Fixed with `inAttrQuote` tracking.
- **Off-by-one in page formula**: `intra * totalPages` rounds up
incorrectly for last-page positions. Changed to `intra * (totalPages -
1)` to map the `[0, 1]` intra fraction correctly onto the `[0,
totalPages-1]` page range. Example: page 14 of 17 was returned as 15.
**Upload — `toKOReader` (CrossPoint page → server XPath)**
- **Off-by-one in page-to-intra formula**: Symmetric fix — `pageNumber /
totalPages` changed to `pageNumber / (totalPages - 1)`, with the guard
updated from `> 0` to `> 1` to avoid division by zero.
- **`<li>`-based XPath generation**: When the current page starts on a
list item, `findXPathForProgress` now generates `ul[N]/li[M]` XPaths
rather than falling back to the preceding `<p>`. Requires the new
`listItemIndex` field in `PageLutEntry` (section cache version bumped to
23).
- **Text-node precision with correct `text()[N].M` format**: KOReader
expects `text()[N].M` where `N` is the 1-based index of the specific
text node within the element. The previous attempt generated `text().M`
(no brackets), which caused KOReader to jump to the front of the book.
Implements a per-element text-node index stack in
`XPathProgressResolver` — parallel to the existing element path stack —
that correctly tracks text node indices relative to each element. Empty
text nodes from bare anchor elements (`<a id="anchor"/>`) are
intentionally skipped, matching KOReader's own text node counting
behavior.
**Reviewer-caught bugs**
- **Double `onCloseTag()` on malformed `</br/>`**: Both the `tagIsClose`
path and the self-closing `/` check were firing, double-decrementing
`htmlDepth`. Fixed with a `!tagIsClose` guard.
- **Dangling pointer in `LOG_DBG`**:
`std::to_string(*nextParagraphPage).c_str()` passed a pointer to a
temporary destroyed before the variadic call. Fixed with `snprintf` into
a stack `char[8]` buffer.
## Additional Context
- Section cache version bumped from 22 → 23 due to the new
`listItemIndex` field in `PageLutEntry`. Users upgrading will see a
one-time re-render of all cached sections on first load — no data loss.
- The `textNodeIndexStack` in `XPathProgressResolver` is a
`std::vector<int>` that mirrors the existing `path` and `parentStates`
stacks — same depth, same lifetime. No additional heap pressure beyond
what was already present.
- All fixes verified on device with *Gentle and Lowly* by Dane C.
Ortlund (spine 21, 17 pages). Download syncs land on the correct page;
upload syncs land at the correct paragraph with character-level offset.
## Test plan
- [ ] Download: sync from KOReader → CrossPoint lands on correct page
for `text()[N].M` XPaths
- [ ] Download: ancestry correctly resolves `<li>` positions inbound
from KOReader
- [ ] Upload: sync from CrossPoint → KOReader lands within one page for
mid-paragraph positions
- [ ] Upload: sync from CrossPoint → KOReader correctly targets `<li>`
elements when page starts on a list item
- [ ] Upload: `text()[N].M` format XPaths do not cause KOReader to jump
to front of book
- [ ] Section cache version 23: delete `.crosspoint/` and verify clean
re-parse with no crashes
---
### AI Usage
Did you use AI tools to help write this code? **YES** — developed with
Claude Code (Anthropic).
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary
* **What is the goal of this PR?** Fix KOSync failing with "Network
error" on large/complex EPUBs, and simplify the sync navigation flow by
removing the callback/result pattern.
* **What changes are included?**
This PR combines the approaches from #1855 and #1760 into a single,
cleaner solution:
**Memory fix (from #1855):**
- `EpubReaderActivity` pre-computes the local KOReader position and
chapter name, then explicitly releases `epub` and `section` before
launching `KOReaderSyncActivity`. This frees ~65KB measured on device,
giving the TLS handshake sufficient heap. The root cause was
`MBEDTLS_ERR_X509_ALLOC_FAILED` (-0x2880) when a 3-cert chain consumed
~48KB during the handshake with only ~50KB available.
- `KOReaderSyncActivity` no longer receives a `shared_ptr<Epub>` at
construction — it lazy-loads the Epub after TLS only if remote progress
is found (`ensureEpubLoaded()`).
- Added `MIN_HEAP_FOR_TLS = 55000` guard in `KOReaderSyncClient` —
returns `LOW_MEMORY` early if aggregate free heap is too low before
attempting a TLS connection.
**Navigation simplification (from #1760):**
- Replaced `startActivityForResult` + callback with
`activityManager.replaceActivity` / `activityManager.goToReader`.
Progress is saved to `progress.bin` before the epub is released
(cancel/upload paths) and in `saveProgressAndReturn` (apply remote
path). The reader re-launches from the saved position naturally via
`goToReader`, eliminating the need to reload the epub in a callback.
- Extracted `ReaderUtils::saveProgress()` as a shared helper used by
both `EpubReaderActivity` and `KOReaderSyncActivity`.
- Added `STR_SAVE_PROGRESS_FAILED` to all 22 language files for the case
where writing the synced position to SD fails.
**Orientation fix (found during device testing):**
- `EpubReaderActivity::onExit()` resets the renderer to portrait before
destruction. With `replaceActivity` the reader is fully torn down before
KOSync starts, so KOSync was always rendering in portrait even when
reading in landscape. Fixed by calling `ReaderUtils::applyOrientation`
in `KOReaderSyncActivity::onEnter()`.
## Additional Context
Heap measurements on device (large EPUB with complex CSS):
| Metric | Before | After |
|---|---|---|
| Heap before Epub release | 88,156 bytes | — |
| Heap after Epub release | — | 153,892 bytes (+65,736) |
| Heap at TLS handshake | ~50,000 bytes (fails) | ~116,384 bytes
(passes) |
| Min-free-ever during sync session | 2,600 bytes | 33,052 bytes |
| TLS result | `MBEDTLS_ERR_X509_ALLOC_FAILED` | HTTP 200 |
Tested on device: sync from inside a large EPUB in both portrait and
landscape, cancel, apply remote progress, upload local progress.
---
### AI Usage
Did you use AI tools to help write this code? _**YES**_
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a complete SD card font subsystem that enables users to install and
use custom fonts beyond the three built-in families. This combines the
back-end firmware support (#1327) with the font configuration, build
pipeline, CI distribution, and user-facing management UI (#1392).
Core font system:
- Custom .cpfont binary format (v4) with multi-style support (regular,
bold, italic, bold-italic) packed into a single file per size
- On-demand glyph loading from SD card with two-pass prewarm rendering
to bulk-read glyphs per page, achieving near-flash performance for
Latin text (~697ms vs ~681ms) and viable CJK rendering (~32% slower)
- Persistent advance cache for layout measurement without SD I/O
- Overflow ring buffer for glyph cache misses during rendering
- Memory-conscious design: only advance tables kept in RAM; glyph
bitmaps, kern tables, and ligatures loaded on demand from SD
Font management:
- On-device WiFi download from GitHub Releases with manifest-based
discovery, install/update detection, and progress UI
- Web interface font upload, listing, and deletion via /fonts page
- Manual SD card copy to /fonts/ or /.fonts/ directories
- Font selection integrated into Settings > Reader > Font Family
Build pipeline:
- Declarative YAML config (sd-fonts.yaml) as single source of truth
for the 17-family font library (serif, sans, mono, accessibility)
- Python converter (fontconvert_sdcard.py) for TTF/OTF to .cpfont with
FreeType rasterization, class-based kerning, and ligature extraction
- Parallel build orchestrator with variable font instance extraction
- CI workflow publishing versioned + stable releases to a dedicated
crosspoint-fonts repository with auto-incrementing revision tags
- Centralized version constants (cpfont_version.py) shared across
build tooling and CI, with firmware headers as manual sync points
Additional fixes:
- CJK characters no longer get hyphens inserted at line breaks
- Advance table eliminates 30+ second stalls during CJK section
indexing for paragraphs with >512 unique codepoints
Closes#930
Co-authored-by: Zach Nelson <zach@zdnelson.com>
Co-authored-by: Justin <itsthisjustin@users.noreply.github.com>
Co-authored-by: jpirnay <jens@pirnay.com>
Co-authored-by: mcrosson <kemonine@kemonine.info>
## Summary
Add ability to show a status bar for 1-bit XTC files (closes#1848)
Overlays a status bar (similar style to that for epubs) over the image.
Defaults to hidden, users can set to show and either top or bottom of
the screen within "Customize Status Bar" reader settings.
I've leaned toward a simple overlay approach rather than trying anything
clever e.g. resizing the original image or allowing for shifting the
image around. I think it's more straight forward for the user to create
a buffer zone when generating the XTC files.
For 2-bit image files, it'd require more work to handle the multiple
render passes so I'm holding off on that.
Sample from a Japanese text XTC
<img width="400" height="600" alt="image"
src="https://github.com/user-attachments/assets/03ac67cf-acec-4b49-969f-73e3656760ce"
/>
### 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 | PARTIALLY | NO
>**_
YES - Codex
---------
Co-authored-by: Zach Nelson <zach@zdnelson.com>
## Summary
Follow-up to 23aad213 which removed redundant FsFile close() calls from
the codebase. This updates CLAUDE.md so AI agents and contributors stop
reintroducing them. Adds the build flag to the Critical Build Flags
section with a clear "do not add file.close() on local FsFile variables"
rule, enumerates the three cases where explicit close is still required
(close before Storage.remove, close before reopening the same variable,
and member variables that outlive function scope), and updates the
HalStorage example, RAII guidance, and Activity Lifecycle sections to be
consistent with the new policy.
---
### 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?**
Add Wi-Fi network management to the Web UI settings page, similar to
existing OPDS server management, so users can view, add, edit, and
delete saved Wi-Fi credentials from the browser. Closes
https://github.com/crosspoint-reader/crosspoint-reader/issues/1544 and
https://github.com/crosspoint-reader/crosspoint-reader/discussions/607
**What changes are included?**
- Added Wi-Fi API endpoints:
- GET /api/wifi Listing saved networks (without exposing plaintext
passwords)
- POST /api/wifi Creating/updating networks
- POST /api/wifi/delete Deleting networks by index
- Added a new Wi-Fi Networks management section web UI settings page
<img width="921" height="712" alt="image"
src="https://github.com/user-attachments/assets/42c20a63-f335-4389-8246-d38065d6b65a"
/>
---
### 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: Copilot <copilot@github.com>
## Summary
* **What is the goal of this PR?**
Enhance file browser long-press behavior so the delete action now works
for both files and directories.
* **What changes are included?**
Updated `FileBrowserActivity.cpp` to support for directory deletion on
long-press
---
### 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
Deduplicated battery drawing code for Roundraff theme with other themes.
Now `BaseTheme` provides non-virtual `drawBatteryLeft` and
`drawBatteryRight` methods, which use a shared static
`drawBatteryOutline` implementation, and defer to a virtual
`fillBatteryIcon` implementation. In Classic and Roundraff
`fillBatteryIcon` uses a solid fill, while Lyra and Lyra Extended use a
segmented fill.
The charging indicator inside the battery was missing for Roundraff, but
is now consistent with other themes because of the shared drawing
implementation.
---
### 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
With the Roundraff theme selected and no recent books, the home screen
menu shows a "Continue Reading" option and menu handling is offset by
one ("Continue Reading" actually does "Browse Files", "File Transfer"
actually does "Settings", etc.) Simple fix here is to omit the "Continue
Reading" menu item when there are no recent books.
---
### 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**_
When Power+Down is released with a slight stagger (Down before Power),
the Power release event leaked through to the activity loop, triggering
short-press Power actions like clip selection. Add a
screenshotComboActive flag that suppresses all input processing until
Power is fully released after a screenshot combo.
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* **What changes are included?**
## Additional Context
* 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? _**YES**_
## Summary
Before, any sub-activity of a reader activity needed to override
`isReaderActivity` to maintain correct bookkeeping through
`ActivityManager::isReaderActivity`. We could easily miss this in any
new sub-activities. Instead, simplify so each reader activity correctly
reports and then `ActivityManager` checks for any reader activity in its
stack.
---
### 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
Simplify duplicated code in `XtcReaderActivity` to use
`ReaderUtils::detectPageTurn`. This implementation is now shared with
`EpubReaderActivity` and `TxtReaderActivity`. Also deduplicated the
chapter skip time constant.
---
### 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
Update the Open X4 SDK to point to the CrossPoint organization fork.
This allows us to take SDK changes without being blocked on the upstream
repository.
---
### 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
* **What is the goal of this PR?**
Fixes a bug in the jpeg nearest-neighbor downscaler where source image
rows containing visible content may be cropped.
* **What changes are included?**
Split the single `fineScaleFP`/`invScaleFP` scale pair in
`JpegToFramebufferConverter.cpp` into separate X
(`fineScaleFPX`/`invScaleFPX`) and Y (`fineScaleFPY`/`invScaleFPY`)
pairs
## Additional Context
The one case that I encountered in my epub:
<img width="480" height="37" alt="img_6_0"
src="https://github.com/user-attachments/assets/0de3778c-cf3d-42dc-a1ba-4ba729f6b1c4"
/>
The image generated from pxc cache with the latest commit:
<img width="464" height="36" alt="img_6_0_master pxc"
src="https://github.com/user-attachments/assets/92ba06e5-be15-4db6-840e-882bdddb1baf"
/>
With this fix, the bottom outline of each character is still there:
<img width="464" height="36" alt="img_6_0_fixed pxc"
src="https://github.com/user-attachments/assets/4246ce55-5a20-480f-862f-7988f804c1fb"
/>
---
### 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 vertical misalignment between
the "File Manager" title and the action buttons (Upload / New Folder /
Delete Selected).
* **What changes are included?**
- `h2`: `margin-top: 0` → `margin: 0` — removes the default browser
`margin-bottom` that was inflating the height of the header-left flex
container, pushing the calculated flex center below the visual midpoint
of the title text
- `.page-header-left`: `align-items: baseline` → `align-items: center` —
ensures the title and breadcrumbs are center-aligned with each other and
with the buttons
### Before
<img width="1198" height="560" alt="Screenshot From 2026-04-30 22-29-28"
src="https://github.com/user-attachments/assets/b9a43069-cd93-4028-ab21-75be253ed1f0"
/>
### After
<img width="1198" height="560" alt="Screenshot From 2026-04-30 22-48-49"
src="https://github.com/user-attachments/assets/b9977c65-e479-4490-8ec3-15ae99cfccd7"
/>
## Additional Context
Small CSS-only change, no JavaScript or HTML structure touched.
---
### AI Usage
Did you use AI tools to help write this code? _**YES**_
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary
Added RAM delta output to firmware_size_history.py output, e.g.:
```
% scripts/firmware_size_history.py --commits adcd7968e18472
[info] Will restore to 'sd-card-fonts' when finished.
[info] Stashing uncommitted changes...
[info] Building 2 commits...
[1/2] adcd7961c9 feat: self-heal from transient WiFi loss, add dBm indicator during WebServerActivity (#1780)
Building (env: default)...
Flash: 5,751,209, RAM: 97,996 bytes
[2/2] 8e184724d1 Merge branch 'master' into sd-card-fonts
Building (env: default)...
Flash: 5,762,395, RAM: 97,988 bytes
[info] Restoring 'sd-card-fonts'...
[info] Restoring stashed changes...
Commit Flash Delta RAM Delta Title
────────── ─────────── ─────── ─────────── ─────── ────────────────────────────────────────
adcd7961c9 5,751,209 97,996 feat: self-heal from transient WiFi loss, add dBm indicator during WebServerActivity (#1780)
8e184724d1 5,762,395 +11,186 97,988 -8 Merge branch 'master' into sd-card-fonts
```
---
### 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?**
I was seeing hangs during File Transfer. Troubleshooting via serial
showed it had to do with the SHUTTING_DOWN state. Strengthened the WiFi
State Machine a bit and added self-healing. Additionally, I found it
obnoxious to need to keep referring to serial for my wifi strength, so a
dBm meter is added opposing the SSID when on the File Transfer page.
* **What changes are included?**
The dBm meter was at risk of causing rapid screen updates (if, say, we
hovered right around a threshold), so I've implemented a basic
hysteresis around this. RISING/FALLING would be more canonical variable
names, but are reserved in the framework.
## Additional Context
My X4 has a terrible antenna, apparently, and I was running into this
failure condition pretty regularly. Wifi "bars" were chosen as a
relatively pan-cultural glyph rather than relying on localization.
Tested on hardware at -83 dBm under sustained EPUB upload (60 books, ~30
MB). Transient losses up to ~14s now ride through; pre-fix the same
losses required a power-cycle after as little as a 2s blip.
---
### 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? Nope. Commits are hand
written. Claude was used to build a local test harness for validation
only during iteration.
## Summary
Simplify code calculating compile-time array sizes with
`sizeof(array)/sizeof(element)` to use `std::size`.
---
### 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
* Add new block style stack to track accumulated styles
* When dropping into nested block, apply new styles on last BlockStyle
in the stack and push to the back
* When leaving block, pop the stack
* Fix issue where the image % width always went off screen width, use
current container width based on accumulated styles
* Bump section cache version to regenerate pages
## Additional Context
Fixes https://github.com/crosspoint-reader/crosspoint-reader/pull/1581
more correctly, and includes fix from
https://github.com/crosspoint-reader/crosspoint-reader/pull/1580
Consider:
```html
<div class="c1"> <!-- marginLeft: 10px -->
<div class="c2"> <!-- marginLeft: 20px -->
<p class="c3">text</p> <!-- marginLeft: 5px -->
</div>
<p class="c4">text2</p> <!-- marginLeft: 5px -->
</div>
```
| Element | Expected | Before #1581 (leaked) | After #1581 (reset) |
This PR (style stack) |
| --- | --- | --- | --- | --- |
| `c3` | 10+20+5 = 35px | 35px | 25px | 35px |
| `c4` | 10+5 = 15px | 35px (wrong, leaked c2) | 5px (wrong, lost c1) |
15px |
This PR replaces the reset-on-close approach with a proper block style
stack. When a block element opens, its resolved style is pushed onto the
stack. When it closes, the stack pops back to the parent's style. This
correctly accumulates nested margins/padding while preventing style
leakage to siblings.
---
### 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: Zach Nelson <zach@zdnelson.com>
## Summary
* Add new block style stack to track accumulated styles
* When dropping into nested block, apply new styles on last BlockStyle
in the stack and push to the back
* When leaving block, pop the stack
* Fix issue where the image % width always went off screen width, use
current container width based on accumulated styles
* Bump section cache version to regenerate pages
## Additional Context
Fixes https://github.com/crosspoint-reader/crosspoint-reader/pull/1581
more correctly, and includes fix from
https://github.com/crosspoint-reader/crosspoint-reader/pull/1580
Consider:
```html
<div class="c1"> <!-- marginLeft: 10px -->
<div class="c2"> <!-- marginLeft: 20px -->
<p class="c3">text</p> <!-- marginLeft: 5px -->
</div>
<p class="c4">text2</p> <!-- marginLeft: 5px -->
</div>
```
| Element | Expected | Before #1581 (leaked) | After #1581 (reset) |
This PR (style stack) |
| --- | --- | --- | --- | --- |
| `c3` | 10+20+5 = 35px | 35px | 25px | 35px |
| `c4` | 10+5 = 15px | 35px (wrong, leaked c2) | 5px (wrong, lost c1) |
15px |
This PR replaces the reset-on-close approach with a proper block style
stack. When a block element opens, its resolved style is pushed onto the
stack. When it closes, the stack pops back to the parent's style. This
correctly accumulates nested margins/padding while preventing style
leakage to siblings.
---
### 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: Zach Nelson <zach@zdnelson.com>
## Summary
Small cleanup to avoid a dynamically allocated static structure for auto
page-turn rate values.
---
### 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
Small simplification to node sorting algorithm in
GfxRenderer::fillPolygon.
---
### 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
**What is the goal of this PR?**
Restore compatibility with older XTC files that use the legacy 48-byte
header layout, so they open correctly instead of failing
header/page-table validation.
**What changes are included?**
- Updates the XTC parser to accept legacy files where pageTableOffset
starts at 0x30 instead of the newer full header size.
- Avoids treating legacy header bytes as a valid chapterOffset, so older
files are not misclassified as having chapters.
- Switches XTC file size and seek operations to 64-bit-safe wrapper
calls in HalStorage, which matches the XTC format’s 64-bit offset fields
and keeps page/chapter lookups from narrowing offsets.
- Keeps the existing bounds checks and improves related logging when XTC
page loads fail.
## Additional Context
- This is a narrow compatibility fix for XTC handling. The functional
change is in the XTC parser; the HalStorage changes are supporting
wrapper methods needed by that parser update.
- Risk should be low outside XTC reading, since the HAL changes only
expose existing underlying file APIs rather than changing storage
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**
## Summary
The GitHub recent release API returns lots of data, including the full
release notes. For 1.2.0, that data is 30,530 bytes. The existing
OtaUpdater HTTP handler and single-buffer JSON parsing can't reliably
handle that much data in the constrained ESP32 environment.
This change does a few things:
1. Adds a very simple lib/JsonParser/StreamingJsonParser.cpp with
SAX-style callbacks to read JSON data incrementally.
2. Adds a very simple lib/JsonParser/ReleaseJsonParser.cpp building on
StreamingJsonParser, which specifically parses the GitHub release JSON
for the release version, URL, and size.
3. Updates OtaUpdater.cpp to use an instance of ReleaseJsonParser to
incrementally parse the large response it may receive from GitHub.
Building from this commit while overriding my local version to 1.1.9, I
was able to run the OTA update process ~5 times in a row successfully.
Fixes#1561 (second part, after #1805).
---
### 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
Omit any duplicate strings in non-English localization lookup tables.
For non-English lookup offsets, tag bit 15 to indicate the offset
applies to the English localization table.
Saves 18,766 bytes of flash.
---
### 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
The file type was shown twice in the file browser table: as a bold badge
in the **Name** column and as plain text in the **Type** column.
**Before:** badge in Name column + redundant text in Type column
**After:** badge lives exclusively in the Type column
| | Before | After |
|---|---|---|
| Folder | Name: `📁 folder` **FOLDER** · Type: `Folder` | Name: `📁
folder` · Type: **FOLDER** |
| EPUB | Name: `📗 book.epub` **EPUB** · Type: `EPUB` | Name: `📗
book.epub` · Type: **EPUB** |
| Other | Name: `📄 file.txt` · Type: `TXT` | unchanged |
### Before
<img width="1125" height="571" alt="Screenshot From 2026-04-30 22-07-59"
src="https://github.com/user-attachments/assets/794c66d3-5ad5-4536-a142-708989b9dee8"
/>
### After
<img width="1125" height="571" alt="Screenshot From 2026-04-30 22-08-45"
src="https://github.com/user-attachments/assets/f9fb2898-b165-478a-813b-f2290ad3e42c"
/>
## Changes
- Removed `epub-badge` and `folder-badge` from the Name column
- Moved badges into the Type column, replacing the redundant plain text
- Non-EPUB files are unaffected (extension text remains in Type column)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary
Another follow up to #1408. That PR revised the language.bin settings
file to version 2. After merging, it occurred to me that it would make
more sense to have the language inside the overall settings.json file,
easy for users to see and change directly.
This change adds migration from language.bin to settings.json. It only
migrates from the pre-#1408 v1 language.bin format, because v2 has only
been in for a couple of commits and I don't think we need long-term
maintenance code to handle it.
#1408 had a potential long-term maintenance issue for migration. It
assumed that the enum order of languages never changes, only grows. So,
e.g. "Portuguese (Portugal)" could never be added next to "Portuguese
(Brazil)", only appended. This change includes a hardcoded frozen
ordering of the language indices as they existed at 2f969a9, and
migrates to storing a language code string in the settings.json instead
for future reordering flexibility.
Similar to the settings.bin to settings.json migration, which renames
settings.bin to settings.bin.bak, this change renames language.bin to
language.bin.bak after adding the language setting to settings.json.
---
### 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
Quick follow up to #1408. We overlooked the fact that the change wasn't
using the LOG_ macros. Also, removed redundant file.close() calls, as in
23aad21.
---
### 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?** Reduce the flash footprint of the
i18n string data and tooling improvements to `gen_i18n.py`.
* **What changes are included?**
### 1. `lib/I18n/I18n.cpp` — use offset-based lookup
`I18n::get()` previously dereferenced a `const char* const*` pointer
array. It now uses a two-field `LangStrings` struct (a flat char blob +
a `uint16_t` offset table) generated for each language:
```cpp
// before
const char* const* strings = getStringArray(_language);
return strings[index];
// after
const LangStrings lang = getLanguageStrings(_language);
return lang.data + lang.offsets[index];
```
Lookup cost is unchanged — still O(1), one array load and one addition.
### 2. `scripts/gen_i18n.py` — new generated layout
Each language's string data is now emitted as:
- **`STRINGS_XX_DATA[]`** — a single `const char[]` blob of all strings
concatenated with `\0` separators.
- **`OFFSETS_XX[]`** — a `uint16_t` array of one byte-offset per `StrId`
into the blob.
Previously each language had a `const char* const STRINGS_XX[]` pointer
array (4 bytes/entry on ESP32-C3).
#### Flash savings
| Table type | Size per language | 19 languages |
|---|---|---|
| `const char*` pointer array (before) | `339 × 4 = 1,356 B` | **25,764
B** |
| `uint16_t` offset table (after) | `339 × 2 = 678 B` | **12,882 B** |
| **Saved** | | **12,882 B (~12.6 KB)** |
String data size is unchanged — 133,092 B across 19 languages.
**Total: 158,856 B → 145,974 B** (pointer tables → offset tables).
### 3. Build-time stripping of unused strings
`gen_i18n.py` now scans the `src/` and `lib/` trees for `STR_*`
references and, during a PlatformIO build, automatically omits the 52
strings that are defined in YAML but never referenced in code. This
further reduces the compiled output from 339 → 287 string keys per
language.
---
## `gen_i18n.py` CLI reference
```
python gen_i18n.py [translations_dir [output_dir]] [options]
```
| Argument / Flag | Default | Description |
|---|---|---|
| `translations_dir` | `lib/I18n/translations` | Path to the
per-language YAML files |
| `output_dir` | `lib/I18n/` | Where to write the generated `.h` /
`.cpp` files |
| `--src-dirs DIR [DIR …]` | `src lib` | Directories scanned for `STR_*`
usage |
| `--strip-unused` | off | Remove unreferenced `STR_*` keys from
generated output |
| `--verbose` / `-v` | off | Print per-key INFO/WARNING messages and
`Generated:` lines |
The PlatformIO build (SCons `pre:` hook) calls `main(strip_unused=True)`
automatically, so unused strings are always stripped from firmware
builds without any manual flag.
**Default run output** (no flags) shows a per-language summary table:
```
Language Code Own Fallback Unused Data (B)
------------------ ---- --- -------- ------ --------
English EN 339 0 52 5,266
Belarusian BE 310 29 52 9,673
…
Total: 339 | Used in code: 287 | Never used: 52
Flash (now): 133,092 B strings + 12,882 B offset tables (uint16_t) = 145,974 B
Flash (before): 133,092 B strings + 25,764 B pointer tables (ptr32) = 158,856 B
Saved by offset tables: 12,882 B
```
---
### AI Usage
Did you use AI tools to help write this code? **YES** (GitHub Copilot)
---------
Co-authored-by: Zach Nelson <zach@zdnelson.com>
## Summary
I love the new screenshot feature but I can tell that soon I'm gonna
have a folder full of screenshots and not know what's what. It would be
great if the folder could self organize.
When a screenshot is taken while reading, the filename would now include
the book title, chapter (EPUB), page number, and progress percentage.
Example: My-Great-Book_ch3_p5_42pct_12345.bmp
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* **What changes are included?**
## Additional Context
* Non-reader screens keep the existing screenshot-<millis>.bmp naming.
---
### 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 >**_
I reviewed all the changes but I don't have any experience with this
project so this is my best guess
## Summary
* **What is the goal of this PR?** Fix KOReader sync failing on HTTPS
servers due to TLS out-of-memory on ESP32-C3
* **What changes are included?**
- Replace `WiFiClientSecure`/`HTTPClient` with `esp_http_client`
(ESP-IDF native API) for all KOSync HTTP requests
- Use 2KB TLS buffers instead of the default 16KB — KOSync payloads are
tiny JSON (<1KB), so this is more than sufficient
- Use `esp_crt_bundle_attach` for proper TLS certificate verification
(replaces `setInsecure()`)
- Strip trailing slashes from server URL to prevent double-slash in API
paths (e.g. `https://server.com//users/auth`)
- Add `lastHttpCode` static field for diagnostics
- Add free heap logging to help debug memory issues
### Problem
The ESP32-C3 has ~46KB free heap after WiFi is initialized.
`WiFiClientSecure` allocates 16KB for TLS RX + 16KB for TLS TX = 32KB
just for the TLS buffers, leaving almost no room for the actual TLS
handshake (which needs additional dynamic allocations). This causes
KOReader sync to:
1. Fail silently with network errors on HTTPS servers (including the
default `sync.koreader.rocks`)
2. Occasionally crash with heap exhaustion
### Solution
`esp_http_client` (the ESP-IDF native HTTP client) allows configuring
`buffer_size` and `buffer_size_tx` independently. Setting both to 2KB
(total 4KB) leaves plenty of heap for the TLS handshake while still
being sufficient for KOSync's small JSON payloads.
This also fixes the `setInsecure()` anti-pattern —
`esp_crt_bundle_attach` provides proper certificate verification using
the ESP-IDF's built-in CA bundle, so credentials and reading history are
no longer sent over unverified TLS connections.
### Files changed
| File | Change |
|------|--------|
| `lib/KOReaderSync/KOReaderSyncClient.cpp` | Replace
WiFiClientSecure/HTTPClient with esp_http_client; add ResponseBuffer,
base64 encoder, createClient helper |
| `lib/KOReaderSync/KOReaderSyncClient.h` | Add `lastHttpCode` static
field |
| `lib/KOReaderSync/KOReaderCredentialStore.cpp` | Strip trailing
slashes from base URL |
## Additional Context
Tested on a CrossPoint X4 (ESP32-C3 with 4MB flash). Before this change,
KOSync auth to `sync.koreader.rocks` (HTTPS) would fail ~80% of the
time. After: works reliably.
The `base64Encode` helper is needed because `esp_http_client` doesn't
have a built-in `setAuthorization()` method like `HTTPClient` does. This
is used for the HTTP Basic Auth header required by Calibre-Web-Automated
KOSync servers.
Fixes#581
---
### 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**_ AI
assisted with the esp_http_client migration pattern and base64 encoder.
The root cause analysis (TLS buffer OOM) and solution design were
manual.
---------
Co-authored-by: trilwu <trilwu@users.noreply.github.com>
Co-authored-by: Justin Mitchell <justin@jmitch.com>
This allows KOSync to work for files downloaded from OPDS when set to
use filename matching
## Summary
* **What is the goal of this PR?** Brings the names of files downloaded
via OPDS into line with the KOReader OPDS plugin so the files sync when
KOSync is set to filename matching
* **What changes are included?** Change the filename generated by the
OPDS browser from "\<title\> - \<author\>" to "\<author\> - \<title\>"
so it matches KOReader.
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
Both KOReader and crosspoint sanitise the filename by removing special
characters. This step seems to already use the same rules.
---
### 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
- **What is the goal of this PR?** Fix chapter-opener text collapsing to
1-2 words per line in EPUBs that apply large em-based horizontal CSS
insets.
- **What changes are included?** Caps `marginLeft`, `marginRight`,
`paddingLeft`, and `paddingRight` at 2em in `BlockStyle::fromCssStyle`
(`lib/Epub/Epub/blocks/BlockStyle.h`). Vertical margins/padding are
unchanged - the bug is horizontal-only.
## Additional Context
**Repro:** *Mother Night* by Kurt Vonnegut, Chapter 21 ("My best
friend..."). The chapter-opening element's embedded CSS sets a large
horizontal inset.
- Settings: Embedded Style = On, Justify alignment (default).
- Before: 1-2 words per line with a visible river between them.
- After: body text fills the usable page width like every other
paragraph.
**Why the clamp lives in `fromCssStyle`:** This is the single point
where CSS lengths resolve to pixels, so clamping here keeps both
`effectiveWidth` call sites in `ChapterHtmlSlimParser` (`:1131-1133`
makePages, `:841-844` long-block split) consistent with the
`leftInset()` xOffset. A width-only clamp at either call site would
leave text pushed right by a large xOffset.
**Test plan:**
- [x] Flashed to Xteink X4. Chapter 21 of *Mother Night* renders
normally with Embedded Style on.
- Users with cached layouts of affected books need to delete
`.crosspoint/epub_<hash>/sections/` (or the whole `.crosspoint/`) on the
SD card to pick up the fix.
**Before / After:**
<p float="left">
<img height="400" alt="IMG_0320"
src="https://github.com/user-attachments/assets/de8815ae-f2f4-4845-be1f-449c5a25d191"
/>
<img height="400" alt="IMG_0324"
src="https://github.com/user-attachments/assets/95f5f4cb-8aa1-418e-b611-18e0c342cbef"
/>
</p>
---
### 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 >**_
I used Claude Code (Opus 4.7) to evaluate the codebase and find the
relevant code related to this rendering. From there, it was human
designed, reviewed and tested.
Co-authored-by: rhoopr <>
## Summary
The Python scripts in the current repo have many requirements that are
not mentioned in any requirements.txt files, so I have therefore added
them to the directories that need them.
Question: Should these changes maybe moved into a "root"
requirements.txt 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? _**NO**_
## Summary
- Add new `roundedraff` theme.
- Fix sleep screen artifact where book cover showed grid lines in
`Cover` mode with `Crop`.
## Additional Context
## Key changes
- Added `src/components/themes/roundedraff/` theme implementation.
- Updated sleep cover rendering logic in:
- `src/activities/boot_sleep/SleepActivity.cpp`
- `src/activities/boot_sleep/SleepActivity.h`
- Improved cover generation/regeneration paths in:
- `lib/Epub/Epub.cpp`
- `lib/Epub/Epub.h`
- `lib/Txt/Txt.cpp`
- `lib/Txt/Txt.h`
## Verification
- Sleep screen set to Book cover
- Sleep screen cover mode set to Crop
- Confirmed grid artifacts no longer appear on cover sleep screen.
### 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: CaptainFrito <yzq6x5zypy@privaterelay.appleid.com>
Co-authored-by: Zach Nelson <zach@zdnelson.com>
Co-authored-by: Uri Tauber <142022451+Uri-Tauber@users.noreply.github.com>
## Summary
* **What is the goal of this PR?**
Enables the PlatformIO build cache to speed up compilation.
## Additional Context
Significant improvement when switching branches, as unchanged files
won't need re-compilation.
See
[Docs](https://docs.platformio.org/en/latest/projectconf/sections/platformio/options/directory/build_cache_dir.html).
Note: May need to manually delete .cache folder if updating toolchain or
framework.
---
### 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
* Includes short SHA in version string for better version tracking, so
it looks like `1.1.0-dev-feat-kosync-xpath-05c6cf8`
* Closes
https://github.com/crosspoint-reader/crosspoint-reader/issues/1247
---
### 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
* Add swedish translation of strings added by "feat: Support for
multiple OPDS servers (#1209)"
---
### 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
* Adds pre-commit git hook that automatically runs
./bin/clang-format-fix
---
### 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: Copilot <175728472+Copilot@users.noreply.github.com>
## Summary
* **What is the goal of this PR?**
Fix the visual keyboard cursor not advancing when typing a space. Typing
one space leaves the cursor at position 0; typing two spaces advances it
only by one space width. This affects all input types except URL.
* **What changes are included?**
Replace `getTextWidth()` with `getTextAdvanceX()` in four locations
within `KeyboardEntryActivity::render()` for cursor positioning and
line-wrapping calculations.
## Additional Context
* **Root cause**: `getTextWidth()` returns the bounding-box width of the
drawn glyphs. The space glyph has `width=0` and `height=0` (it's
invisible), so `getTextWidth(" ") == 0`. The trailing `advanceX` of the
last character is only flushed when the *next* character is processed,
so a string ending in space reports zero width. `getTextAdvanceX()`
correctly includes the final glyph's advance, matching how `drawText()`
actually positions characters.
---
### 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**_
Adds royalty.dev as a funding platform for contributors by adding custom
funding link and badge to README
A maintainer would need to click the "Are you the maintainer?" button on
https://app.royalty.dev/crosspoint-reader/crosspoint-reader to install
the github app which pulls the PRs in and ranks them.
(Disclosure, Royalty is a product I've recently built and am using for
my own OSS and currently gating custom font builds and nightlys on
crosspointreader.com)
This page currently can collect donations but without the integration it
wont notify contributors and it wont rescore fresh PRs.
## Summary
* Add the swedish translations of the text strings added by #1697
---
### 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
* Add support for configuring and using multiple OPDS servers, replacing
the previous single-server limitation. Closes
https://github.com/crosspoint-reader/crosspoint-reader/issues/1178
* New OpdsServerStore singleton (modeled after WifiCredentialStore) that
persists up to 8 OPDS servers to /.crosspoint/opds.json with MAC-based
password obfuscation.
* One-time migration from legacy single-server fields in
CrossPointSettings to the new store on first boot.
* New OpdsServerListActivity for the device UI — works in two modes: a
settings list (add/edit/delete servers) and a picker (select which
server to browse). When only one server is configured, the picker is
skipped automatically.
* Renamed CalibreSettingsActivity → OpdsSettingsActivity for clarity. It
now edits individual OpdsServer entries (name, URL, username, password,
delete).
* OpdsBookBrowserActivity now receives an OpdsServer at construction and
uses its credentials for all fetches/downloads, and shows the server
name in the header.
* HttpDownloader::fetchUrl and downloadToFile accept optional per-call
username/password parameters instead of reading from global settings.
* REST API endpoints on CrossPointWebServer: GET /api/opds, POST
/api/opds, POST /api/opds/delete — passwords are never exposed over the
API (only a hasPassword flag), and omitting the password field on update
preserves the existing one.
* Web UI (SettingsPage.html) with dynamic OPDS server management cards —
add, edit, save, and delete servers from the browser.
<img width="932" height="906" alt="SCR-20260416-stvu"
src="https://github.com/user-attachments/assets/a8f18d84-4204-46a0-bb31-b73d24b3255f"
/>
## Additional Context
* The OpdsServerStore JSON format and obfuscation scheme are identical
to WifiCredentialStore, so the same JsonSettingsIO infrastructure
handles both.
* The web API uses POST /api/opds/delete instead of DELETE /api/opds
because the ESP32 WebServer doesn't support the DELETE method with a
request body.
* Existing single-server configurations are migrated automatically — no
user action required. After migration the legacy CrossPointSettings
fields are cleared so it only runs once.
* The HttpDownloader changes are backward-compatible: the credential
parameters default to empty strings, so existing callers are unaffected.
---
### 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: Copilot <175728472+Copilot@users.noreply.github.com>
## Summary
Fixes#258. Bookerly is not licensed for use in CrossPoint. Switched to
Google's open Noto Serif font.
---
### 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
* **Goal**: Align `FilesPage` and `SettingsPage` headers with the
existing `HomePage` pattern.
* **Changes**:
- Added `<h1>📚 CrossPoint Reader</h1>` at the top of `<body>` on both
pages.
- Demoted the page-specific `📁 File Manager` heading on `FilesPage` from
`<h1>` to `<h2>` (inside `.page-header`), so every page has a single
`<h1>` — the app name.
- Moved the accent-colored `border-bottom` from `.page-header` to `h1`,
matching the HomePage CSS.
All three pages now share the same top structure:
`<h1>CrossPoint Reader</h1>` → nav links → page content.
## Screenshots
### Home
**Before and after is the same**
<img width="1153" height="388" alt="home before"
src="https://github.com/user-attachments/assets/4eb1c969-89fd-4965-ba17-5ff956576a8b"
/>
### File Manager
**Before**
<img width="1153" height="388" alt="files before"
src="https://github.com/user-attachments/assets/ba689d6b-56b1-44d1-a151-3f67b0fcdb16"
/>
**After**
<img width="1153" height="388" alt="files after"
src="https://github.com/user-attachments/assets/437db5e4-9102-4a19-b049-7a93e4e403b2"
/>
### Settings
**Before**
<img width="1153" height="388" alt="settings before"
src="https://github.com/user-attachments/assets/39a7e489-f0d3-4ac1-a2d7-98028aae65c0"
/>
**After**
<img width="1153" height="388" alt="settings_after"
src="https://github.com/user-attachments/assets/1c479618-ec8b-4dee-b704-3da07e6c279e"
/>
## Additional Context
* Pure markup + CSS, no JavaScript or behavioral changes.
* Improves HTML semantics (single `<h1>` per page, referring to the
app).
* No new CSS duplication introduced — borrows existing styling from
HomePage.
---
### 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: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary
Following up on #1685. One tweaked string confirmed by @alan0ford.
---
### 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
* **What is the goal of this PR?**
* Increase the allowed link length for footnotes
* **What changes are included?**
* Un-magic-numbers parts of the footnote parser
* Increases max href length
## Additional Context
* Additional RAM usage was not noticable to me, worst case 16 * 128 * 2
= 4KB instead of previously 16 * 88 * 2 ~ 2.8KB, see #1031
* Motivation: My book navigated me to the start of the footnote chapter,
which required manual navigating for every footnote due to ungodly file
name choices which pushed the required href length to 72, which caused
truncating, which caused Crosspoint to not find the anchor (Sacred and
Terrible Air, available via reddit)
---
### 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
* This change will make better use of the space available for Title and
Subtitle in the Settings (Theme: Lyra).
Instead of having a fixed maxwidth in pixels for subtitle, it will use
the whole line and shorten the longest one if needed.
So instead of: <img width="480" height="140" alt="image"
src="https://github.com/user-attachments/assets/67e4777b-10d0-4a05-b5c7-fb775776c186"
/>
It will now show: <img width="480" height="142" alt="image"
src="https://github.com/user-attachments/assets/2f143f4e-75d7-45c4-b61a-aad56bdf39fb"
/>
---
### 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
* **Goal**: Make browser tab titles, bookmarks, and history entries
distinguishable across pages (including individual folders in the File
Manager).
* **Changes**:
- Reordered `<title>` so the page name comes first, followed by
`CrossPoint Reader`. Browser tabs usually truncate titles from the
right, and bookmark/history lists are sorted alphabetically by title —
putting the differentiating part first keeps it visible in both cases,
instead of every entry starting with `CrossPoint Reader - …`.
- On `FilesPage`, added a small JS hook that updates `document.title`
with the current subfolder leaf when a `path` query parameter is present
— previously all folders shared the same title.
| URL | Title before | Title after |
|-----|--------------|-------------|
| `/` | `CrossPoint Reader` | `CrossPoint Reader` _(unchanged)_ |
| `/settings` | `CrossPoint Reader - Settings` | `Settings - CrossPoint
Reader` |
| `/files` | `CrossPoint Reader - Files` | `Files - CrossPoint Reader` |
| `/files?path=/Books` | `CrossPoint Reader - Files` | `Books - Files -
CrossPoint Reader` |
| `/files?path=/Books/Fantasy` | `CrossPoint Reader - Files` | `Fantasy
- Files - CrossPoint Reader` |
## Additional Context
* Pure client-side change: `<title>` tags plus ~3 lines of JavaScript on
FilesPage. No network, behavior, or memory impact.
* The static `<title>` tag remains as a fallback if JS fails.
* `HomePage` title left as-is since the app name alone is the standard
convention for a root/home page.
---
### 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
Make this more transparent in Spanish:
`STR_AUTO_TURN_PAGES_PER_MIN: "Auto Turn (Pages Per Minute)"`
A weakness of the current translation is that the verb used is "to
read", but it's not clear that it is the user who will be reading a
certain amount of pages per minute. I've replaced it with the verb for
"to turn (pages)" which is the (analogous) physical action happening
regardless of who executes it, and the abbreviated adverb to clarify
that this is an automatic process. Lastly, a clarification of what the
number means in a simple proportion: "X pages in a minute". The Spanish
is very good overall, this one string is very hard to do in such a short
space. Even more so given that the number cannot be accompanied by any
units, so it must be clear from the text alone what it means.
## Additional Context
N/A
---
### 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
* **Goal**: Fix incorrect pluralization in the file manager web UI
summary line.
* **Changes**: `FilesPage.html` always rendered the plural forms
("folders"/"files") regardless of count. The summary now selects
singular or plural based on each count.
Example:
- Before: `1 folders, 1 files, 12 KB`
- After: `1 folder, 1 file, 12 KB`
## Additional Context
* Cosmetic-only fix — no behavior, performance, or memory impact.
* Change is fully client-side (JavaScript inside a single HTML
template).
* English-only; web UI localization is out of scope here.
---
### 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: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary
* **What is the goal of this PR?**
This PR fixes bugs when using Copyparty as an OPDS server.
https://github.com/9001/copyparty?tab=readme-ov-file#opds-feeds
OPDS uses a query parameter `?opds` to differentiate between HTML
requests and OPDS requests for the same path. It also uses relative
paths in the responses instead of full paths. Crosspoint didn't handle
these two cases.
* **What changes are included?**
Fixes to the two issues above.
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
Here is some example XML from my Copyparty instance:
https://gist.github.com/philips/9ecec29dfb69ed0591b032f16e799675
### 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 | PARTIALLY | NO
>**_
Partially. I used Claude code to write the fix. I am not a strong C++
programmer. But, I manually compiled and tested.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses reviewer feedback from #1644:
- **Localize keyboard hint strings** — 13 hardcoded English strings
replaced with \`tr()\` macro (\`STR_KB_HINT_*\`), making them
translatable across all 22 languages (fallback to English when not yet
translated)
- **Deduplicate \`Lyra3CoversMetrics\`** — now derives from
\`LyraMetrics\` via lambda copy, overriding only \`homeCoverTileHeight\`
and \`homeRecentBooksCount\` (eliminates ~30 duplicated metric fields)
- **Unify keyboard drawing in \`BaseTheme\`** — \`drawTextField\` and
\`drawKeyboardKey\` overrides removed from \`LyraTheme\`; variability
controlled via \`keyboardKeyCornerRadius\` metric (0=Base, 6=Lyra).
Unified text field padding to 6, adopted Lyra's secondary label draw
order (main first, then secondary)
- **Add URL-optimized keyboard layout** — \`urlLayout\` with \`:\` and
\`/\` replacing \`=\` and \`,\` for easier URL input without switching
to SYM mode
---
### 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 **_
Switch KOReader sync progress mapping from chapter matching to
XPath-based mapping.
- resolves KOReader positions using real XHTML ancestry paths
- supports paragraph-based upload mapping with text offsets
where needed
- passes the current paragraph index into sync so uploads map
back to KOReader more accurately
No HTTP client changes are included. No reader-state or resume-flow
changes are included.
---------
Co-authored-by: jpirnay <jens@pirnay.com>
## Summary
Fix typos found via `codespell -S
*.txt,*.yaml,generate_kerning_ligature_epub.py -L
currenty,flate,ser,localy,logicaly,ans,clen,portugues,notin,curren`
## Additional Context
* 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? _**NO**_
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
## Summary
**What is the goal of this PR?**
Reading a book with frequent numbers, I noticed that the spacing between
numeral glyphs was strangely large. This was because Bookerly and Noto
Sans default to tabular figures, where every digit gets an identical
advance width. This is designed for column alignment in spreadsheets,
but in rendering prose it produces visually wide gaps between digits.
This change adds a `--pnum` flag to fontconvert.py that applies the
font's OpenType `pnum` (proportional numerals) feature during
conversion. When active, the converter:
- Parses the GSUB table for pnum SingleSubst lookups
- Resolves substitute glyph indices via fonttools' glyph order
- Loads the proportional alternate glyphs instead of the tabular
defaults
- Includes substitute glyph names in kern pair extraction, so kerning
data that references proportional alternates is captured
Bookerly's proportional alternates also carry digit-digit and
digit-punctuation kerning that the tabular glyphs lack (e.g., at 16pt
7->4 at -1.69px, 7->. at -2.31px, 7->1 at +1.00px).
Noto Sans gains proportional advances but no new kerning (its
proportional glyphs have no kern class data in the font).
OpenDyslexic is unaffected. Its `cmap` already points to proportional
glyphs, so `--pnum` is a no-op. `--pnum` is intentionally omitted from
OpenDyslexic in the build script for deliberately uniform digit spacing
as an accessibility choice.
UI fonts (Ubuntu, notosans_8) also omit `--pnum` to preserve tabular
alignment for page numbers, battery percentages, etc.
| Before | After |
| -- | -- |
| <img
src="https://github.com/user-attachments/files/26042238/screenshot-31673.bmp"
width="300" /> | <img
src="https://github.com/user-attachments/files/26042241/screenshot-124075.bmp"
width="300" /> |
---
### 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
Improved Italian translations provided by @alan0ford, closes#1578.
---
### 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**_
# Refactor: Redesign On-Screen Keyboard
## Summary
Complete redesign of the on-screen keyboard (used for WiFi password,
KOReader, Calibre URLs) with improved layout, navigation, visual style,
and new input features: **cursor mode** for text navigation, **password
mode** with visibility toggle, and **URL mode** with pre-defined
snippets.
## Screenshots
### Base Theme
|**master** | **PR #1644** |
|----------|-------------|
| <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/49125857-12d0-4020-b872-05d0ddbf1d94"
/> | <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/ad16656b-d66e-43dd-8697-2b85f709d7f8"
/> |
### Lyra Theme
| **master** | **PR #1644** |
|----------|-------------|
| <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/d9901251-9154-48d2-83b4-376d3223d132"
/> | <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/84b45949-ed61-4924-af1b-d570c4c61e13"
/> |
### Keyboard States
| ABC Mode | Symbol Mode | URL Mode |
|----------|-------------|----------|
| <img width="480" height="280" alt="image"
src="https://github.com/user-attachments/assets/6397a82e-50b8-4d03-92e0-f707ed7c9054"
/> | <img width="480" height="280" alt="image"
src="https://github.com/user-attachments/assets/205d34fe-0413-49e9-9db2-30569c297ba0"
/> | <img width="480" height="280" alt="image"
src="https://github.com/user-attachments/assets/801ddeab-e082-4a22-a9df-c66adb1afc16"
/> |
| Cursor Mode | Password Toggle |
|-------------|-----------------|
| <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/841773e1-7a3c-45e5-aa89-e5787255608c"
/> | <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/9487a0b3-3f47-41ed-9cd3-b24e56e342a8"
/> |
## Changes
### Layout (10-column uniform grid)
- Reduced from 13/11/10 columns per row to **10 uniform columns** across
all rows
- Keyboard now uses **90% of screen width** (was ~66%)
- Row 0: Numbers `1-9, 0` with secondary symbols (`!@#$%^&*()`)
- Rows 1-3: Standard QWERTY letters
- Bottom row: `shift` | `#@!` | `___` | `←` | `OK`
### New Symbol Mode (#@!)
- New mode toggle key `#@!` / `abc` switches between letter and symbol
layouts
- Symbol layout: 4 rows (numbers, inverted symbols, paired symbols,
loose symbols)
- Covers all 95 printable ASCII characters
- **No secondary hints, no long-press** in symbol mode (simple and
direct)
- SHIFT key remains visible but **disabled** in symbol mode
### URL Mode
- In `InputType::Url`, the Space key becomes a **URL toggle** button
- Activating URL mode replaces the 4 content rows with a **3×3 grid of
URL snippets**:
- Col 0 (protocols): `https://`, `http://`, `/opds`
- Col 1 (hosts/ports): `www.`, `192.168.`, `:8080`
- Col 2 (domains): `.com`, `.org`, `.net`
- Snippets are inserted as full strings at the cursor position
- URL mode **persists** after inserting a snippet (does not
auto-deactivate)
- Column alignment: col 0 over ABC, col 1 over URL, col 2 over Del
- Up/Down navigation maps `bottomCol - 1` / `urlCol + 1`
- SHIFT disabled in URL mode
- SpecMode (`abc`) exits URL mode back to ABC
- SpecSpace (`URL`) toggles URL mode on/off; selection always stays on
the URL button
- Button styled with `KeyboardKeyType::Mode` for consistent outline
### Cursor Mode
- **Enter**: Long-press Up (500ms) while in keyboard mode
- **Exit**: Short-press Down while in cursor mode (resets
`passwordVisible`, clears toggle position)
- **Navigate**: Left/Right move cursor position within text (one
position per press, no continuous repeat)
- **Visual**:
- Keyboard mode: underline cursor (2px line + serifs)
- Cursor mode: inverted block cursor (black fill + white character)
- Block width adapts to the actual character width under cursor (minimum
6px for narrow chars like space)
- Block position includes inter-character kerning offset for correct
alignment (calculated via string-difference: `getTextWidth(before+char)
- getTextWidth(before) - getTextWidth(char)`)
- End-of-text: thin 6px block
- Password hidden: 3-part drawing (Part 1 + block + Part 3) to prevent
block overflow onto `*` characters
- Toggle position: caret ("I") cursor at saved position, `[abc]`/`[***]`
label with inverted selection
- **Inactive key styling**:
- BaseTheme: 2px outline rectangle
- LyraTheme: gray filled rounded rectangle (`Color::LightGray`)
- **Password toggle position**: in cursor mode (Password only), Hold
Right (500ms) enters toggle — caret cursor shows saved position,
`[abc]`/`[***]` label becomes selected. Press Confirm to toggle
`passwordVisible`. Press Left to restore cursor to saved position. Right
from toggle is a no-op. Down from toggle exits to keyboard.
- `cursorPos` persists between keyboard and cursor modes
### Password Mode
- `InputType::Password` enum replaces `bool isPassword` parameter
- Text is masked with `*` except for one revealed character:
- Keyboard mode: reveals character at `cursorPos - 1`
- Cursor mode: no reveal in display text (block cursor draws actual char
directly)
- **Toggle `[abc]`/`[***]`**: accessible via cursor mode — Hold Right
(500ms) enters toggle position, Confirm toggles visibility, Left exits
back to cursor. Caret ("I") shown at saved position while in toggle.
- `passwordVisible` resets to `false` when exiting cursor mode
- **Long-press Del (1.5s)**: clears all text and resets cursor to 0
### InputType Enum
- Replaced `bool isPassword` constructor parameter with `enum class
InputType { Text, Password, Url }`
- Callers updated: `WifiSelectionActivity`, `KOReaderSettingsActivity`,
`CalibreSettingsActivity`
### Contextual Tips
- `"Tips:"` header followed by context-sensitive hints, centered between
text field underline and keyboard as a block
- ABC mode: `"Hold SELECT for UPPERCASE or secondary char"` (shift ON:
`"lowercase"` variant) + `"Hold DEL to clear all text"` (only if text
not empty)
- ABC + `InputType::Url`: same + `"Press URL for snippets"`
- Symbol mode: `"Hold DEL to clear all text"` (only if text not empty)
- URL mode: `"Press ABC to exit URL mode"` + `"Hold DEL to clear all
text"` (only if text not empty)
- Cursor mode: `"Press DOWN to return to keyboard"`
### Hint Phases (cursor mode, Password only)
- **Phase 1**: `"Hold UP to edit entry"` — shown after 2× DEL press,
auto-hides after 4s, positioned below underline
- **Phase 2**: `"Press < or > to move cursor"` + dynamic password toggle
hint — shown when entering cursor mode, positioned below underline,
visible until exit
- When `!passwordVisible`: `"Hold > then press [abc] to show password"`
- When `passwordVisible`: `"Hold > then press [***] to hide password"`
- When in toggle position: `"Press < to return to cursor position"`
### Long-Press Alternative Character
- Holding Confirm (>500ms) inserts the **alternative character** instead
of the primary
- Letters: long-press inserts opposite case (e.g., `a`→`A`, `A`→`a`)
- Numbers/symbols (row 0): long-press inserts secondary (e.g., `0`→`)`,
`)`→`0`)
- Only active in ABC mode; disabled in Symbol mode and URL mode
- **`InputType::Url`**: Hold SELECT on ABC rows 1+ (letters) returns
primary character only (same as short press). Row 0 (symbols) still
returns secondary character on Hold SELECT.
### Shift (2 sticky states)
- Reduced from 3 states (shift/SHIFT/LOCK) to **2 sticky states**
(shift/SHIFT)
- Shift stays active after typing until manually toggled off
- Label: `shift` (off) / `SHIFT` (on)
### SpecialKeyType Enum
- `enum class SpecialKeyType { Shift, Mode, Space, Del, Ok }` replaces
plain `enum` (`SpecShift`, `SpecMode`, etc.) for type safety
- All switch cases updated to `SpecialKeyType::*` with
`static_cast<int>()` for array indexing
- `onExit()` reverted to simple `Activity::onExit()` call (half-refresh
removed)
- **Bottom row column mapping**: navigating up/down between content rows
and bottom row uses `col/2` and `col*2` formulas for consistent
positioning (10 cols ↔ 5 cols)
- **URL mode column mapping**: `bottomCol - 1` / `urlCol + 1` (3 cols ↔
5 cols)
- **Wrap-around**: row 0 → up → bottom row and bottom row → down → row 0
both apply correct column mapping
### Visual Improvements (both Base and Lyra themes)
- **Space key**: underscore-style horizontal line (60% of key width, 3px
thick)
- **Delete key**: arrow `←` drawn with lines (3px thick) instead of
"DEL" text
- **Secondary label** (ABC row 0): small hint in top-right corner with
separation from primary number
- **BaseTheme**: selection uses **inverted fill** (black rect + white
text) instead of `[bracket]` markers
- **BaseTheme**: text field brackets drawn as **stretchable lines** that
adapt to multi-line input (1px normal, 3px cursor mode)
- **LyraTheme**: text field uses **fixed-width underline** (16px
margins, 8px each side) instead of stretchable line (2px normal, 3px
cursor mode)
- **Both themes**: special keys (shift, mode, space, del, OK) have
bordered/bordered-rounded rectangles
- **Font size**: keyboard uses `UI_12_FONT_ID` in both themes (was
`UI_10` in Base)
- **Key height**: 40px in all themes for better proportions
- **Layout unification**: text and password toggle are left-aligned in
all themes (`keyboardCenteredText = false` for Lyra/Lyra3Covers)
- **`primaryOffset` removed**: dead code eliminated from BaseTheme and
LyraTheme `drawKeyboardKey`
### New Theme Metrics
- `keyboardVerticalOffset`: per-theme vertical adjustment of keyboard
position
- Base: `-13`, Lyra: `-7`
- `keyboardBottomKeySpacing`: independent spacing for bottom row keys
- Base: `5`, Lyra: `5`
- Bottom-aligned keyboard in both themes for consistent vertical
positioning
- Bottom row total width calculated to match content rows width (10-col
based, consistent across modes)
- 4px extra gap between content rows and bottom row when `bkSpacing > 0`
- `keyboardCenteredText`: `false` for all themes (unified left-aligned
text)
### Defensive Improvements
- **State reset on re-entry**: `onEnter()` resets all mutable state
(`symMode`, `urlMode`, `cursorMode`, `togglePos`, `passwordVisible`,
`shiftState`, `selectedRow`, `selectedCol`, `rightHeld`,
`rightLongHandled`, `savedCursorPos`, `rightStartCursorPos`,
`delPressCount`, `hintVisible`, `hintShowTime`) — prevents stale state
when re-entering the keyboard
- **Bounds checking**: `insertChar`/`insertString` clamp `cursorPos` to
`text.length()` before inserting
- **Empty string guard**: `insertString` returns early on empty string
- **`std::string::npos`**: used instead of `SIZE_MAX` for size_t
sentinel (proper C++ idiom)
- **`<algorithm>` header**: included for `std::max`
## Files Modified
| File | Changes |
|------|---------|
| `src/activities/util/KeyboardEntryActivity.h` | `InputType` enum,
`KeyDef` struct, 10-col layouts, cursor/password/URL/toggle state, hints
(`delPressCount`, `hintVisible`, `hintShowTime`), held vars
(`rightHeld`, `rightLongHandled`, `savedCursorPos`,
`rightStartCursorPos`), `mapColContentBottom` helper |
| `src/activities/util/KeyboardEntryActivity.cpp` | Complete rewrite:
layout rendering, symbol/cursor/password/URL modes, toggle position,
long-press, contextual tips, hint phases, block cursor kerning
alignment, defensive bounds checks, state reset |
| `src/components/themes/BaseTheme.h` | `KeyboardKeyType` enum, new
`drawTextField`/`drawKeyboardKey` signatures, `keyboardVerticalOffset`,
`keyboardBottomKeySpacing` metrics |
| `src/components/themes/BaseTheme.cpp` | Redesigned `drawTextField`
(stretchable brackets), `drawKeyboardKey` (inverted selection,
space/delete graphics, secondary label, inactive selection), removed
`primaryOffset` dead code |
| `src/components/themes/lyra/LyraTheme.h` | Override signatures,
`keyboardVerticalOffset`, `keyboardBottomKeySpacing`,
`keyboardKeyHeight` adjustments |
| `src/components/themes/lyra/LyraTheme.cpp` | `drawTextField` (fixed
underline), `drawKeyboardKey` (rounded rects for special keys,
space/delete graphics, secondary label, inactive selection), removed
`primaryOffset` dead code |
| `src/components/themes/lyra/Lyra3CoversTheme.h` |
`keyboardCenteredText = false`, `keyboardVerticalOffset = -7`, inherits
Lyra overrides |
| `src/activities/network/WifiSelectionActivity.cpp` | `bool isPassword`
→ `InputType::Password` |
| `src/activities/settings/KOReaderSettingsActivity.cpp` | `bool
isPassword` → `InputType::Text`/`InputType::Password`/`InputType::Url` |
| `src/activities/settings/CalibreSettingsActivity.cpp` | `bool
isPassword` → `InputType::Text`/`InputType::Password`/`InputType::Url` |
## Backward Compatibility
- **API change**: Constructor parameter changed from `bool isPassword`
to `InputType inputType` (default `InputType::Text`)
- **All callers updated**: WiFi, KOReader, and Calibre integrations
migrated to new `InputType` enum
## Testing
### Input & Text Handling
- [x] Empty input → press OK (submit empty string)
- [x] Back button → cancel (no text returned)
- [x] Pre-filled initial text (e.g., editing existing WiFi password)
- [x] Password mode: text masked with `*` characters, one character
revealed
- [x] Delete on empty text (no crash)
- [x] Very long text near maxLength limit
- [x] URL with path and port (~60 chars)
- [x] Multi-line text wrapping in input field
- [x] Space insert in middle of text (cursor mode)
- [x] Delete last character repeatedly
- [ ] Type all 95 printable ASCII characters
### Mode Switching
- [x] ABC → #@! preserves typed text and cursor position
- [x] #@! → ABC preserves typed text and cursor position
- [x] Shift state preserved when switching modes
- [x] Switch modes multiple times rapidly
### Shift Behavior
- [x] Shift OFF → type letter → inserts lowercase, shift stays OFF
- [x] Shift ON → type letter → inserts uppercase, shift stays ON
- [x] Shift ON → type number → inserts symbol, shift stays ON
- [x] Shift ON → navigate rows → shift stays ON
- [x] Shift ON → switch to #@! → shift shows "shift" (disabled)
- [x] Shift ON → switch to ABC → shift state preserved
- [x] Shift ON → switch to URL → shift shows "shift" (disabled)
- [x] Shift disabled in URL mode: pressing shift does nothing
### Long-Press
- [x] Long-press letter with shift OFF → inserts uppercase
- [x] Long-press letter with shift ON → inserts lowercase
- [x] Long-press number → inserts secondary symbol
- [x] Long-press symbol (row 0) → inserts opposite (number)
- [x] Long-press key without secondary (e.g., `-`, `=` in rows 2-3) →
inserts primary character on release
- [x] Long-press on special keys (shift, mode, space, del, ok) → no
alternative inserted
- [x] Long-press in #@! mode → no effect (disabled)
- [x] Long-press in URL mode → no effect (disabled)
- [x] Long-press number in row 0 with InputType::Url → inserts secondary
symbol (same as non-URL)
- [x] Short press after cancelled long-press → normal behavior
- [x] Long-press at maxLength → no character inserted
- [x] Long-press Del (1.5s) → clears all text
### Cursor Mode
- [x] Long-press Up → enters cursor mode
- [x] Short-press Down → exits cursor mode (resets passwordVisible)
- [x] Left/Right navigate within text
- [x] Left at position 0 → no movement
- [x] Right at end of text → no movement in Text mode, enters toggle in
Password mode (Hold Right)
- [x] Block cursor visual: correct width for character, thin block at
end
- [x] Underline cursor visual (keyboard mode): correct position with
serifs
- [x] Inactive key styling: outline (Base) or gray fill (Lyra) on
selected key
- [x] Typing with cursor mid-text → inserts at cursor position
- [x] Deleting with cursor mid-text → deletes character before cursor
- [x] Exit cursor mode → type at cursor position (inserts mid-text, not
at end)
- [x] Exit cursor mode from toggle → cursor at saved position (not end
of text)
### Password Mode
- [x] Masked text with one revealed character at `cursorPos - 1`
- [x] Cursor mode: block shows actual character, display text all `*`
- [x] Toggle `[abc]`/`[***]`: Hold Right (500ms) in cursor mode enters
toggle, Confirm toggles visibility, Left exits back to cursor
- [x] Exiting cursor mode resets `passwordVisible` to false
- [x] Long-press Del clears all text
### URL Mode
- [x] URL toggle activates/deactivates URL mode
- [x] URL button stays selected after toggle (both on and off)
- [x] Deactivating URL mode returns to ABC (not SYM)
- [x] 3×3 snippet grid displays correctly
- [x] Column alignment: col 0 over ABC, col 1 over URL, col 2 over Del
- [x] Snippet insertion: inserts full string at cursor position
- [x] URL mode persists after snippet insertion
- [x] Shift disabled in InputType::Url
- [x] SpecMode (`abc`) exits URL mode to ABC
- [x] Up/Down navigation between URL grid and bottom row
### Re-entry State Reset
- [x] Enter keyboard → activate URL mode → exit → re-enter → URL mode
OFF
- [x] Enter keyboard → switch to SYM → exit → re-enter → ABC mode
- [x] Enter keyboard → enter cursor mode → exit → re-enter → keyboard
mode
- [x] Enter keyboard → enter toggle pos → exit → re-enter → togglePos
OFF
- [x] Enter keyboard → activate shift → exit → re-enter → shift OFF
- [x] Enter password keyboard → toggle password visible → exit →
re-enter → password hidden
### Navigation
- [x] Left/right wrap-around within content rows
- [x] Left/right wrap-around within bottom row
- [x] Up from row 0 → bottom row (correct column mapping)
- [x] Down from bottom row → row 0 (correct column mapping)
- [x] Up from bottom row → last content row (correct column)
- [x] Down from last content row → bottom row (correct column)
- [x] Navigate horizontally in bottom row, then up → correct content
column
- [x] Navigate horizontally in bottom row, then down (wrap) → correct
content column
### Visual (both themes)
- [x] Secondary hints only on ABC row 0
- [x] No secondary hints in #@! mode or URL mode
- [x] No secondary hints on letter rows (1-3)
- [x] Space bar: horizontal line centered, not touching edges
- [x] Delete: arrow `←` drawn correctly
- [x] Selected key: inverted colors (black fill, white text)
- [x] All special keys have border rectangles
- [x] Fixed underline in text field (Both themes)
- [x] Mode key label: `#@!` in ABC mode, `abc` in symbol mode, `abc` in
URL mode
- [x] URL key label: `URL` (only in InputType::Url), styled same as
other bottom keys
- [x] Shift label: `shift` when OFF, `SHIFT` when ON, `shift` when
disabled (SYM/URL)
- [x] Both themes: bottom row total width matches content rows width
- [x] URL snippet grid centered over ABC/URL/Del buttons
### Device & Theme Coverage
- [ ] Base Theme on X3
- [x] Base Theme on X4
- [ ] Lyra Theme on X3
- [x] Lyra Theme on X4
- [ ] Lyra Extended Theme on X3
- [x] Lyra Extended Theme on X4
### Toggle Position
- [x] Hold Right > 500ms in cursor mode (Password) → enters toggle,
caret visible at saved position
- [x] Short-press Right in cursor mode (Password) → advances cursor 1
position, does not jump to toggle
- [x] Short-press Left in cursor mode (Password) → moves cursor left 1
position, from toggle returns to saved position
- [x] Confirm in toggle → toggles `passwordVisible`
- [x] Left from toggle → returns to saved position, caret disappears,
block cursor appears
- [x] Right from toggle → no-op
- [x] Down from toggle → exits to keyboard, cursor at saved position
- [x] Hold Right in cursor mode (InputType::Text) → no effect
- [x] Hold Right in cursor mode (InputType::Url) → no effect
- [x] Hold Right < 500ms released in cursor mode (Password) → short
press, advances cursor 1
- [x] No continuous repeat when holding Left or Right in cursor mode
### Caret Visual in Toggle
- [x] In toggle: caret "I" visible at saved cursor position
- [x] Character under cursor visible (no gap) in password not-visible
mode
- [x] Character under cursor visible in password visible mode
- [x] `[abc]`/`[***]` label with inverted selection in toggle
### Contextual Tips
- [x] `"Tips:"` header centered above contextual hints
- [x] Single tip → `"Tips:"` + one line
- [x] Multiple tips → `"Tips:"` + multiple lines, all centered as block
- [x] No tips shown when not applicable (e.g., ABC with empty text and
non-URL)
- [x] `"UPPERCASE"` shown when shift OFF
- [x] `"lowercase"` shown when shift ON
- [x] `"secondary char"` shown for InputType::Url
### Hint Phases
- [x] 2× DEL → Phase 1 appears ("Hold UP to edit entry")
- [x] Phase 1 auto-hides after 4s
- [x] Phase 2 appears when entering cursor mode ("Press < or > to move
cursor")
- [x] Phase 2 shows "Hold > then press [abc] to show password" when
`!passwordVisible`
- [x] Phase 2 shows "Hold > then press [***] to hide password" when
`passwordVisible`
- [x] Phase 2 shows "Press < to return to cursor position" when in
toggle
- [x] Phase 2 disappears when exiting cursor mode
### Long-Press `InputType::Url` Behavior
- [x] Hold SELECT on letter rows (rows 1+) with InputType::Url → same
character as short press
- [x] Hold SELECT on row 0 with InputType::Url → secondary character
works normally
### Number Row Reorder
- [x] Number row order: 1-9, 0 left to right
- [x] `(` and `)` are adjacent (positions 8 and 9) via secondary labels
- [x] Long-press on row 0 returns correct secondary symbols in new order
- [x] SYM row 1: `(` and `)` also adjacent (positions 8 and 9)
### Block Cursor Alignment
- [x] Block cursor correctly positioned for consecutive spaces (kerning
offset applied)
- [x] Block cursor correctly positioned for mixed characters (letters,
numbers, symbols)
- [x] Block width minimum 6px for narrow characters (space) — visible as
block, not thin line
- [x] Password hidden: 3-part drawing prevents block overflow onto `*`
characters
- [x] Password visible: block post-loop draws correctly on continuous
text (no 3-part needed)
- [x] End-of-text block: thin 6px block at correct position
### Integration
- [x] WiFi password entry (connect to network)
- [ ] KOReader username, password, and sync server URL
- [ ] Calibre OPDS URL, username, and password
- [ ] Calibre OPDS URL: empty → opens with "https://" prefilled
- [ ] Calibre OPDS URL: type "http://" or "https://" only → saved as
empty
- [ ] Calibre OPDS URL: type full URL → saved correctly
- [ ] Calibre OPDS URL: existing URL → opens with existing URL (not
"https://" prefill)
Opening XTC files with a high page count (e.g. *The Magic Mountain* at
4,187 pages) causes an immediate `abort()` crash and reboot loop. The
device becomes unusable until the book is removed from the SD card.
**Crash log:**
```
abort() was called at 0x4214a5fb on core 0
```
### Root cause
During `XtcParser::open()`, the parser calls
`m_pageTable.resize(pageCount)` to load the entire page table into RAM.
Each `PageInfo` entry is 16 bytes, so:
- 4,187 pages x 16 bytes = **66,992 bytes (~65KB)** as a single
contiguous heap allocation
On the ESP32-C3 with ~380KB total RAM (no PSRAM), this allocation fails
after firmware, fonts, and the activity system are already loaded.
Because the firmware is compiled with `-fno-exceptions`, the failed
`new` inside `std::vector::resize()` calls `abort()` instead of
throwing.
This affects any XTC file with roughly 3,000+ pages, depending on heap
state at the time of loading.
## Solution
Replace the bulk page table allocation with on-demand reads from the SD
card. Instead of loading all page table entries into a vector at file
open, we now:
1. Read only the **first** page table entry at open time (to get default
page dimensions)
2. Read a **single** 16-byte entry from the SD card each time a page is
loaded
This reduces page table memory usage from `pageCount * 16` bytes to
**zero bytes**, regardless of how many pages the file contains.
### Changes
| File | What changed |
|------|-------------|
| `XtcParser.h` | Removed `std::vector<PageInfo> m_pageTable`. Added
`readPageTableEntry()` for on-demand reads. |
| `XtcParser.cpp` | Replaced `readPageTable()` with
`readFirstPageInfo()`. Updated `getPageInfo()`, `loadPage()`, and
`loadPageStreaming()` to seek and read individual entries from the file.
|
## Trade-offs
### Performance
Each page turn now requires one additional SD card seek + 16-byte read
to look up the page table entry before reading the page data itself.
- SD card sequential read latency: ~0.1-0.5ms for a 16-byte read
- E-ink full display refresh: ~1,000-2,000ms
I personally can't see any performance difference while reading and the
trade off of not boot looping seems to make this well worth it.
### Memory
| Metric | Before | After |
|--------|--------|-------|
| Page table RAM (4,187 pages) | ~65KB | 0 bytes |
| Page table RAM (1,000 pages) | ~16KB | 0 bytes |
| Page table RAM (max 65,535 pages) | ~1MB (impossible) | 0 bytes |
## Summary
Clarify that X3 device has a reliable RTC chip, so clock feature can be
in-scope depending on device.
---
### 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
Updating the docs so it has the accurate dimensions for sleep screens on
the X3 and the X4.
---
### 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
* **What is the goal of this PR?**
Make adaptation instead of pure translation.
* **What changes are included?**
Fix issues where text could not fit in line.
Fix didn't cover `KOReader` settings
**Additional Reviever**
@mirus-ua
---
### 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: Kym_Adnriy <kym_andr@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
## Summary
**What is the goal of this PR?**
Added `destroyXmlParser()` helper to replace the repeated 4-line parser
cleanup block (stop, clear callbacks, free, null) that was copyied
across 6 XML parser files.
---
### 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**_
## Problem
Custom sleep wallpapers feel repetitive — the same image appearing
multiple times in a short session. Only the single most-recently-shown
index was stored (`uint8_t lastSleepImage`), so on collections of 3–5
images, two-pick cycles were common. On larger collections, any image
could reappear within the next few picks.
## Solution
Add a 16-entry circular recency buffer to `CrossPointState` that
excludes recently shown wallpapers from selection.
**Recency buffer** (`recentSleepImages[16]`, `recentSleepPos`,
`recentSleepFill` — 34 bytes DRAM):
- Tracks the last 16 shown wallpaper indices
- On each pick, rerolls up to 20 times if the candidate was recently
shown
- Window auto-shrinks to `numFiles - 1` for small collections
(guarantees a non-repeat is always possible)
- `isRecentSleep()` clamps to `recentSleepFill` to avoid false positives
on unwritten buffer slots
- State persisted to `state.json` so the buffer survives sleep/wake
cycles
**Migration**:
- Binary (`state.bin`): old `lastSleepImage` field seeded into the new
buffer if valid
- JSON (`state.json`): legacy `lastSleepImage` key detected and seeded
into the buffer when upgrading from older firmware
## Index type fix
`randomFileIndex` upgraded from `uint8_t` to `uint16_t` — silently
truncated for collections larger than 255 wallpapers.
## Repeat probability: before vs after
Chance of seeing a recently-shown image on the next pick. After:
`(numFiles - 16) / numFiles` once collection exceeds the window; 0%
while ≤17.
| Collection | Before | After |
|---|---|---|
| 3 | 50% | 0% |
| 5 | 75% | 0% |
| 10 | 89% | 0% |
| 17 | 94% | 0% |
| 18 | 94% | 6% |
| 20 | 95% | 20% |
| 25 | 96% | 36% |
| 30 | 97% | 47% |
| 50 | 98% | 68% |
| 100 | 99% | 84% |
## Memory impact
| Addition | Size |
|---|---|
| `recentSleepImages[16]` | 32 bytes DRAM |
| `recentSleepPos` + `recentSleepFill` | 2 bytes DRAM |
| **Total** | **34 bytes DRAM** |
## Files changed
- `src/CrossPointState.h` — recency buffer fields + `isRecentSleep()` /
`pushRecentSleep()` declarations
- `src/CrossPointState.cpp` — `isRecentSleep()` / `pushRecentSleep()`
implementations + binary migration path
- `src/JsonSettingsIO.cpp` — JSON serialisation of buffer state + JSON
migration
- `src/activities/boot_sleep/SleepActivity.cpp` — retry loop with
recency check
Co-authored-by: Patryk Radtke <patryk@Patryks-MacBook-Pro.local>
## Summary
* **What is the goal of this PR?** Replace the o(r^2) routines with a
o(r) scanline logic - will make fillArc roughly 50% faster and drawArc
roughly 5x faster. Still probably unnoticeable.
* **What changes are included?**
## Additional Context
---
### 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
Battery percentage is calculated from the voltage, which is not totally
stable. We smooth the battery percentage using a moving average.
## Additional Context
issue discussion:
https://github.com/crosspoint-reader/crosspoint-reader/issues/1444
---
### 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
- Removes the vendored `picojpeg` library and rewrites
`JpegToBmpConverter` to use the already-present `JPEGDEC` (bitbank2)
dependency
- Eliminates the redundancy of having two JPEG decoders in the firmware
- All BMP output (headers, fixed-point scaling, Atkinson/Floyd-Steinberg
dithering) is identical to before — cached cover BMPs are unaffected
## Size impact
| | Before | After | Delta |
|---|---|---|---|
| Flash | 5,754,089 bytes (87.8%) | 5,744,777 bytes (87.7%) | **−9,312
bytes** |
| RAM | 95,212 bytes (29.1%) | 92,852 bytes (28.3%) | **−2,360 bytes** |
## Implementation notes
- `bmpDrawCallback` receives MCU-sized blocks from JPEGDEC (up to 16
rows × MCU-width), accumulates them into a pre-allocated `mcuBuf`, and
applies the same scaling + dithering logic once each MCU row is complete
- File I/O uses a file-scope static `FsFile*` (safe in single-threaded
embedded context) via JPEGDEC's open/read/seek callbacks — same pattern
as `JpegToFramebufferConverter`
- Added a 52 KB free-heap guard before allocating the JPEGDEC object
(~17 KB)
- `lib/picojpeg/` deleted (2,087 lines of C removed)
## Test plan
- [ ] Build compiles without warnings
- [ ] Cover art BMP cache regenerates correctly for EPUB books
- [ ] Home screen thumbnails (1-bit BMP path) render correctly
- [ ] Custom-size thumbnails (`jpegFileToBmpStreamWithSize`) render
correctly
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
Previouls where ALL whitespace (and square brackes) removed from the
footnote link text, however some link texts are multiworded, like "`turn
to 252`" which were truncated into "`turnto252`", (an example from the
first book "Flight from the Dark" of the Lone Wolf book series by Joe
Dever, see [link](https://www.projectaon.org/en/Main/FlightFromTheDark))
* This change will only remove whitespaces from the beginning and end of
the string
so "` [ 12 ] `" will become "`12`" just like before, and "` turn to 252
`" will become "`turn to 252`".
---
### 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
* Add missing swedish translations
---
### 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
* **What is the goal of this PR?** After displaying an image via the
filebrowser ghosting artifacts remained on the screen
* **What changes are included?**
## Additional Context
* 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? _**< NO >**_
## Summary
* **What is the goal of this PR?** Dont format files in .venv directory
* **What changes are included?**
## Additional Context
* 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? _**< YES | PARTIALLY | NO
>**_
## Summary
Make popups (like "Going to sleep") respect the current screen
orientation when shown from reader activities.
**What changes are included?**
- Apply reader orientation in SleepActivity before showing popup when
`lastSleepFromReader` is true
- Make popup Y-position proportional to screen height (7.5% for
BaseTheme, 16.5% for LyraTheme) instead of hardcoded pixel values,
ensuring correct positioning in both portrait and landscape modes.
- Add `isReaderActivity()` override to all reader sub-screens (menu,
chapter selection, percent selection, footnotes, QR display, KOReader
sync), so sleep popups rotate correctly when entering sleep from any
reader context.
## Additional Context
<img
src="https://github.com/user-attachments/assets/47d88c2c-ffc5-41a7-b3f2-af272ea0150e"
width="400" height="240">
---
### 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**_ (Claude Opus
4.5)
## Summary
**What is the goal of this PR?**
Following up on #1427:
- Extracted shared battery drawing logic, including lightning bolt, to
reduce duplication.
- In Lyra with segmented battery the lightning bolt was hard to see, so
when charging Lyra now uses a solid battery fill.
---
### 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?** (e.g., Implements the new feature for
file uploading.)
`DESTRUCTOR_CLOSES_FILE=1` is set in platformio.ini, which makes SdFat's
FsBaseFile destructor call close() automatically when a file goes out of
scope.
Three categories of file close calls remain untouched:
1. Close before Storage.remove() on the same path: ScreenshotUtil.cpp
closes the file before deleting it on write error. The remove might fail
if the file is still open.
2. Close before reopening the same variable: Epub.cpp writes a temp
NCX/nav file, closes it, then reopens it for reading. The
RecentBooksStore.cpp close before saveToFile() is the same pattern, it
rewrites the same file.
3. Close on member variables: BookMetadataCache.cpp (bookFile,
spineFile, tocFile), Section.cpp (file), XtcParser.cpp (m_file),
ZipFile.cpp (file). These persist beyond any single function scope, so
the destructor timing doesn't match the intended close point.
---
### 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?** (e.g., Implements the new feature for
file uploading.)
Combining diacritical marks (U+0300–U+036F) were positioned using a
heuristic that centered them at the midpoint of the base glyph's
**advance width**. This worked acceptably for Bookerly but produced
visibly off-center marks for Noto Sans due to a fundamental difference
in how the two fonts design their combining mark metrics.
Builds on the work of #1037.
## The problem
The two built-in body fonts encode combining mark `left` offsets with
very different conventions:
| Mark | Bookerly `left` | Noto Sans `left` |
|---|---|---|
| U+0301 (acute) | -2 | -10 |
| U+0300 (grave) | -5 | -15 |
| U+0302 (circumflex) | -5 | -5 |
| U+0323 (dot below) | -2 | -11 |
Noto Sans uses large negative `left` values because its marks are
designed for placement at the post-advance cursor position, with `left`
pulling the bitmap back over the base glyph. Bookerly uses small offsets
because its marks sit closer to the glyph origin. The old `advance/2`
centering split the difference poorly — it happened to land close to
correct for Bookerly but placed Noto Sans marks roughly 6px left of
center on a typical lowercase letter.
There was also a bug in the vertical gap heuristic. It unconditionally
computed a `raiseBy` value to prevent above-baseline marks from
colliding with tall base glyphs, but it applied the same logic to
**below-baseline** marks like cedilla (U+0327), dot below (U+0323), and
ogonek (U+0328). For those marks, the math produced a large positive
raise (e.g., 24px for dot-below on 'a'), launching them above the
x-height instead of keeping them below the baseline.
## The fix
**Horizontal positioning**: Instead of centering at `advance/2`, align
the mark bitmap's visual midpoint directly over the base glyph bitmap's
visual midpoint. This uses the base glyph's actual `left` and `width`
rather than its advance width, producing correct results regardless of
how the font encodes its mark offsets.
**Vertical positioning**: The raise heuristic now checks `markTop -
markHeight > 0` and skips below-baseline marks entirely, leaving them at
their font-designed position.
**Consolidation**: The shared math is extracted into two `constexpr`
helpers (`combiningMark::centerOver` and
`combiningMark::raiseAboveBase`) in `EpdFontData.h`, eliminating the
previously triplicated inline calculations across `drawText`,
`drawTextRotated90CW`, and `getTextBounds`. The `MIN_COMBINING_GAP_PX`
constant is also centralized as `combiningMark::MIN_GAP_PX`.
| Before | After |
| -- | -- |
| <img
src="https://github.com/user-attachments/files/25752257/before-noto.bmp"
width="250" /> | <img
src="https://github.com/user-attachments/files/25752258/after-noto.bmp"
width="250" /> |
| <img
src="https://github.com/user-attachments/files/25752259/before-bookerly.bmp"
width="250" /> | <img
src="https://github.com/user-attachments/files/25752260/after-bookerly.bmp"
width="250" /> |
---
### 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 to analyze
differences between Noto Sans and Bookerly font metrics**_
---------
Co-authored-by: Uri Tauber <142022451+Uri-Tauber@users.noreply.github.com>
## Summary
If the system reboots from crash, display the reason and tell user to
include `crash_report.txt` file.
To test this, simply add an `assert(false)` somewhere inside the code.

---
### 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
Adds a full path display at the bottom of the file browser with a
separator line matching the header style. Path uses the small font,
left-truncates to always show the deepest folder when path is too long.
Toggleable via Settings > System > Show Full Path (default 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 >**_
---------
Co-authored-by: Patryk Radtke <patryk@Patryks-MacBook-Pro.local>
Added instructions for flashing firmware using esptool.
## Summary
Instructions for flashing a specific firmware from the terminal using
`esptool.py`.
## Additional Context
Not everyone has Chrome or Chromium installed. An alternative to the
web-based firmware utility shouldn't be hard to find.
---
### AI Usage
Did you use AI tools to help write this code? _**PARTIALLY**_ : `dmesg`
command analog for MacOS
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
This fixes#682 by restoring API compatibility for external users, eg
deleting on-device books in bulk via calibre using the crosspoint plugin
works now.
* **What changes are included?**
The `/delete` webserver API now accepts the old `path` argument and maps
it to a JSON array element. It is made an error to provide both
arguments.
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
I have compiled, flashed, and tested this on master at commit
0245972132. Before this change, calibre
cannot delete books, either individually or in bulk.
After this change, it can do so successfully.
---
### 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
The cover/custom setting was a bit misleading to me. Usually after you
start reading a book you never see your customs again unless a cover
fails to render for some reason. With this, you can easily show your
custom images by just sleeping from the menus instead of the reader
## Additional Context
* 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? _**< NO >**_
## Summary
* **What is the goal of this PR?** Slightly increase the OOM limit when
loading epubs
* **What changes are included?** Switched vectors for parsing epubs to
deques, allowing use of more memory
## Additional Context
* Increases loadable epub size from 2000+ chapter/ToC entries loadable
to 5000+ chapter/ToC entries
* #1574, but without the complicated stuff
---
### 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**_
### Testing
| Commit | Book | Time |
|----------|------|-------|
| 83cd96bc2f | 1000.epub | ~30 sec |
| 83cd96bc2f | 5000.epub | crash |
| PR | 1000.epub | ~29 sec |
| PR | 5000.epub | ~2 min 20 sec |
=> No actual loading time regressions
[tested_epubs.zip](https://github.com/user-attachments/files/26645243/tested_epubs.zip)
## Summary
Adds an option to allow manual screen refresh on power button short
press.
## Additional Context
there's an option to refresh the screen after a set number of pages. but
sometimes a manual refresh is needed to clear up stale ink pixels. this
works everywhere both in and out of reading mode.
resolves#550
---
### 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**_, to
understand the project structure.
## Summary
**What is the goal of this PR?**
Adds OPDS search support, allowing users to search a catalog directly
from the book browser when the server exposes an OpenSearch template.
**What changes are included?**
- `OpdsParser`: parses the OpenSearch template URL from feed-level
`<link rel="search">` elements and exposes it via `getSearchTemplate()`
- `OpdsBookBrowserActivity`: fetches and stores the search template
after each feed load; shows a Search hint on the Left button when a
template is available; launches the existing `KeyboardEntryActivity` for
query input; URL-encodes the query and fetches the result feed
- Absolute search result URLs are handled correctly in `fetchFeed`
(skips prepending the server base URL)
- A `consumeConfirm` guard prevents the Confirm release that submits the
keyboard from immediately triggering a book download on the first
browsing frame after search results load
## Additional Context
- Search is silently unavailable if the server does not advertise an
OpenSearch template — no UI change in that case
- Tested against a Calibre-Web OPDS endpoint which exposes `<link
rel="search" type="application/opensearchdescription+xml">`
- The inline URL encoder in `performSearch` was necessary as
`StringUtils` has no such utility; worth considering extracting to
`StringUtils` in a follow-up
- No new dependencies introduced
---
### 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**_
---------
Co-authored-by: kira <rammah@tuta.io>
Co-authored-by: Justin Mitchell <justin@jmitch.com>
## Summary
* **What is the goal of this PR?**
Fixes skipping chapter when it's enabled in Settings and you take a
Screenshot.
* **What changes are included?**
A simple return if Power and Down were released before the skipChapter.
## Additional Context
* There is an Issue related #1595.
* Checked that it kept the Power button funcionality for Next page and
Suspend.
---
### 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
This fixes navigating back from the BMP Viewer to the FileBrowser which
was broken when moving to the new ActivityManager
This is fixed by making FileBrowserActivity able to take a full file
path on enter and splitting the basePath and fileName from it and
navigating to the correct place.
fixes:
https://github.com/crosspoint-reader/crosspoint-reader/issues/1553
duplicates:
https://github.com/crosspoint-reader/crosspoint-reader/pull/910 to some
extend but mine has the file cursor at the correct file instead of the
first one in the folder
## Additional Context
* 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**_
---------
Co-authored-by: Jan Ivanov <jan.ivanov@sirma.com>
Adds a PlatformIO pre-build script to patch JPEGDEC library. When
decoding progressive JPEGs with AC coefficients, MCU_SKIP (-8) causes
array index 0xFFFFF8, creating a wild pointer ~33MB past the sMCUs
array. The patch redirects pMCU to sMCUs[0] when MCU_SKIP is active,
preventing store-access faults while maintaining correct behavior for
JPEG_SCALE_EIGHTH decoding. Devices with larger framebuffers (like the
x3) (792×528 = 52,272 bytes vs 800×480 = 48,000 bytes) have less free
heap, shifting the allocation and changing where the wild pointer lands.
Commit 8628297 guarded the DC coefficient write (pMCU[0]) with if (iMCU
>= 0), which prevents crashes for progressive JPEGs whose first scan is
DC-only (iScanEnd == 0). However, if the first scan includes AC
coefficients (iScanEnd > 0), the AC decode loop still writes through the
wild pointer and crashes.
Adde
## Summary
* **What is the goal of this PR?**
Adding Slovenian translation
* **What changes are included?**
Just new slovenian.yaml file with translated strings
## Additional Context
I kindly ask to include it in next release.
---
### 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 >**_
---------
Co-authored-by: Andrej Kralj <andrej.kralj@gmail.com>
Co-authored-by: Uri Tauber <142022451+Uri-Tauber@users.noreply.github.com>
## Summary
**What is the goal of this PR?**
Replaced manual 1-bit BMP header logic in `Xtc::generateCoverBmp()` and
`Xtc::generateThumbBmp()` with calls to the existing `createBmpHeader()`
utility. Added a `BmpRowOrder` enum and param to support the top-down
row order of XTC cover images.
---
### 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 two issues pointed out by `CodeRabbit` in #1433:
1. Free the output buffer in `readFileToMemory` on early error paths
(prevents memory leaks).
2. Check `out.write()` return value in the STORED path (same as in the
DEFLATED path).
I can confirm both issues were real (not hallucinations). Both fixes are
minimal — no behavior change on success.
---
### AI Usage
Did you use AI tools to help write this code? _**< NO >**_
## Summary
**What is the goal of this PR?**
Added `ScopedOpenClose` RAII guard to eliminate repetitive
`wasOpen`/`close()` boilerplate across all ZipFile methods.
---
### 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
Improves text antialiasing quality on the Xteink X3 (SSD1677) display to
bring it closer to X4 rendering quality. Addresses white lines through
letter strokes and ghosting artifacts during page turns and screen
transitions.
## Changes
### Display Driver (open-x4-sdk)
- Dedicated X3 grayscale LUTs with tuned VDL drive strengths for dark
gray (2 time units) and light gray (3 time units), with active GND hold
on non-gray transitions to prevent floating source crosstalk
- Tight scan timing: TP2/TP3 reduced to 1 (total gate-on 7 units vs 17),
minimizing parasitic charge leakage that caused white lines through
letter strokes
- Fast diff BB reinforcement: Added mild VDH reinforcing pulse to
lut_x3_bb_full so black pixels are actively driven during differential
refreshes, clearing gray residue/ghosting
- displayGrayBuffer() updated to use the dedicated gray LUT bank instead
of full refresh LUTs for X3
### Rendering Pipeline
- Re-enabled light gray rendering for X3 text and images, now safe with
dedicated gray LUTs providing proper 4-level gray
- Removed isLightGrayRestricted() gating that was limiting X3 to 3-level
gray
- Runtime display dimensions in DirectPixelWriter and ScreenshotUtil,
replaced hardcoded constants with runtime getters to support X3 792x528
resolution
### Note
The open-x4-sdk submodule references a commit on
juicecultus/community-sdk. A corresponding PR to
open-x4-epaper/community-sdk should be merged first so the submodule ref
resolves on upstream.
## Testing
Tested on physical X3 hardware. White lines through letters
significantly reduced, in-book ghosting improved via BB reinforcement,
antialiasing visually closer to X4 quality.
## AI Disclosure
Yes, AI was used to assist with the development of these changes.
---------
## Summary
* **What is the goal of this PR?** The very first Wifi connection
attempt with saved credentials failed, subsequent attempts succeeded.
This PR fixes the first-attempt-issue.
* **What changes are included?**
## Additional Context
Claude analysis for WifiSelectionActivity
In attemptConnection() ,there's no WiFi.disconnect() before WiFi.begin()
— unlike the scan path earlier which does do a disconnect first.
The root cause on ESP32 is the built-in auto-connect feature: the ESP32
WiFi stack saves credentials to NVS flash and automatically starts
trying to connect on boot before your application code runs. When your
attemptConnection() then calls WiFi.begin(), the stack is already in a
transitional CONNECTING state, and the new begin() call either gets
ignored or collides with the in-progress attempt.
The fix is to add WiFi.disconnect(true) + a short delay in
attemptConnection() before calling WiFi.begin(), and optionally call
WiFi.persistent(false) to stop the ESP32 from auto-connecting on its
own.
---
### 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? (e.g., Implements the new feature for
file uploading.)**
Currently, pressing forward at the end of a book loops back to the last
page. This change will instead sends you to the home page instead.
* **What changes are included?**
Applies the change to the three supported format: EPUB, XTC, TXT
## Additional Context
* This is more of a QOL improvement than a new feature. If there's
interest, we could extend this to track a completed state for ebooks.
---
### 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
The progressive JPEG fixes (AC table skip, MCU_SKIP guard) from PR #1136
have been fixed upstream in bitbank2/JPEGDEC@8628297. Pin to that commit
and remove the pre-build patch script.
## Additional Context
Tested on Strange Pictures epub
---
### 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
* More consistent string formatting in logPrintf
* Moved [ and ] from log level strings into new format string
* Behaviour change: Early exit if user string format fails
## Additional Context
* Should not have performance implications, debug monitor etc work as
before
* Clamp may be unnecessary due to information snprintf currently never
being able to exceed max buffer length, but not having it would bug me
---
### 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, to reason
about correctness**_
## Summary
**What is the goal of this PR?**
A tweak to the fixed-point x-advance and kerning calculations to ensure
that the spacing between any two glyphs is always calculated
consistently.
I noticed that sometimes I'd see common character pairs like "oo" more
than once on a page, and the distance between the two snapped to
different pixels depending on the running accumulated error for the line
of text.
This change uses a differential rounding approach where each glyph's
x-advance plus the kerning relative to the next glyph are combined in
fixed-point precision, then snapped to a pixel to draw the next glyph.
This results in a consistent inter-glyph spacing any time the same two
glyphs show up adjacent to each other, regardless of the accumulated
error across the line.
---
### 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**_
Replace hardcoded DISPLAY_WIDTH, DISPLAY_HEIGHT, and DISPLAY_WIDTH_BYTES
constants with runtime values from display object to support multiple
device models (X3 and X4) with different screen dimensions.
## Summary
* **What is the goal of this PR?**
Solve the issue
https://github.com/crosspoint-reader/crosspoint-reader/issues/1409
* **What changes are included?**
Updated translation for footnotes
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
---
### AI Usage
Did you use AI tools to help write this code? _**NO**_
## Summary
* **What is the goal of this PR?**
Fix inaccurate Russian UI text for the reader auto-turn feature and make
the affected Russian labels consistent with how adjacent UI strings are
formatted.
* **What changes are included?**
Updated Russian translations in `lib/I18n/translations/russian.yaml`:
- changed `Auto Turn` text from wording that implied screen rotation to
wording that means automatic page turning
- adjusted a few Russian prefix/separator strings to include spacing
where the UI concatenates labels with dynamic values
## Screenshots
| Before| After |
|--------|--------|
| <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/db416dd2-6174-4a46-bd3a-6f52d1cc01cb"
/>| <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/dd9055e0-d4f1-459d-bd40-60fc4970d458"
/>|
---
### 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
**What is the goal of this PR?**
Replace verbose constructor initializer lists with in-class default
member initializers in JpegContext and PngContext
---
### 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
**What is the goal of this PR?**
Replace SFINAE std::enable_if_t with a C++20 `requires` clause for
clearer constraint expression and better compiler diagnostics on
mismatch.
---
### 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
EPUBs that use ISO 639-2 three-letter language codes in their
`dc:language` metadata (e.g. `<dc:language>eng</dc:language>`) got no
hyphenation. The hyphenator registry only matched ISO 639-1 two-letter
codes (`"en"`, `"fr"`, etc.), so `"eng"` produced a null hyphenator and
every word in the book was treated as unhyphenatable.
Added a normalization step in `hyphenatorForLanguage` that maps ISO
639-2 codes (both bibliographic and terminological variants) to their
two-letter equivalents before the registry lookup.
Discovered via *Project Hail Mary* (Random House), which uses
`<dc:language>eng</dc:language>`.
---
### 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
Adds Xteink X3 hardware support to CrossPoint Reader. The X3 uses the
same SSD1677 e-ink controller as the X4 but with a different panel
(792x528 vs 800x480), different button layout, and an I2C fuel gauge
(BQ27220) instead of ADC-based battery reading.
All X3-specific behavior is gated by runtime device detection — X4
behavior is unchanged.
Depends on community-sdk X3 support: open-x4-epaper/community-sdk#19
(merged).
## Changes
### HAL Layer
**HalGPIO** (`lib/hal/HalGPIO.cpp/.h`)
- I2C-based device fingerprinting at boot: probes for BQ27220 fuel
gauge, DS3231 RTC, and QMI8658 IMU to distinguish X3 from X4
- Detection result cached in NVS for fast subsequent boots
- Exposes `deviceIsX3()` / `deviceIsX4()` helpers used throughout the
codebase
- X3 button mapping (7 GPIOs vs X4's layout)
- USB connection detection and wake classification for X3
**HalDisplay** (`lib/hal/HalDisplay.cpp/.h`)
- Calls `einkDisplay.setDisplayX3()` before init when X3 is detected
- Requests display resync after power button / flash wake events
- Runtime display dimension accessors (`getDisplayWidth()`,
`getDisplayHeight()`, `getBufferSize()`)
- Exposed as global `display` instance for use by image converters
**HalPowerManager** (`lib/hal/HalPowerManager.cpp/.h`)
- X3 battery reading via I2C fuel gauge (BQ27220 at 0x55, SOC register)
- X3 power button uses GPIO hold for deep sleep
### Display & Rendering
**GfxRenderer** (`lib/GfxRenderer/GfxRenderer.cpp/.h`)
- Buffer size and display dimensions are now runtime values (not
compile-time constants) to support both panel sizes
- X3 anti-aliasing tuning: only the darker grayscale level is applied to
avoid washed-out text on the X3 panel. X4 retains both levels via
`deviceIsX4()` gate
**Image Converters** (`lib/JpegToBmpConverter`, `lib/PngToBmpConverter`)
- Cover image prescale target uses runtime display dimensions from HAL
instead of hardcoded 800x480
### UI Themes
**BaseTheme / LyraTheme** (`src/components/themes/`)
- X3 button position mapping for the different physical layout
- Adjusted UI element positioning for 792x528 viewport
### Boot & Init
**main.cpp**
- X3 hardware detection logging
- Adjusted init sequence for X3 (no `HalSystem::begin()` dependency on
X3 path)
**HomeActivity**
- Uses runtime `renderer.getBufferSize()` instead of static
`GfxRenderer::getBufferSize()`
FYI I did not add support for the gyro page turner. That can be it's own
PR.
2026-04-04 10:25:43 -05:00
381 changed files with 148941 additions and 121332 deletions
--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."
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)
// No file.close() needed — DESTRUCTOR_CLOSES_FILE=1 handles it at scope exit
}
```
**Usage**: See example above. Uses `FsFile` (SdFat), NOT Arduino `File`.
**Usage**: See example above. Uses `FsFile` (SdFat), NOT Arduino `File`. Do NOT add `file.close()` for local variables (see DESTRUCTOR_CLOSES_FILE above).
**SdFat is not thread-safe; all SD access MUST go through HalStorage**:
- SdFat's `SdSpiCard` tracks SPI bus state with an unsynchronized `m_spiActive` bool. Two tasks calling SdFat concurrently can confuse that state machine and end with one task calling `SPIClass::endTransaction()` against a paramLock the *other* task is holding. That trips FreeRTOS's `xTaskPriorityDisinherit` assert (`tasks.c:5156, pxTCB == pxCurrentTCBs[0]`) and panics the system. See SdFat issue #518.
-`HalStorage` serializes everything via `storageMutex`. Downstream code 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.
---
@@ -167,7 +183,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 +273,78 @@ When a template is necessary, limit instantiations: use explicit template instan
**Rules**: NO exceptions, NO abort(), ALWAYS log before error return
**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)
// 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]
- 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.
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.
## What can CrossPoint do?
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**.
- **Reader engine**: EPUB 2/3 rendering with embedded-style option, image handling, hyphenation, kerning, chapter navigation, footnotes, go-to-percent, auto page turn, orientation control, focus reading, KOReader progress sync and more.
This project is **not affiliated with Xteink**; it's built as a community project.
- **Various formats**: native handling for `.epub`, `.xtc/.xtch`, `.txt`, and `.bmp`.
## Features & Usage
- **Screenshots.**
- [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
- **Custom fonts**: install your favorite fonts on the SD card.
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).
- **Tilt page turn (X3 only)**.
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint, including the
For more details about the scope of the project, see the [SCOPE.md](SCOPE.md) document.
- **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 WiFi), both with QR helpers
- Calibre wireless connect flow
- OPDS browser with saved servers (up to 8), search, pagination, and direct download
- OTA update checks and installs from GitHub releases
## Installing
- **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.
### Web (latest firmware)
- **Localization**: 22 UI languages and counting.
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"
### Coming soon:
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.
- RTL support — Arabic, Hebrew, and Farsi.
### Web (specific firmware version)
- Bookmarks.
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
- Dictionary lookup — inline word lookup without leaving the reader.
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.
- More themes.
- Much more! stay tuned.
---
## 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**.
>
> **The Papyrix fork has removed OTA update support from its code.** If you flash Papyrix onto a
> USB-locked unit, you will have **zero update or recovery path** and will be stuck on it forever. **Do not flash
> Papyrix (or any other unsupported firmware) on a locked device.**
## 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.
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.
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_12471232/ # Each EPUB is cached to a subdirectory named `epub_<hash>`
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.
Removing`/.crosspoint`clears all cached metadata and forces a full regeneration on next open. Note: the cache isn't cleared automatically when you delete a book, and moving a file to a new path resets its reading progress.
For more details on the internal file structures, see the [file formats document](./docs/file-formats.md).
---
## Contributing
Contributions are very welcome!
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.
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
Everyone here is a volunteer, so please be respectful and patient. For governance and community expectations, see [GOVERNANCE.md](./GOVERNANCE.md).
---
CrossPoint Reader is **not affiliated with Xteink or any manufacturer of the X4 hardware**.
## Community forks
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.
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.
- [crosspoint-reader (jpirnay)](https://github.com/jpirnay/crosspoint-reader) — Faster integration of functionality. Tracks upstream PRs and integrates the good ones ahead of the official merge.
- [crosspoint-reader-cjk](https://github.com/aBER0724/crosspoint-reader-cjk) — Purpose-built for Chinese, Japanese, and Korean reading.
- [inx](https://github.com/obijuankenobiii/inx) — Completely reimagines the user interface with tabbed navigation.
- [crosspoint-reader-papers3](https://github.com/juicecultus/crosspoint-reader-papers3) — Crosspoint port for M5Stack Paper S3.
**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.
@@ -27,6 +27,12 @@ usability over "swiss-army-knife" functionality.
***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.
***Clock Display (device dependent):**
| Device | Scope |
| -- | -- |
| X3 | The X3 uses a dedicated DS3231 RTC, which maintains accurate time across sleep cycles and can be treated as a reliable wall clock. |
| X4 | The X4 relies on the ESP32-C3's internal RTC, which drifts significantly during deep sleep. NTP sync could correct this, with an appropriate user experience around connecting to the internet on wake or on demand. This causes some tension with the **Active Connectivity** section below, so please open a discussion about this UX if it's a feature you would find useful. |
### Out-of-Scope
@@ -43,8 +49,6 @@ usability over "swiss-army-knife" functionality.
*These features align with CrossPoint's goals but are impractical on the current hardware or produce poor UX.*
* **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.
* **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.
@@ -155,7 +157,7 @@ The Settings screen allows you to configure the device's behavior. There are a f
#### 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".
@@ -194,12 +196,52 @@ The Settings screen allows you to configure the device's behavior. There are a f
- **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.
- **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.
- **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
#### 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 WiFi network management, see [Web Settings (WiFi + OPDS)](#366-web-settings-wifi--opds).
#### 3.6.6 Web Settings (WiFi + OPDS)
While in **File Transfer** mode, the web settings page includes management cards for both **WiFi Networks** and **OPDS Servers**.
1. On device: open **File Transfer** and connect to WiFi.
1. In a browser, open `http://<device-ip>/settings` or `http://crosspoint.local`.
1. In **WiFi Networks**, add, edit, or delete saved network entries (SSID + optional password).
1. 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 device-side WiFi 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.
@@ -336,7 +378,8 @@ 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.
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 will trigger a re-index of your current book, the same as when changing font settings. Once indexing is complete, page turns proceed as normal. No changes are made to your EPUB files.
## Examples
<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.
Loaded 20 of 381 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.