Compare commits

..

48 Commits

Author SHA1 Message Date
Dave Allie 5fdf23f1d2 Cut release 0.12.0 2026-01-03 19:42:14 +11:00
Justinian 2fb417ee90 Feat/sleep and refresh settings (#209)
## Summary

* **What is the goal of this PR?** (e.g., Fixes a bug in the user
authentication module, 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).

---------

Co-authored-by: ratedcounsel <hello@ratedcounsel.com>
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-03 19:33:42 +11:00
V c8f6160fbc Refactor semantic version comparison for OTA updates (#216)
## Summary

* **What is the goal of this PR?** (e.g., Fixes a bug in the user
authentication module, Implements the new feature for
  file uploading.)

This PR refactors the semantic version comparison logic used during OTA
update checks.

Memory stats before : 
RAM:   [===       ]  30.8% (used 101068 bytes from 327680 bytes)
Flash: [========= ]  85.7% (used **5617830** bytes from 6553600 bytes)

Memory stats before : 
RAM:   [===       ]  30.8% (used 101068 bytes from 327680 bytes)
Flash: [========= ]  85.7% (used **5616870** bytes from 6553600 bytes)


* **What changes are included?**

Replaced std::string::substr() and std::stoi() based parsing with a
lightweight, heap-free approach.
Version parsing is now done in a single pass without creating temporary
std::string objects.
Behavior remains identical: versions are still compared as
MAJOR.MINOR.PATCH.

## Additional Context

`std::string::substr() ` creates a new string and performs heap
allocation

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks, specific areas to
focus on).
2026-01-03 19:19:53 +11:00
Brendan O'Leary 8e4484cd22 Add buton hints to keyboard screen (#205)
## Summary

This adds the correctly styled button hints to the keyboard screen as
well as the ability to add hints to the side buttons (and up/down hints
to that screen)

## Additional Context

N/A
2026-01-03 19:17:53 +11:00
Pavel Liashkov 0332e1103a Add EPUB 3 nav.xhtml TOC support (#197)
## Summary

* **What is the goal of this PR?** Add EPUB 3 support by implementing
native navigation document (nav.xhtml) parsing with NCX fallback,
addressing issue Fixes: #143.

  * **What changes are included?**
- New `TocNavParser` for parsing EPUB 3 HTML5 navigation documents
(`<nav epub:type="toc">`)
- Detection of nav documents via `properties="nav"` attribute in OPF
manifest
- Fallback logic: try EPUB 3 nav first, fall back to NCX (EPUB 2) if
unavailable
- Graceful degradation: books without any TOC now load with a warning
instead of failing

  ## Additional Context

* The implementation follows the existing streaming XML parser pattern
using Expat to minimize RAM usage on the ESP32-C3
* EPUB 3 books that include both nav.xhtml and toc.ncx will prefer the
nav document (per EPUB 3 spec recommendation)
* No breaking changes - existing EPUB 2 books continue to work as before
* Tested on examples from
https://idpf.github.io/epub3-samples/30/samples.html
2026-01-03 19:10:35 +11:00
Jonas Diemer 5790d6f5dc Subtract time it took reaching the evaluation from the press duration. (#208)
Adresses #206
2026-01-03 18:54:23 +11:00
Jake Lyell 062d69dc2a Add support for uploading multiple epubs (#202)
Upload multiple files at once in sequence. Add retry button for files
that fail

## Summary

* **What is the goal of this PR?**
Add support for selecting multiple epub files in one go, before
uploading them all to the device
* **What changes are included?**
Allow multiple selections to be submitted to the input field.
Sends each file to the device one by one in a loop
Adds retry logic and UI for easy re-trying of failed uploads

Addresses #201 


button now says "Choose Files", and shows the number of files you
selected
<img width="506" height="199" alt="image"
src="https://github.com/user-attachments/assets/64b0b921-1e67-438e-9cd7-57d5466f2456"
/>

Shows which file is uploading:
<img width="521" height="283" alt="image"
src="https://github.com/user-attachments/assets/17b4d349-0698-4712-984c-b72fcdcb0918"
/>

Failed upload dialog:
<img width="851" height="441" alt="image"
src="https://github.com/user-attachments/assets/e8bf4aa6-d3d2-4c0b-9c7a-420e8c413033"
/>
<img width="834" height="641" alt="image"
src="https://github.com/user-attachments/assets/656a9732-3963-4844-94e3-4d8736f6d9d5"
/>
2026-01-02 18:32:26 +11:00
Maeve Andrews 5e9626eb2a Add paragraph alignment setting (justify/left/center/right) (#191)
## Summary

* **What is the goal of this PR?** 

Add a new user setting for paragraph alignment, instead of hard-coding
full justification.

* **What changes are included?**

One new line in the settings screen, with 4 options
(justify/left/center/right). Default is justified since that's what it
was already. I personally only wanted to disable justification and use
"left", but I included the other options for completeness since they
were already supported.

## Additional Context

Tested on my X4 and looks as expected for each alignment.

Co-authored-by: Maeve Andrews <maeve@git.mail.maeveandrews.com>
2026-01-02 18:21:48 +11:00
Jonas Diemer 00e83af4e8 Show "Entering Sleep" on black, so it's quicker to notice (in book). (#181)
Black popup is easier to notice at higher contrast.
2026-01-02 17:55:21 +11:00
Jonas Diemer 39080c0e51 Skip soft hyphens. (#195)
For now, let's skip the soft hyphens (later, we can treat them in the
layouter). See
https://github.com/daveallie/crosspoint-reader/discussions/17#discussioncomment-15378475
2026-01-02 17:54:46 +11:00
Brendan O'Leary 9e59a5106b Fix race condition with keyboard and Wifi entry (#204)
## Summary

* **What is the goal of this PR?** Fixes a bug -
https://github.com/daveallie/crosspoint-reader/issues/187 - where the
screen would freeze after entering a WiFi password, causing the device
to appear hung.

* **What changes are included?**
- Fixed a race condition in `WifiSelectionActivity::displayTaskLoop()`
that caused rendering of an empty screen when transitioning from the
keyboard subactivity
- Added `vTaskDelay()` when a subactivity is active to prevent CPU
starvation from a tight `continue` loop
- Added a check to skip rendering when in `PASSWORD_ENTRY` state,
allowing the state machine to properly transition to `CONNECTING` before
the display updates

## Additional Context

* **Root cause:** When the keyboard subactivity exited after password
entry, the display task would wake up and attempt to render. However,
the `state` was still `PASSWORD_ENTRY` (before `attemptConnection()`
changed it to `CONNECTING`), and since there was no render case for
`PASSWORD_ENTRY`, the display would show a cleared/empty buffer,
appearing frozen.

* **Performance implications:** The added `vTaskDelay(10)` calls when
subactivity is active or in `PASSWORD_ENTRY` state actually improve
performance by preventing CPU starvation - previously the display task
would spin in a tight loop with `continue` while a subactivity was
present.

* **Testing focus:** Test the full WiFi connection flow:
  1. Enter network selection
  2. Select a network requiring a password
  3. Enter password and press OK
  4. Verify "Connecting..." screen appears
  5. Verify connection completes and prompts to save password
2026-01-02 17:49:16 +11:00
Pavel Liashkov a922e553ed Prevent device sleep during WiFi file transfer and OTA updates (#203)
## Summary

* **What is the goal of this PR?** Fixes #199 - Device falls asleep
during WiFi file transfer after 10 minutes of inactivity, disconnecting
the web server.

  * **What changes are included?**
    - Add `preventAutoSleep()` virtual method to `Activity` base class
- Modify main loop to reset inactivity timer when `preventAutoSleep()`
returns true
- Override `preventAutoSleep()` in `CrossPointWebServerActivity`
(returns true when web server running)
- Override `preventAutoSleep()` in `OtaUpdateActivity` (returns true
during update check/download)

  ## Additional Context

* The existing `skipLoopDelay()` method controls loop timing (yield vs
delay) for HTTP responsiveness. The new `preventAutoSleep()` method is
semantically separate - it explicitly signals that an activity should
keep the device awake.
* `CrossPointWebServerActivity` uses both methods: `skipLoopDelay()` for
responsive HTTP handling, `preventAutoSleep()` for staying awake.
* `OtaUpdateActivity` only needs `preventAutoSleep()` since the OTA
library handles HTTP internally.
2026-01-02 17:44:17 +11:00
Dave Allie 04ad4e5aa4 Replace book and section bin format images with ImHex hexpat definition (#189)
## Summary

* Replace book and section bin format images with ImHex hexpat
definition
* This should give readers an understanding of the file format but also
supply some utility when validating content/output
2025-12-31 13:28:24 +11:00
Dave Allie 6e9ba1006a Use sane smaller data types for data in section.bin (#188)
## Summary

* Update EpdFontFamily::Style to be u8 instead of u32 (saving 3 bytes
per word)
* Update layout width/height to be u16 from int
* Update page element count to be u16 from u32
* Update text block element count to be u16 from u32
* Bumped section bin version to version 8
2025-12-31 13:11:36 +11:00
Luke Stein 40f9ed485c Enhance USER_GUIDE with links and clarifications (#185)
Updated USER_GUIDE.md for clarity and added links to sections.

## Summary

* Clarify and improve documentation
2025-12-31 10:01:48 +11:00
Dave Allie b82e044ac3 Cut release 0.11.2 2025-12-31 09:28:14 +11:00
Dave Allie 026733a4fe Hide "System Volume Information" folder (#184)
## Summary

* Hide "System Volume Information" folder
* It's a FAT filesystem folder, shouldn't be used

## Additional Context

* Fixes https://github.com/daveallie/crosspoint-reader/issues/177
2025-12-31 09:27:17 +11:00
Dave Allie 57b075ec97 Update README.md features 2025-12-31 09:18:05 +11:00
dangson 648c688642 Update button hints on OTA update screen and update user guide to reflect current settings (#183)
## Summary

- Update the button hints on the OTA update screen to reflect the
current front button layout.
- Update user guide to reflect current available settings. A lot has
been added recently!
2025-12-31 09:15:40 +11:00
Jonas Diemer 06065dfd8b Show book title instead of "Select Chapter". (#169)
I think this is nicer ;)
2025-12-31 09:10:41 +11:00
Eunchurn Park 93226c9fbb Fix file browser navigation for non-ASCII folder names (#178)
## Summary

* **What is the goal of this PR?**

Fix file browser failing to navigate into subdirectories with non-ASCII
(Korean/Unicode) folder names.

* **What changes are included?**

- Enable UTF-8 long file names in SdFat (`USE_UTF8_LONG_NAMES=1`)
- Add directory validation before iterating files
- Add `rewindDirectory()` call for stability

## Additional Context
2025-12-31 09:08:31 +11:00
Dave Allie 941643cf97 Cut release 0.11.1 2025-12-31 02:47:24 +11:00
Dave Allie 9bba41ed96 Move home screen battery indicator to avoid clashing with button hints (#174)
## Summary

* Move home screen battery indicator to avoid clashing with button hints
* Default button mapping was fine, but others clashes with the indicator
2025-12-31 02:46:46 +11:00
Dave Allie 34cf5f0636 Add button mapping for Left, Back, Confirm, Right (#173)
## Summary

* Add button mapping for Left, Back, Confirm, Right for front buttons

## Additional Context

* Asked for in
https://github.com/daveallie/crosspoint-reader/discussions/78#discussioncomment-15375326
2025-12-31 02:46:35 +11:00
Dave Allie f2ca65d752 Swap from Aleo to Bookerly for wider glyph support (#172)
## Summary

* Swap from Aleo to Bookerly for wider glyph support
* Swap from Space Grotesk to a small Noto Sans

## Additional Context

* 0.11.0 swapped to Aleo which has a few issues (things like Cyrillic
support for eg)
2025-12-31 02:28:25 +11:00
Dave Allie 6a8971fc20 Cut release 0.11.0 2025-12-30 23:42:19 +11:00
Dave Allie e2cba5be83 Show battery percentage on home screen (#167)
## Summary

* Show battery percentage on home screen
  * Moved battery rendering logic into shared ScreenComponents class
* As discussed
https://github.com/daveallie/crosspoint-reader/discussions/155
2025-12-30 23:41:47 +11:00
Dave Allie 52a0b5bbe9 Small cleanups from https://github.com/juicecultus/crosspoint-reader-x4 2025-12-30 23:19:08 +11:00
Dave Allie 3abcd0d05d Redesign home screen (#166)
## Summary

* Redesigned home screen with big option to continue reading and
slightly nicer options to navigate to core sections
* Attempt to use the cached EPUB details (title, author) if they exist,
otherwise fall back to file name
* Adjusted button hints on home screen, removed Back option and changed
left/right to up/down

## Additional Context

* Core of this work comes from @ChandhokTannay in
https://github.com/ChandhokTannay/crosspoint-reader/commit/1d36a86ef1f016e796ef993c14fb1f9ea2f0101d
2025-12-30 23:18:10 +11:00
Jonas Diemer 03f0ce04cc Feature: go to text/start reference in epub guide section at first start (#156)
This parses the guide section in the content.opf for text/start
references and jumps to this on first open of the book.

Currently, this behavior will be repeated in case the reader manually
jumps to Chapter 0 and then re-opens the book. IMO, this is an
acceptable edge case (for which I couldn't see a good fix other than to
drag a "first open" boolean around).

---------

Co-authored-by: Sam Davis <sam@sjd.co>
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-30 23:02:46 +11:00
Dave Allie be1b5bad21 Parse the author name from content.opf file (#165)
## Summary

* Parse the author name from content.opf file
  * Listed in the dc:creator tag within the metadata section
2025-12-30 22:15:44 +11:00
Dave Allie 3dd52f30fa Adjust screen title position 2025-12-30 22:06:57 +11:00
Dave Allie e43fec79be Add setting for line spacing to adjust space between lines (#164)
## Summary

* Add setting for line spacing to adjust space between lines
* Aleo is already a bit tighter than Noto Sans and Open Dyslexic, so
have adjusted the values to match, this can be tweaked in the future
2025-12-30 19:34:46 +11:00
Dave Allie bf7bffd506 Aleo, Noto Sans, Open Dyslexic fonts (#163)
## Summary

* Swap out Bookerly font due to licensing issues, replace default font
with Aleo
* I did a bunch of searching around for a nice replacement font, and
this trumped several other like Literata, Merriwether, Vollkorn, etc
* Add Noto Sans, and Open Dyslexic as font options
  * They can be selected in the settings screen
* Add font size options (Small, Medium, Large, Extra Large)
  * Adjustable in settings
* Swap out uses of reader font in headings and replaced with slightly
larger Ubuntu font
* Replaced PixelArial14 font as it was difficult to track down, replace
with Space Grotesk
* Remove auto formatting on generated font files
* Massively speeds up formatting step now that there is a lot more CPP
font source
* Include fonts with their licenses in the repo

## Additional Context

Line compression setting will follow

| Font | Small | Medium | Large | X Large |
| --- | --- | --- | --- | --- |
| Aleo |
![IMG_5704](https://github.com/user-attachments/assets/7acb054f-ddef-4080-b3c8-590cfaf13115)
|
![IMG_5705](https://github.com/user-attachments/assets/d4819036-5c89-486e-92c3-86094fa4d89a)
|
![IMG_5706](https://github.com/user-attachments/assets/35caf622-d126-4396-9c3e-f927eba1e1f4)
|
![IMG_5707](https://github.com/user-attachments/assets/af32370a-6244-400f-bea9-5c27db040b5b)
|
| Noto Sans |
![IMG_5708](https://github.com/user-attachments/assets/1f9264a5-c069-4e22-9099-a082bfcaabc5)
|
![IMG_5709](https://github.com/user-attachments/assets/ef6b07fe-8d87-403a-b152-05f50b69b78e)
|
![IMG_5710](https://github.com/user-attachments/assets/112a5d20-262c-4dc0-b67d-980b237e4607)
|
![IMG_5711](https://github.com/user-attachments/assets/d25e0e1d-2ace-450d-96dd-618e4efd4805)
|
| Open Dyslexic |
![IMG_5712](https://github.com/user-attachments/assets/ead64690-f261-4fae-a4a2-0becd1162e2d)
|
![IMG_5713](https://github.com/user-attachments/assets/59d60f7d-5142-4591-96b0-c04e0a4c6436)
|
![IMG_5714](https://github.com/user-attachments/assets/bb6652cd-1790-46a3-93ea-2b8f70d0d36d)
|
![IMG_5715](https://github.com/user-attachments/assets/496e7eb4-c81a-4232-83e9-9ba9148fdea4)
|
2025-12-30 19:21:47 +11:00
Dave Allie 9f31f80c80 Show previous title for unnamed spines (#158)
## Summary

* Show previous title for unnamed spines
* The spec is a little unclear, but there are plenty of cases where
chapters are split up in parts and should show the previous chapter's
title
* List TOC items instead of spine items in chapter select
* Bump `BOOK_CACHE_VERSION` to `2` to force regeneration of spine item's
TOC indexes
2025-12-30 18:52:42 +11:00
Dave Allie fb5fc32c5d Add exFAT support (#150)
## Summary

* Swap to updated SDCardManager which uses SdFat
* Add exFAT support
  * Swap to using FsFile everywhere
* Use newly exposed `SdMan` macro to get to static instance of
SDCardManager
* Move a bunch of FsHelpers up to SDCardManager
2025-12-30 16:09:30 +11:00
Jonas Diemer d4bd119950 Add option to apply format fix only on changed files (much faster) (#153)
The default version parses a lot of files and takes ~5s on my machine.
This adds an option `-g` to run only on files modified/staged in git.
2025-12-30 16:05:06 +11:00
Dave Allie 85d76da967 Split XTC file version into major minor bytes (#161)
## Summary

* Split XTC file version into major minor bytes
  * Continue to support both 1.0 and 0.1

## Additional Context

* See
https://github.com/daveallie/crosspoint-reader/issues/146#issuecomment-3698223951
for more detail

FYI @treetrum @eunchurn
2025-12-30 15:48:20 +11:00
Dave Allie e4ac90f5c1 Accept big endian version in XTC files (#159)
## Summary

* Accept big endian version in XTC files
* Recently, two issues
(https://github.com/daveallie/crosspoint-reader/issues/157 and
https://github.com/daveallie/crosspoint-reader/issues/146) have popped
up with XTC files with a big endian encoded version. This is read out as
256.
  * We should be more lax and accept these values.
2025-12-30 13:36:25 +11:00
Sam Davis 278b056bd0 Add chapter select support to XTC files (#145)
## Summary

- **What is the goal of this PR?** Add chapter selection support to the
XTC reader activity, including parsing chapter metadata from XTC files.
- **What changes are included?** Implemented XTC chapter parsing and
exposure in the XTC library, added a chapter selection activity for XTC,
integrated it into XtcReaderActivity, and normalized chapter page
indices by shifting them to 0-based.

  ## Additional Context

- The reader uses 0-based page indexing (first page = 0), but the XTC
chapter table appears to be 1-based (first page = 1), so chapter
start/end pages are shifted down by 1 during parsing.
2025-12-30 12:49:18 +11:00
Jonas Diemer b01eb50325 Shorten continueLabel to actual screen width. (#151) 2025-12-29 23:18:23 +11:00
Jonas Diemer 1bfe694807 Improvement/settings selection by inversion (#152)
Style in settings like everywhere else. Also improved the alignment of
the settings value.
2025-12-29 23:18:16 +11:00
Jonas Diemer 7b32a87596 Recalibrated power button duration, decreased long setting slightly. (#149)
Slight tuning, as I noticed sometimes inconsistent behavior (reported
>200ms of calibration value, I assume related to the Serial output).
2025-12-29 23:18:12 +11:00
Dave Allie 071ccb9d1b Custom zip parsing (#140)
## Summary

* Use custom zip central directory parsing to lower memory usage when
loading zipped epub content
2025-12-29 21:17:29 +11:00
Yona d7f4bd54f5 Add side button layout configuration while on reader (#147)
## Summary

Allow swapping the side button layout between *next page - prev page*
and *prev page - next page* while reading
2025-12-29 21:17:10 +11:00
Dave Allie 2437943c94 Remove usused module 2025-12-29 21:07:26 +11:00
dangson 140d8749a6 Support swapping the functionality of the front buttons (#133)
## Summary

**What is the goal of this PR?** 

Adds a setting to swap the front buttons. The default functionality are:
Back/Confirm/Left/Right. When this setting is enabled they become:
Left/Right/Back/Confirm. This makes it more comfortable to use when
holding in your right hand since your thumb can more easily rest on the
next button. The original firmware has a similar setting.

**What changes are included?**

- Add the new setting.
- Create a mapper to dynamically switch the buttons based on the
setting.
- Use mapper on the various activity screens.
- Update the button hints to reflect the swapped buttons.

## Additional Context

Full disclosure: I used Codex CLI to put this PR together, but did
review it to make sure it makes sense.

Also tested on my device:
https://share.cleanshot.com/k76891NY
2025-12-29 14:59:14 +11:00
Dave Allie 534504cf7a Consolidate chapter page data into single file (#144)
## Summary

* Consolidate chapter page data into single file
* Header structure of the file stays the same, following the page count,
we now put a LUT offset
   * The page data is all then appended to this file
* Finally the LUT is appended to the end of the file, and the page count
is updated
* This will also significantly improve the duration of cache cleanup
which takes a while to scan the directory and cleanup content
* Remove page file version as it's all tied up into the section file now
* Bumped section file version to 7
* Moved section content into sub directory
* Updated docs

## Additional Context

* Benchmarks:
  * Generating 74 pages of content from a chapter in Jade Legacy took:
    * master: 6,229ms
    * this PR: 1,305ms
    * Speedup of 79%
  * Generating 207 pages of content from Livesuit book:
    * With progress bar UI updates:
      * master: 24,250ms
      * this PR: 8,063ms
      * Speedup of 67%
    * Without progress bar UI updates:
      * master: 13,055ms
      * this PR: 3,600ms
      * Speedup of 72%
2025-12-29 13:19:54 +11:00
174 changed files with 261245 additions and 21906 deletions
+20 -21
View File
@@ -25,7 +25,7 @@ This project is **not affiliated with Xteink**; it's built as a community projec
## Features & Usage
- [x] EPUB parsing and rendering
- [x] EPUB parsing and rendering (EPUB 2 and EPUB 3)
- [ ] Image support within EPUB
- [x] Saved reading position
- [x] File explorer with file picker
@@ -33,11 +33,13 @@ This project is **not affiliated with Xteink**; it's built as a community projec
- [x] Support nested folders
- [ ] EPUB picker with cover art
- [x] Custom sleep screen
- [ ] Cover sleep screen
- [x] Cover sleep screen
- [x] Wifi book upload
- [ ] Wifi OTA updates
- [ ] Configurable font, layout, and display options
- [ ] Screen rotation
- [x] Wifi OTA updates
- [x] Configurable font, layout, and display options
- [ ] User provided fonts
- [ ] Full UTF support
- [x] Screen rotation
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint.
@@ -98,9 +100,9 @@ CrossPoint Reader is pretty aggressive about caching data down to the SD card to
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.
### EPUB caching
### Data caching
The first time chapters of an EPUB are loaded, they are cached to the SD card. Subsequent loads are served from the
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:
@@ -108,25 +110,22 @@ cache. This cache directory exists at `.crosspoint` on the SD card. The structur
.crosspoint/
├── epub_12471232/ # Each EPUB is cached to a subdirectory named `epub_<hash>`
│ ├── progress.bin # Stores reading progress (chapter, page, etc.)
│ ├── 0/ # Each chapter is stored in a subdirectory named by its index (based on the spine order)
│ │ ├── section.bin # Section metadata (page count)
│ │ ├── page_0.bin # Each page is stored in a separate file, it
│ │ ├── page_1.bin # contains the position (x, y) and text for each word
│ │ └── ...
│ ├── 1/
│ │ ├── section.bin
│ │ ├── page_0.bin
│ │ ├── page_1.bin
│ │ └── ...
│ └── ...
│ ├── cover.bmp # Book cover image (once generated)
│ ├── book.bin # Book metadata (title, author, spine, table of contents, etc.)
│ └── sections/ # All chapter data is stored in the sections subdirectory
│ ├── 0.bin # Chapter data (screen count, all text layout info, etc.)
│ ├── 1.bin # files are named by their index in the spine
│ └── ...
│
└── epub_189013891/
```
Deleting the `.crosspoint` directory will clear the cache.
Deleting the `.crosspoint` directory will clear the entire cache.
Due the way it's currently implemented, the cache is not automatically cleared when the EPUB is deleted and moving an
EPUB file will reset the reading progress.
Due the way it's currently implemented, the cache is not automatically cleared when a book is deleted and moving a book
file will use a new cache directory, resetting the reading progress.
For more details on the internal file structures, see the [file formats document](./docs/file-formats.md).
## Contributing
+39 -16
View File
@@ -5,7 +5,7 @@ the device.
## 1. Hardware Overview
The device utilises the standard buttons on the Xtink X4 in the same layout:
The device utilises the standard buttons on the Xtink X4 (in the same layout as the manufacturer firmware, by default):
### Button Layout
| Location | Buttons |
@@ -13,20 +13,23 @@ The device utilises the standard buttons on the Xtink X4 in the same layout:
| **Bottom Edge** | **Back**, **Confirm**, **Left**, **Right** |
| **Right Side** | **Power**, **Volume Up**, **Volume Down** |
Button layout can be customized in **[Settings](#35-settings)**.
---
## 2. Power & Startup
### Power On / Off
To turn the device on or off, **press and hold the Power button for half a second**. In **Settings** you can configure
To turn the device on or off, **press and hold the Power button for half a second**. In **[Settings](#35-settings)** you can configure
the power button to trigger on a short press instead of a long one.
### First Launch
Upon turning the device on for the first time, you will be placed on the **Home** screen.
Upon turning the device on for the first time, you will be placed on the **[Home](#31-home-screen)** screen.
> **Note:** On subsequent restarts, the firmware will automatically reopen the last book you were reading.
> [!NOTE]
> On subsequent restarts, the firmware will automatically reopen the last book you were reading.
---
@@ -34,10 +37,10 @@ Upon turning the device on for the first time, you will be placed on the **Home*
### 3.1 Home Screen
The Home Screen is the main entry point to the firmware. From here you can navigate to the **Book Selection** screen,
**Settings** screen, or **File Upload** screen.
The Home Screen is the main entry point to the firmware. From here you can navigate to **[Reading Mode](#4-reading-mode)** with the most recently read book, **[Book Selection](#32-book-selection)**,
**[Settings](#35-settings)**, or the **[File Upload](#34-file-upload-screen)** screen.
### 3.2 Book Selection (Read)
### 3.2 Book Selection
The Book Selection acts as a folder and file browser.
@@ -45,13 +48,13 @@ The Book Selection acts as a folder and file browser.
and down through folders and books.
* **Open Selection:** Press **Confirm** to open a folder or read a selected book.
### 3.3 Reading Screen
### 3.3 Reading Mode
See [4. Reading Mode](#4-reading-mode) below for more information.
See [Reading Mode](#4-reading-mode) below for more information.
### 3.4 File Upload Screen
The File Upload screen allows you to upload new e-books to the device. When you enter the screen you'll be prompted with
The File Upload screen allows you to upload new e-books to the device. When you enter the screen, you'll be prompted with
a WiFi selection dialog and then your X4 will start hosting a web server.
See the [webserver docs](./docs/webserver.md) for more information on how to connect to the web server and upload files.
@@ -62,11 +65,32 @@ The Settings screen allows you to configure the device's behavior. There are a f
- **Sleep Screen**: Which sleep screen to display when the device sleeps, options are:
- "Dark" (default) - The default dark sleep screen
- "Light" - The same default sleep screen, on a white background
- "Custom" - Custom images from the SD card, see [3.6 Sleep Screen](#36-sleep-screen) below for more information
- "Custom" - Custom images from the SD card, see [Sleep Screen](#36-sleep-screen) below for more information
- "Cover" - The book cover image (Note: this is experimental and may not work as expected)
- **Status Bar**: Configure the status bar displayed while reading, options are:
- "None" - No status bar
- "No Progress" - Show status bar without reading progress
- "Full" - Show status bar with reading progress
- **Extra Paragraph Spacing**: If enabled, vertical space will be added between paragraphs in the book, if disabled,
paragraphs will not have vertical space between them, but will have first word indentation.
- **Short Power Button Click**: Whether to trigger the power button on a short press or a long press.
- **Reading Orientation**: Set the screen orientation for reading, options are:
- "Portrait" (default) - Standard portrait orientation
- "Landscape CW" - Landscape, rotated clockwise
- "Inverted" - Portrait, upside down
- "Landscape CCW" - Landscape, rotated counter-clockwise
- **Front Button Layout**: Configure the order of the bottom edge buttons, options are:
- "Bck, Cnfrm, Lft, Rght" (default) - Back, Confirm, Left, Right
- "Lft, Rght, Bck, Cnfrm" - Left, Right, Back, Confirm
- "Lft, Bck, Cnfrm, Rght" - Left, Back, Confirm, Right
- **Side Button Layout**: Swap the order of the volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
- **Reader Font Family**: Choose the font used for reading, options are:
- "Bookerly" (default) - Amazon's reading 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", "Large", or "X Large".
- **Reader Line Spacing**: Adjust the spacing between lines, options are "Tight", "Normal", or "Wide".
- **Check for updates**: Check for firmware updates over WiFi.
### 3.6 Sleep Screen
@@ -74,7 +98,7 @@ You can customize the sleep screen by placing custom images in specific location
- **Single Image:** Place a file named `sleep.bmp` in the root directory.
- **Multiple Images:** Create a `sleep` directory in the root of the SD card and place any number of `.bmp` images
inside. If images are found in this directory, they will take priority over the `sleep.png` file, and one will be
inside. If images are found in this directory, they will take priority over the `sleep.bmp` file, and one will be
randomly selected each time the device sleeps.
> [!NOTE]
@@ -102,8 +126,9 @@ Once you have opened a book, the button layout changes to facilitate reading.
* **Previous Chapter:** Press and **hold** the **Left** (or **Volume Up**) button briefly, then release.
### System Navigation
* **Return to Home:** Press **Back** to close the book and return to the Book Selection screen.
* **Chapter Menu:** Press **Confirm** to open the Table of Contents/Chapter Selection screen.
* **Return to Book Selection:** Press **Back** to close the book and return to the **[Book Selection](#32-book-selection)** screen.
* **Return to Home:** Press and hold **Back** to close the book and return to the **[Home](#31-home-screen)** screen.
* **Chapter Menu:** Press **Confirm** to open the **[Table of Contents/Chapter Selection](#5-chapter-selection-screen)**.
---
@@ -123,5 +148,3 @@ Please note that this firmware is currently in active development. The following
are planned for future updates:
* **Images:** Embedded images in e-books will not render.
* **Text Formatting:** There are currently no settings to adjust font type, size, line spacing, or margins.
* **Rotation**: Different rotation options are not supported.
+17 -1
View File
@@ -1,3 +1,19 @@
#!/bin/bash
find src lib \( -name "*.c" -o -name "*.cpp" -o -name "*.h" -o -name "*.hpp" \) -exec clang-format -style=file -i {} +
GIT_LS_FILES_FLAGS=""
if [[ "$1" == "-g" ]]; then
GIT_LS_FILES_FLAGS="--modified"
fi
# --- Main Logic ---
# Format all files (or only modified files if -g is passed)
# Use 'git ls-files' to get a list of all files tracked by git:
# --modified: files tracked by git that have been modified (staged or unstaged)
# --exclude-standard: ignores files in .gitignore
# Additionally exclude files in 'lib/EpdFont/builtinFonts/' as they are script-generated.
git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \
| grep -E '\.(c|cpp|h|hpp)$' \
| grep -v -E '^lib/EpdFont/builtinFonts/' \
| xargs -r clang-format -style=file -i
+221
View File
@@ -0,0 +1,221 @@
# File Formats
## `book.bin`
### Version 3
ImHex Pattern:
```c++
import std.mem;
import std.string;
import std.core;
// === Configuration ===
#define EXPECTED_VERSION 3
#define MAX_STRING_LENGTH 65535
// === String Structure ===
struct String {
u32 length [[hidden, comment("String byte length")]];
if (length > MAX_STRING_LENGTH) {
std::warning(std::format("Unusually large string length: {} bytes", length));
}
char data[length] [[comment("UTF-8 string data")]];
} [[sealed, format("format_string"), comment("Length-prefixed UTF-8 string")]];
fn format_string(String s) {
return s.data;
};
// === Metadata Structure ===
struct Metadata {
String title [[comment("Book title")]];
String author [[comment("Book author")]];
String coverItemHref [[comment("Path to cover image")]];
String textReferenceHref [[comment("Path to guided first text reference")]];
} [[comment("Book metadata information")]];
// === Spine Entry Structure ===
struct SpineEntry {
String href [[comment("Resource path")]];
u32 cumulativeSize [[comment("Cumulative size in bytes"), color("FF6B6B")]];
s16 tocIndex [[comment("Index into TOC (-1 if none)"), color("4ECDC4")]];
} [[comment("Spine entry defining reading order")]];
// === TOC Entry Structure ===
struct TocEntry {
String title [[comment("Chapter/section title")]];
String href [[comment("Resource path")]];
String anchor [[comment("Fragment identifier")]];
u8 level [[comment("Nesting level (0-255)"), color("95E1D3")]];
s16 spineIndex [[comment("Index into spine (-1 if none)"), color("F38181")]];
} [[comment("Table of contents entry")]];
// === Book Bin Structure ===
struct BookBin {
// Header
u8 version [[comment("Format version"), color("FFD93D")]];
// Version validation
if (version != EXPECTED_VERSION) {
std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION));
}
u32 lutOffset [[comment("Offset to lookup tables"), color("6BCB77")]];
u16 spineCount [[comment("Number of spine entries"), color("4D96FF")]];
u16 tocCount [[comment("Number of TOC entries"), color("FF6B9D")]];
// Metadata section
Metadata metadata [[comment("Book metadata")]];
// Validate LUT offset alignment
u32 currentOffset = $;
if (currentOffset != lutOffset) {
std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset));
}
// Lookup Tables
u32 spineLut[spineCount] [[comment("Spine entry offsets"), color("4D96FF")]];
u32 tocLut[tocCount] [[comment("TOC entry offsets"), color("FF6B9D")]];
// Data Entries
SpineEntry spines[spineCount] [[comment("Spine entries (reading order)")]];
TocEntry toc[tocCount] [[comment("Table of contents entries")]];
};
// === File Parsing ===
BookBin book @ 0x00;
// Validate we've consumed the entire file
u32 fileSize = std::mem::size();
u32 parsedSize = $;
if (parsedSize != fileSize) {
std::warning(std::format("Unparsed data detected: {} bytes remaining at offset 0x{:X}", fileSize - parsedSize, parsedSize));
}
```
## `section.bin`
### Version 8
ImHex Pattern:
```c++
import std.mem;
import std.string;
import std.core;
// === Configuration ===
#define EXPECTED_VERSION 8
#define MAX_STRING_LENGTH 65535
// === String Structure ===
struct String {
u32 length [[hidden, comment("String byte length")]];
if (length > MAX_STRING_LENGTH) {
std::warning(std::format("Unusually large string length: {} bytes", length));
}
char data[length] [[comment("UTF-8 string data")]];
} [[sealed, format("format_string"), comment("Length-prefixed UTF-8 string")]];
fn format_string(String s) {
return s.data;
};
// === Page Structure ===
enum StorageType : u8 {
PageLine = 1
};
enum WordStyle : u8 {
REGULAR = 0,
BOLD = 1,
ITALIC = 2,
BOLD_ITALIC = 3
};
enum BlockStyle : u8 {
JUSTIFIED = 0,
LEFT_ALIGN = 1,
CENTER_ALIGN = 2,
RIGHT_ALIGN = 3,
};
struct PageLine {
s16 xPos;
s16 yPos;
u16 wordCount;
String words[wordCount];
u16 wordXPos[wordCount];
WordStyle wordStyle[wordCount];
BlockStyle blockStyle;
};
struct PageElement {
u8 pageElementType;
if (pageElementType == 1) {
PageLine pageLine [[inline]];
} else {
std::error(std::format("Unknown page element type: {}", pageElementType));
}
};
struct Page {
u16 elementCount;
PageElement elements[elementCount] [[inline]];
};
// === Section Bin Structure ===
struct SectionBin {
// Header
u8 version [[comment("Format version"), color("FFD93D")]];
// Version validation
if (version != EXPECTED_VERSION) {
std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION));
}
// Cache busting parameters
s32 fontId;
float lineCompression;
bool extraParagraphSpacing;
u16 viewportWidth;
u16 vieportHeight;
u16 pageCount;
u32 lutOffset;
Page page[pageCount];
// Validate LUT offset alignment
u32 currentOffset = $;
if (currentOffset != lutOffset) {
std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset));
}
// Lookup Tables
u32 lut[pageCount];
};
// === File Parsing ===
SectionBin book @ 0x00;
// Validate we've consumed the entire file
u32 fileSize = std::mem::size();
u32 parsedSize = $;
if (parsedSize != fileSize) {
std::warning(std::format("Unparsed data detected: {} bytes remaining at offset 0x{:X}", fileSize - parsedSize, parsedSize));
}
```
+5 -6
View File
@@ -2,8 +2,7 @@
#include <Utf8.h>
inline int min(const int a, const int b) { return a < b ? a : b; }
inline int max(const int a, const int b) { return a < b ? b : a; }
#include <algorithm>
void EpdFont::getTextBounds(const char* string, const int startX, const int startY, int* minX, int* minY, int* maxX,
int* maxY) const {
@@ -32,10 +31,10 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
continue;
}
*minX = min(*minX, cursorX + glyph->left);
*maxX = max(*maxX, cursorX + glyph->left + glyph->width);
*minY = min(*minY, cursorY + glyph->top - glyph->height);
*maxY = max(*maxY, cursorY + glyph->top);
*minX = std::min(*minX, cursorX + glyph->left);
*maxX = std::max(*maxX, cursorX + glyph->left + glyph->width);
*minY = std::min(*minY, cursorY + glyph->top - glyph->height);
*maxY = std::max(*maxY, cursorY + glyph->top);
cursorX += glyph->advanceX;
}
}
+5 -5
View File
@@ -1,6 +1,6 @@
#include "EpdFontFamily.h"
const EpdFont* EpdFontFamily::getFont(const EpdFontStyle style) const {
const EpdFont* EpdFontFamily::getFont(const Style style) const {
if (style == BOLD && bold) {
return bold;
}
@@ -22,16 +22,16 @@ const EpdFont* EpdFontFamily::getFont(const EpdFontStyle style) const {
return regular;
}
void EpdFontFamily::getTextDimensions(const char* string, int* w, int* h, const EpdFontStyle style) const {
void EpdFontFamily::getTextDimensions(const char* string, int* w, int* h, const Style style) const {
getFont(style)->getTextDimensions(string, w, h);
}
bool EpdFontFamily::hasPrintableChars(const char* string, const EpdFontStyle style) const {
bool EpdFontFamily::hasPrintableChars(const char* string, const Style style) const {
return getFont(style)->hasPrintableChars(string);
}
const EpdFontData* EpdFontFamily::getData(const EpdFontStyle style) const { return getFont(style)->data; }
const EpdFontData* EpdFontFamily::getData(const Style style) const { return getFont(style)->data; }
const EpdGlyph* EpdFontFamily::getGlyph(const uint32_t cp, const EpdFontStyle style) const {
const EpdGlyph* EpdFontFamily::getGlyph(const uint32_t cp, const Style style) const {
return getFont(style)->getGlyph(cp);
};
+14 -14
View File
@@ -1,24 +1,24 @@
#pragma once
#include "EpdFont.h"
enum EpdFontStyle { REGULAR, BOLD, ITALIC, BOLD_ITALIC };
class EpdFontFamily {
public:
enum Style : uint8_t { REGULAR = 0, BOLD = 1, ITALIC = 2, BOLD_ITALIC = 3 };
explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr,
const EpdFont* boldItalic = nullptr)
: regular(regular), bold(bold), italic(italic), boldItalic(boldItalic) {}
~EpdFontFamily() = default;
void getTextDimensions(const char* string, int* w, int* h, Style style = REGULAR) const;
bool hasPrintableChars(const char* string, Style style = REGULAR) const;
const EpdFontData* getData(Style style = REGULAR) const;
const EpdGlyph* getGlyph(uint32_t cp, Style style = REGULAR) const;
private:
const EpdFont* regular;
const EpdFont* bold;
const EpdFont* italic;
const EpdFont* boldItalic;
const EpdFont* getFont(EpdFontStyle style) const;
public:
explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr,
const EpdFont* boldItalic = nullptr)
: regular(regular), bold(bold), italic(italic), boldItalic(boldItalic) {}
~EpdFontFamily() = default;
void getTextDimensions(const char* string, int* w, int* h, EpdFontStyle style = REGULAR) const;
bool hasPrintableChars(const char* string, EpdFontStyle style = REGULAR) const;
const EpdFontData* getData(EpdFontStyle style = REGULAR) const;
const EpdGlyph* getGlyph(uint32_t cp, EpdFontStyle style = REGULAR) const;
const EpdFont* getFont(Style style) const;
};
+55
View File
@@ -0,0 +1,55 @@
#pragma once
#include <builtinFonts/bookerly_12_bold.h>
#include <builtinFonts/bookerly_12_bolditalic.h>
#include <builtinFonts/bookerly_12_italic.h>
#include <builtinFonts/bookerly_12_regular.h>
#include <builtinFonts/bookerly_14_bold.h>
#include <builtinFonts/bookerly_14_bolditalic.h>
#include <builtinFonts/bookerly_14_italic.h>
#include <builtinFonts/bookerly_14_regular.h>
#include <builtinFonts/bookerly_16_bold.h>
#include <builtinFonts/bookerly_16_bolditalic.h>
#include <builtinFonts/bookerly_16_italic.h>
#include <builtinFonts/bookerly_16_regular.h>
#include <builtinFonts/bookerly_18_bold.h>
#include <builtinFonts/bookerly_18_bolditalic.h>
#include <builtinFonts/bookerly_18_italic.h>
#include <builtinFonts/bookerly_18_regular.h>
#include <builtinFonts/notosans_8_regular.h>
#include <builtinFonts/notosans_12_bold.h>
#include <builtinFonts/notosans_12_bolditalic.h>
#include <builtinFonts/notosans_12_italic.h>
#include <builtinFonts/notosans_12_regular.h>
#include <builtinFonts/notosans_14_bold.h>
#include <builtinFonts/notosans_14_bolditalic.h>
#include <builtinFonts/notosans_14_italic.h>
#include <builtinFonts/notosans_14_regular.h>
#include <builtinFonts/notosans_16_bold.h>
#include <builtinFonts/notosans_16_bolditalic.h>
#include <builtinFonts/notosans_16_italic.h>
#include <builtinFonts/notosans_16_regular.h>
#include <builtinFonts/notosans_18_bold.h>
#include <builtinFonts/notosans_18_bolditalic.h>
#include <builtinFonts/notosans_18_italic.h>
#include <builtinFonts/notosans_18_regular.h>
#include <builtinFonts/opendyslexic_10_bold.h>
#include <builtinFonts/opendyslexic_10_bolditalic.h>
#include <builtinFonts/opendyslexic_10_italic.h>
#include <builtinFonts/opendyslexic_10_regular.h>
#include <builtinFonts/opendyslexic_12_bold.h>
#include <builtinFonts/opendyslexic_12_bolditalic.h>
#include <builtinFonts/opendyslexic_12_italic.h>
#include <builtinFonts/opendyslexic_12_regular.h>
#include <builtinFonts/opendyslexic_14_bold.h>
#include <builtinFonts/opendyslexic_14_bolditalic.h>
#include <builtinFonts/opendyslexic_14_italic.h>
#include <builtinFonts/opendyslexic_14_regular.h>
#include <builtinFonts/opendyslexic_8_bold.h>
#include <builtinFonts/opendyslexic_8_bolditalic.h>
#include <builtinFonts/opendyslexic_8_italic.h>
#include <builtinFonts/opendyslexic_8_regular.h>
#include <builtinFonts/ubuntu_10_bold.h>
#include <builtinFonts/ubuntu_10_regular.h>
#include <builtinFonts/ubuntu_12_bold.h>
#include <builtinFonts/ubuntu_12_regular.h>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More