Open firmware for the Xteink X3 and X4 e-readers. Bare-metal Rust, no_std on
an ESP32-C3, no heap on the reading path, ~424 ms page turns on the X3 (379 ms
of which is the panel itself).
To look before you flash, try the emulator in your browser — the firmware's own app and rendering code compiled to WebAssembly, driving a simulated e-ink panel with a shelf of public-domain books.
- EPUB 2 & 3 — native table of contents for each (EPUB 3 nav, NCX fallback)
- Typography you control — Literata or Merriweather, plus an optional custom typeface from the SD card; three sizes, two weights, three line spacings
- Portrait or landscape — both landscape holds; front buttons swappable
- Whole-book pagination cache — paginates once, reopens in tens of ms; a first open publishes pages progressively so you can start reading immediately
- Fast page turns — ~45 ms of firmware work per turn; the rest is panel BUSY
- Refresh policy — fast only, clean on wake, or periodic clean pass
- Durable progress — two-generation writes so an interrupted flush never loses your place
- Streamed catalog — library size isn't bounded by RAM
- Local Wi-Fi shelf — upload, list, and delete books from your browser
- Zero-config onboarding — no stored credentials? the reader raises a WPA2 hotspot with a per-session password, captive portal, and QR code
- Per-book cache clearing — drop one book's cache without touching the book or anything else on the card
- Three ways in — the web flasher over USB, an SD-card image for units that shipped with USB flashing disabled in eFuse, or an in-app update from the card
- A recovery anchor — updates install to the far OTA slot and slot 0 stays pinned as the anchor, so holding Back + Up at reset boots back into it
- A wrong-board guard — the X3 and X4 are the same chip, so flashing one
board's image onto the other used to boot into a blank or scrambled screen
with no explanation. The device now detects the board at startup, stops, and
tries to write
BOARDID.TXTto the SD card naming the image to flash instead. With no card in the slot it says the same thing over serial
The X3 is the reference board — developed, bench-measured, and selected by
default in the emulator and web flasher. The X4 is fully supported;
tools/check.sh all covers both boards. The maintainer only has an X3, so X4
changes are host-verified.
The board is a compile-time feature (workspace default is X4); X3 commands carry
--features device-x3.
Newer production runs of both readers ship a different panel controller behind
the same glass and case — a UC8279d in place of the X3's UC8253, a UC8179 in
place of the X4's SSD1677 — and nothing on the outside tells you which one you
have. The reader asks the panel directly once per power-on — waking from sleep
reuses the answer — and writes it to /READER/PROBE.TXT on the card, so a unit
that renders wrong can be diagnosed without a serial cable. Drivers for the two
newer controllers are not written yet; a unit carrying one runs the existing
driver, exactly as it did before the check existed.
Install Rust with rustup, then the firmware target and the flashing tool:
rustup target add riscv32imc-unknown-none-elf wasm32-unknown-unknown
cargo install espflash
./tools/install-hooks.sh # optional: git hooks for local feedbacktools/cargo.sh run -p fw --release --features device-x3 # build, flash, serial monitor
tools/check.sh fast # fmt, clippy, host tests
tools/check.sh emulator # X3 + X4 golden frames
tools/check.sh all # complete required Rust/firmware verification, before a pull requestOnly flashing needs the device on USB; everything else builds and tests on a
plain host. Host-side Cargo commands need an explicit --target because the
workspace defaults to the firmware target.
The browser emulator builds one wasm per board:
tools/build-web.sh _site # X3 + X4 wasm + index.html into _site/
python3 -m http.server -d _site 8000 # http://localhost:8000 — add ?board=x4 for the X4.github/workflows/pages.yml publishes on push to main.
Hardware bench runs live in tools/bench. Run page-turn and sleep-sync
after display, input, sleep, or SD changes; save soak/storage runs for risky
merges and releases.
tools/bench/bench.py channel-stress --host # host-only concurrency checks
tools/bench/bench.py page-turn --port /dev/cu.usbmodem101 --turns 50tools/cargo.sh run flashes over USB. Tagged releases publish app and SD
images for units without a toolchain or with USB disabled;
docs/FLASHING.md covers all three paths.
- docs/ARCHITECTURE.md — tasks, ownership rules, memory budget, and the measured performance table
- docs/FLASHING.md — partition layout, release images, locked units, in-app updates, and the recovery hatch
- docs/CUSTOM_FONTS.md — building and installing a custom typeface
- AGENTS.md — the working contract for changes to this repository
- Jon-Vii's MarigoldOS, which CalendulaOS is a fork of
- Literata and Merriweather (both OFL) for the reading typefaces
- The OpenX4 community SDK for panel addressing behavior
- Crosspoint Reader for the community reverse-engineering behind X3 device support
MIT