Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 8 additions & 27 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,37 +65,21 @@ jobs:
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high

build:
runs-on: ubuntu-latest
# Never run untrusted fork PR code on the self-hosted runner.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: [self-hosted, linux, crossink-build]
steps:
- uses: actions/checkout@v6
with:
submodules: recursive

- uses: actions/setup-python@v6
with:
python-version: "3.14"

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: true

- name: Cache PlatformIO packages
uses: actions/cache@v4
with:
path: ~/.platformio
key: ${{ runner.os }}-platformio-${{ hashFiles('platformio.ini', 'platformio.local.example.ini') }}
restore-keys: |
${{ runner.os }}-platformio-

- name: Install PlatformIO Core
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
- name: Verify PlatformIO Core
run: pio --version

- name: Build CrossInk
- name: Build CrossInk tiny
run: |
set -euo pipefail
pio run | tee pio.log
pio run -e tiny | tee pio.log

- name: Extract firmware stats
run: |
Expand All @@ -108,12 +92,9 @@ jobs:
- name: Upload firmware artifacts
uses: actions/upload-artifact@v7
with:
name: firmware
name: firmware-tiny
path: |
.pio/build/teensy/firmware-teensy.bin
.pio/build/tiny/firmware-tiny.bin
.pio/build/xlarge/firmware-xlarge.bin
.pio/build/no_emoji/firmware-no_emoji.bin
if-no-files-found: error

# This job is used as the PR required actions check, allows for changes to other steps in the future without breaking
Expand Down
44 changes: 26 additions & 18 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,41 @@
## [Unreleased]

### Added
- Added an adjustable reader line-height setting with percent-based spacing for EPUB and TXT books.
- Added a Recent Books long-press menu in both List and Grid views with delete, cache delete, completion, and remove-from-recents actions.
- Added a Minimal sleep screen option that shows the current book cover and reading progress on a dark background.
- Added aggregate all-time Reading Stats support from peer-synced per-device stats files.
- Added an in-reader confirmation message when a shortcut turns tilt-to-turn on or off.
- Added a 9pt `Itty Bitty` reader font size, plus build flags for omitting Itty Bitty and Large reader font assets in size-constrained firmware variants.

### Fixed
- Fixed Lyra Carousel popup rendering so loading, indexing, and sleep-entry popups appear in the right place again.
- Improved OPDS book download throughput by using a larger transfer buffer while keeping SD-card font downloads on the lower-memory path.
- Fixed OPDS feed errors so low-memory parser-buffer failures show the specific memory message instead of the generic parse error.
- Free the active SD-card reader font before opening OPDS catalogs so WiFi/feed parsing has more memory available.

### Changed

## [v1.3.0] - 2026-05-21

### Added
- Added Back/Cancel support while downloading books from OPDS catalogs.
- Added a Recent Books long-press menu in both List and Grid views with delete, cache delete, completion, and remove-from-recents actions.
- Added a Minimal sleep screen option that shows the current book cover and reading progress on a dark background.
- Added more detailed WiFi connection debug logs for scans, selected networks, status changes, disconnect reasons, and timeouts.
- Added a 9pt `Itty Bitty` reader font size, plus build flags for omitting Itty Bitty and Large reader font assets in size-constrained firmware variants.
- Added an in-reader confirmation message when a shortcut turns tilt-to-turn on or off.

### Fixed
- Fixed the in-reader Customise Status Bar screen in landscape so the list no longer extends under the button labels.
- Fixed manual WiFi connections from Settings returning immediately to the settings list after a saved-password or open-network connection succeeded, so the connected status and IP address are shown first.
- Fixed copied or corrupted saved password files being treated as valid credentials by validating device-specific password data before using it.
- Fixed missing Vietnamese labels for the sleep timeout resume settings.
- Fixed File Browser and Lyra Carousel icon alignment issues in icon-based themes.
- Reduced grid-like and over-zoomed artifacts on Lyra Carousel and Minimal theme's EPUB cover thumbnails by cropping normal covers before dithering while containing unusual cover ratios.
- Reduced duplicate Home progress/stat loading when returning from another screen.
- Fixed EPUB cache folder keys so they use a stable path hash across firmware builds, migrate older cache folders when possible, and rebuild stale section caches for the latest low-memory layout fixes.
- Improved low-memory EPUB handling by laying out very long text blocks earlier, streaming table fallback content when heap is tight, failing safely on large OPDS feeds instead of rebooting, and clarifying the warning text.
- Reduced sleep-entry memory and battery risk by reusing cached sleep-screen assets, idling OPDS pages normally after load, and putting the X3 tilt sensor back to sleep outside the reader.
- Improved network and SD-card font download reliability by disabling WiFi power saving during transfers, reducing WebDAV stack usage, tolerating longer stalls, retrying interrupted font files, and freeing active reader fonts when needed.
- Fixed a crash when opening the XTC chapter selector on memory-constrained builds.
- Fixed the Font Size setting to follow the actual sizes installed for the selected SD-card font family.
- Relaxed KOReader Sync auth response validation so compatible self-hosted servers that return valid JSON on successful login can authenticate.
- Fixed WiFi and OPDS connection-flow edge cases so manual Settings connections show the connected status first, copied or corrupted saved-password files are rejected before use, OPDS retries show loading before requests, and large OPDS feeds fail safely under low memory instead of rebooting.
- Fixed reader and Home UI polish issues, including landscape status-bar settings, missing Vietnamese labels, File Browser and Lyra Carousel icon alignment, cover thumbnail artifacts, and duplicate Home progress/stat loading.
- Fixed EPUB cache and low-memory handling by using stable cache folder keys, migrating older cache folders where possible, rebuilding stale section caches, laying out very long text blocks earlier, streaming table fallback content when heap is tight, and clarifying the warning text.
- Fixed sleep-entry, network, and SD-card font download reliability issues by reusing cached sleep-screen assets, idling OPDS pages normally after load, putting the X3 tilt sensor back to sleep outside the reader, disabling WiFi power saving during transfers, reducing WebDAV stack usage, tolerating longer stalls, retrying interrupted font files, and freeing active reader fonts when needed.
- Fixed remaining reader service edge cases, including an XTC chapter selector crash on memory-constrained builds, SD-card font size selection, SD-card font-size shortcuts skipping manually installed sizes, and KOReader Sync login compatibility with self-hosted servers that return valid JSON on success.

### Changed
- Moved the full-time page-as-sleep behavior into a new `Sleep Screen > Quick Resume` option, which also keeps `Quick Resume on Timeout` on, and renamed the timeout-only toggle.
- Moved the in-reader Footnotes shortcut above Select Chapter when footnotes are available on the current page.
- Made book titles in the file browser's long-press action menu smaller and allowed them to wrap to two lines so longer book names are easier to read.
- Reduced unnecessary screen refresh and list-clearing work during OPDS browsing and SD font downloads so transfers spend more time downloading and less time repainting progress.
- Modified upstream "page-as-sleep" behavior into a new `Sleep Screen > Quick Resume` option, which also keeps `Quick Resume on Timeout` on, and renamed the timeout-only toggle.
- Improved reader and browser menu behavior by moving the Footnotes shortcut above Select Chapter, wrapping long book titles in action menus, and reducing progress-screen repaint work during OPDS and SD font downloads.

## [v1.2.11.1] - 2026-05-15

Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ My goal with this fork was to maintain the core Crosspoint firmware while integr

- New reader fonts: ChareInk, Lexend Deca, and Bitter
- Unicode emoji and miscellaneous symbols support (a limited subset)
- Adjusted font sizes: Teensy (8pt), Tiny (10pt), Small (12pt), Medium (14pt), Large (16pt), Extra Large (18pt), Huge (20pt). See [Font Sizes](#font-sizes) for more details.
- Adjusted font sizes: Teensy (8pt), Itty Bitty (9pt), Tiny (10pt), Small (12pt), Medium (14pt), Large (16pt), Extra Large (18pt), Huge (20pt). See [Font Sizes](#font-sizes) for more details.
- Added ~~strikethrough~~ support
- Made <u>underlines</u> thicker for better visibility
- Added a custom `Minimal` theme and sleep screen option for the minimalists out there.
- Added support for `<hr>` section breaks
- Added support for "redaction" style rendering
- Added improved support for tables with simple markup
Expand Down Expand Up @@ -68,7 +69,7 @@ The UI now uses [Inter](https://fonts.google.com/specimen/Inter) as the display

### Font Sizes

There are 3 available build variants to choose from due to build size constraints: tiny, xlarge, and no_emoji
There are 4 available build variants to choose from due to build size constraints: tiny, xlarge, and no_emoji

**teensy**
> Only the small sized fonts.
Expand Down Expand Up @@ -189,6 +190,13 @@ Some simple per-book reading stats are tracked automatically and displayed in tw
- Average session time
- All time reading stats including total number of books read

To include all-time totals from other CrossInk devices, create or sync a
`.crosspoint/synced_stats/` folder between devices. When that folder exists,
each reader writes its own `device_<mac>.bin` contribution file and ignores that
file while summing the folder, so any device can display the aggregate total
without becoming the main device. If the folder is not present, the reader only
uses its local `global_stats.bin`.

**Home screen book card (Lyra theme only):**

- Total reading time
Expand Down Expand Up @@ -371,6 +379,7 @@ The structure is roughly:
.crosspoint/
├── global_stats.bin # All-time reading stats, including total books read
├── global_stats.bin.bak # Backup used if the main global stats file is corrupt
├── synced_stats/ # One per-device stats contribution file for aggregate all-time totals
├── settings.bin # Device settings
├── state.bin # Last-opened book and sleep/session state
├── recent.bin # Recent books list
Expand Down
64 changes: 40 additions & 24 deletions docs/catalog
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,64 @@
"schema_version": 1,
"releases": [
{
"id": "stable-1.2.11.1-tiny",
"id": "stable-1.3.0-teensy",
"channel": "stable",
"name": "1.2.11.1",
"version": "1.2.11.1",
"name": "1.3.0",
"version": "1.3.0",
"variant": "teensy",
"released_at": "2026-05-21T23:44:31Z",
"notes": "CrossInk 1.3.0 stable firmware",
"firmware_url": "https://github.com/uxjulia/CrossInk/releases/latest/download/firmware-teensy-v1.3.0.bin",
"firmware_sha256": "45df9a2839a25ec3f8fccf4955c321f4f3a5b09310fd68097bc6efcf27752047",
"size": 5569744,
"supported_devices": [
"x4",
"x3"
]
},
{
"id": "stable-1.3.0-tiny",
"channel": "stable",
"name": "1.3.0",
"version": "1.3.0",
"variant": "tiny",
"released_at": "2026-05-15T20:15:25Z",
"notes": "CrossInk 1.2.11.1 stable firmware",
"firmware_url": "https://github.com/uxjulia/CrossInk/releases/latest/download/firmware-tiny-v1.2.11.1.bin",
"firmware_sha256": "23f196428ac01e3fce78c17838914e7bb9969ae0d87540df60fc6a36abede506",
"size": 6491440,
"released_at": "2026-05-21T23:44:31Z",
"notes": "CrossInk 1.3.0 stable firmware",
"firmware_url": "https://github.com/uxjulia/CrossInk/releases/latest/download/firmware-tiny-v1.3.0.bin",
"firmware_sha256": "b4a046c56e5fbd7e1600499c2e97aa4311c727cebdbb3f8fd4c5a6bab3083b04",
"size": 6023264,
"supported_devices": [
"x4",
"x3"
]
},
{
"id": "stable-1.2.11.1-xlarge",
"id": "stable-1.3.0-xlarge",
"channel": "stable",
"name": "1.2.11.1",
"version": "1.2.11.1",
"name": "1.3.0",
"version": "1.3.0",
"variant": "xlarge",
"released_at": "2026-05-15T20:15:25Z",
"notes": "CrossInk 1.2.11.1 stable firmware",
"firmware_url": "https://github.com/uxjulia/CrossInk/releases/latest/download/firmware-xlarge-v1.2.11.1.bin",
"firmware_sha256": "49f9b58ed4d845a6fe30da32f47caef2bbff6dd47195e84df57f6b5a95c0239c",
"size": 5829952,
"released_at": "2026-05-21T23:44:31Z",
"notes": "CrossInk 1.3.0 stable firmware",
"firmware_url": "https://github.com/uxjulia/CrossInk/releases/latest/download/firmware-xlarge-v1.3.0.bin",
"firmware_sha256": "2e69f685728fcb7b9245171e53fb0c1dd5c1dc805634e9458c1f54a7cef8f7c6",
"size": 5892288,
"supported_devices": [
"x4",
"x3"
]
},
{
"id": "stable-1.2.11.1-no_emoji",
"id": "stable-1.3.0-no_emoji",
"channel": "stable",
"name": "1.2.11.1",
"version": "1.2.11.1",
"name": "1.3.0",
"version": "1.3.0",
"variant": "no_emoji",
"released_at": "2026-05-15T20:15:25Z",
"notes": "CrossInk 1.2.11.1 stable firmware",
"firmware_url": "https://github.com/uxjulia/CrossInk/releases/latest/download/firmware-no_emoji-v1.2.11.1.bin",
"firmware_sha256": "19a7997f4f636c3a56b52243c7290668b3b67d91ecf43525a2425ed22d1fb793",
"size": 6219440,
"released_at": "2026-05-21T23:44:31Z",
"notes": "CrossInk 1.3.0 stable firmware",
"firmware_url": "https://github.com/uxjulia/CrossInk/releases/latest/download/firmware-no_emoji-v1.3.0.bin",
"firmware_sha256": "9574828a5d36636aea1f4cf8ee67f263f13c4552f2893267bbf0e4c17c1010dd",
"size": 6281840,
"supported_devices": [
"x4",
"x3"
Expand Down
32 changes: 32 additions & 0 deletions docs/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,38 @@ nav_order: 8

# File Formats

## `global_stats.bin`

`/.crosspoint/global_stats.bin` stores this device's all-time reading counters.
If `/.crosspoint/synced_stats/` already exists, saves also mirror the same
counters to `/.crosspoint/synced_stats/device_<mac>.bin`, where `<mac>` is the
device's hardware MAC address without separators. The reader does not create
this folder on its own.

The `/.crosspoint/synced_stats/` directory is designed for peer-to-peer folder
sync: each device owns one contribution file, and display-only Reading Stats
views add every other device's contribution to this device's local
`global_stats.bin`. This device's own contribution file is skipped while
aggregating so mirroring the folder back to the same device does not double
count its local stats.

### Version 2

Adds `completedBooks` after the original counters.

```text
[0] version (= 2)
[1-4] totalSessions uint32 little-endian
[5-8] totalReadingSeconds uint32 little-endian
[9-12] totalPagesTurned uint32 little-endian
[13-16] completedBooks uint32 little-endian
```

### Version 1

Version 1 files are still readable. They are 13 bytes long and do not include
`completedBooks`, so the reader treats that value as zero.

## `book.bin`

### Version 6
Expand Down
1 change: 1 addition & 0 deletions lib/I18n/translations/english.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ STR_UNNAMED: "Unnamed"
STR_NO_SERVER_URL: "No server URL configured"
STR_FETCH_FEED_FAILED: "Failed to fetch feed"
STR_PARSE_FEED_FAILED: "Failed to parse feed"
STR_OPDS_FEED_BUFFER_MEMORY_ERROR: "Couldn't allocate memory for buffer"
STR_NEXT_PAGE: "Next Page »"
STR_PREV_PAGE: "« Previous Page"
STR_NETWORK_PREFIX: "Network: "
Expand Down
8 changes: 8 additions & 0 deletions lib/OpdsParser/OpdsParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ OpdsParser::OpdsParser(OpdsEntry* entries, const size_t entryCapacity)
: entries(entries), entryCapacity(entryCapacity) {
if (!entries || entryCapacity == 0) {
errorOccured = true;
errorReason = OpdsParserError::NO_ENTRY_BUFFER;
LOG_DBG("OPDS", "No entry buffer supplied");
}

Expand All @@ -27,6 +28,7 @@ size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) {
}
if (!xmlData && length > 0) {
errorOccured = true;
errorReason = OpdsParserError::INVALID_INPUT;
return length;
}

Expand All @@ -38,6 +40,7 @@ size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) {
void* const buf = XML_GetBuffer(parser, chunkSize);
if (!buf) {
errorOccured = true;
errorReason = OpdsParserError::BUFFER_MEMORY;
LOG_DBG("OPDS", "Couldn't allocate memory for buffer");
destroyXmlParser(parser);
parser = nullptr;
Expand All @@ -49,6 +52,7 @@ size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) {

if (XML_ParseBuffer(parser, static_cast<int>(toRead), 0) == XML_STATUS_ERROR) {
errorOccured = true;
errorReason = OpdsParserError::XML_PARSE;
LOG_DBG("OPDS", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
XML_ErrorString(XML_GetErrorCode(parser)));
destroyXmlParser(parser);
Expand All @@ -65,6 +69,7 @@ void OpdsParser::flush() {
if (!parser) return;
if (XML_Parse(parser, nullptr, 0, XML_TRUE) != XML_STATUS_OK) {
errorOccured = true;
errorReason = OpdsParserError::XML_PARSE;
destroyXmlParser(parser);
parser = nullptr;
}
Expand All @@ -74,6 +79,7 @@ bool OpdsParser::parse(const uint8_t* xmlData, const size_t length) {
clear();
if (!xmlData && length > 0) {
errorOccured = true;
errorReason = OpdsParserError::INVALID_INPUT;
return false;
}

Expand All @@ -96,6 +102,7 @@ void OpdsParser::clear() {
currentText.clear();
inEntry = inTitle = inAuthor = inAuthorName = inId = false;
errorOccured = !entries || entryCapacity == 0;
errorReason = errorOccured ? OpdsParserError::NO_ENTRY_BUFFER : OpdsParserError::NONE;
resetXmlParser();
}

Expand All @@ -110,6 +117,7 @@ bool OpdsParser::resetXmlParser() {
parser = XML_ParserCreate(nullptr);
if (!parser) {
errorOccured = true;
errorReason = OpdsParserError::PARSER_MEMORY;
LOG_DBG("OPDS", "Couldn't allocate memory for parser");
return false;
}
Expand Down
4 changes: 4 additions & 0 deletions lib/OpdsParser/OpdsParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ enum class OpdsEntryType {
BOOK // Downloadable book
};

enum class OpdsParserError { NONE, NO_ENTRY_BUFFER, INVALID_INPUT, PARSER_MEMORY, BUFFER_MEMORY, XML_PARSE };

/**
* Represents an entry from an OPDS feed (either a navigation link or a book).
*/
Expand Down Expand Up @@ -144,6 +146,7 @@ class OpdsParser final : public Print {
bool parse(const char* xmlData, size_t length) { return parse(reinterpret_cast<const uint8_t*>(xmlData), length); }

bool error() const;
OpdsParserError getErrorReason() const { return errorReason; }

operator bool() const { return !error(); }

Expand Down Expand Up @@ -189,5 +192,6 @@ class OpdsParser final : public Print {
bool inId = false;

bool errorOccured = false;
OpdsParserError errorReason = OpdsParserError::NONE;
bool truncated = false;
};
Loading
Loading