An EPUB reader for the Xteink X4, written from scratch.
Built around one idea: do EPUB rendering really well and nothing else. No Wi-Fi, no Bluetooth, no sync. Just put EPUBs on the SD card and read them.
The first time you open a book there's a single conversion pass to a compact binary format. After that — changing chapters, adjusting font size, tweaking settings — everything is instant.
Features
- EPUB rendering with proportional fonts, bold/italic, and inline images
- Hyphenation via the Liang/TeX algorithm (EN, DE, FR, ES, IT, NL, PT, PL, RU)
- Fully configurable reader: font, size, line spacing, margins, justification
- Multiple built-in and SD card fonts — swap at runtime without reflashing
- Table of contents navigation with chapter progress display
- Book position saved and restored automatically per book
- Customizable sleep screen with looping support
- Single-pass EPUB →
.mrbconversion — fast cold open, instant everything after - Licensed under GPL v2
Warning
Requires an unlocked Xteink X4. Do not flash this on a locked device — you may be permanently stuck on that firmware.
Download the latest .bin from the Releases page.
Flash using the Crosspoint flash tool (browser-based, no install needed), or with esptool directly:
python -m esptool --chip esp32c3 --port COM5 --baud 921600 write_flash 0x0 microreader.binReplace COM5 with your device's port (/dev/ttyUSB0 on Linux, /dev/cu.usbserial-* on macOS). Hold BOOT while connecting if the device doesn't enter flash mode automatically.
Books (.epub) go anywhere on the SD card — the device scans recursively from the root. Fonts (.mfb) go in the fonts/ folder. Sleep images go in the .sleep/ folder.
You can copy files directly to the SD card, or transfer them over USB while the device is connected.
Open microreader-manager in Chrome, Edge, or Firefox (Web Serial API). Provides a file browser, EPUB/font/sleep-image upload, and auto-reconnects on page refresh.
Also includes a Font Generator for building .mfb fonts in the browser with live device preview, size presets, and script/range presets.
Install the plugin to send books directly from Calibre:
- In Calibre: Preferences → Plugins → Load plugin from file → select
tools/calibre-plugin/microreader.zip - Restart Calibre.
The device is detected automatically. Books on the device show checkmarks in the library; you can send and delete books from the Device menu.
Requires Calibre 5+ and the device connected over USB.
The device shows an image when it enters deep sleep. Several images are built into the firmware; add your own by placing BMP files in the .sleep/ folder on the SD card, or upload them via the browser manager.
Supported formats: 1 bpp monochrome, 4/8 bpp indexed, 16 bpp RGB565/BGR555, 24 bpp BGR, 32 bpp BGRA. Use 800×480 (landscape) or 480×800 for best quality — other sizes are scaled to fit.
The first time an image is shown it is converted and cached; subsequent sleeps load the cache directly. Clear the cache via Settings → Clear Cache.
Configure in Settings → Sleep Image: Auto cycles through all images in .sleep/; selecting a specific filename pins the device to that image.
| Tool | Purpose |
|---|---|
| CMake 3.5+ | Build system |
| SDL2 | Desktop emulator |
| Python 3 | Tools and scripts |
| PlatformIO | ESP32 firmware build and flash |
The desktop build runs the full reader UI in an SDL2 window on Windows, Linux, or macOS — no hardware needed. It uses an sd/ folder in the working directory as the virtual SD card: drop .epub files there to read them, .mfb fonts in sd/fonts/, and sleep images in sd/.sleep/.
cmake -S platforms/desktop -B build/desktop-debug -DCMAKE_BUILD_TYPE=Debug "-DCMAKE_POLICY_VERSION_MINIMUM:STRING=3.5"
cmake --build build/desktop-debug --config Debug
.\build\desktop-debug\Debug\microreader_desktop.exe# Build + flash
$env:USERPROFILE\.platformio\penv\Scripts\pio.exe run -t upload
# Serial monitor
$env:USERPROFILE\.platformio\penv\Scripts\pio.exe device monitor --baud 115200Upload baud: 921600.
cd test
cmake -B build2 -DCMAKE_BUILD_TYPE=Debug -DCMAKE_POLICY_VERSION_MINIMUM:STRING=3.5
cmake --build build2 --config Debug
.\build2\Debug\unit_tests.exe # fast (~375 tests, <1s)
.\build2\Debug\microreader_tests.exe # includes real EPUB integration tests# Terminal 1
python tools/run_qemu.py --with-books
# Terminal 2
python tools/test_books.py --port socket://localhost:4444 --pages 20 --delay 0.1lib/microreader/ shared core (platform-agnostic C++20)
content/ EPUB parsing, layout, MRB binary format
display/ Canvas, DisplayQueue, Font interfaces
screens/ UI screen implementations
platforms/desktop/ SDL2 emulator
platforms/esp32/ ESP-IDF + PlatformIO firmware
test/ Google Test suite
tools/ Python scripts and dev tools
calibre-plugin/ Calibre device plugin (build.ps1 → microreader.zip)
docs/ GitHub Pages — browser-based file manager (Web Serial API)
resources/ Fonts, sleep images
tools/serial_cmd.py talks to the device over USB serial using the same CMND protocol as the browser manager. It has two modes:
Non-interactive — pass flags and exit. Useful for scripting and CI:
python tools/serial_cmd.py --port COM5 --upload "book.epub" # upload an EPUB
python tools/serial_cmd.py --port COM5 --upload-dir "path/to/dir" # upload all EPUBs in a folder (skips duplicates)
python tools/serial_cmd.py --port COM5 --upload-sleep "image.bmp" # upload a sleep image
python tools/serial_cmd.py --port COM5 --upload-sd-font "font.mfb" # upload a font to SD card
python tools/serial_cmd.py --port COM5 --list # list books on deviceInteractive — a REPL for manual control:
python tools/serial_cmd.py --port COM5Interactive commands include open, rm, upload, btn (simulate button presses), test (open every book and watch for pass/fail), bench (conversion benchmark), and more. Type help once connected for the full list.
UI fonts (the bitmap fonts used for the device interface) are built from source using tools/generate_font.py and embedded as headers:
python tools/generate_font.py resources/fonts/terminus/Terminus-Bold.ttf 14 --header lib/microreader/display/ui_font_small.h --bw-only --ranges ui
python tools/generate_font.py resources/fonts/terminus/Terminus-Bold.ttf 18 --header lib/microreader/display/ui_font_medium.h --bw-only --ranges ui
python tools/generate_font.py resources/fonts/terminus/Terminus-Bold.ttf 24 --header lib/microreader/display/ui_font_large.h --bw-only --ranges ui
python tools/generate_font.py resources/fonts/terminus/Terminus-Bold.ttf 32 --header lib/microreader/display/ui_font_header.h --bw-only --ranges uiReader fonts (.mfb) are generated via the browser Font Generator in the microreader-manager. SD card fonts must fit within 3.375 MB (0x360000 bytes including the 4 KB header).
Uses the Liang/TeX algorithm with pattern files compiled into binary tries by Typst hypher. Supported languages: EN, DE, FR, ES, IT, NL, PT, PL, RU. Language is detected automatically from the EPUB's xml:lang attribute.
Trie data is embedded as constexpr byte arrays in lib/microreader/content/hyphenation/Liang/ — no heap allocation, no flash reads at runtime (placed in DROM on ESP32).
Adding a new language
- Download the
.binfrom typst/hypher/tries intotools/hyphenation/ - Generate the header:
python tools/generate_trie_header.py tools/hyphenation/<lang>.bin lib/microreader/content/hyphenation/Liang/hyph-<lang>.trie.h <lang> - Add the new enum value to
HyphenationLanginHyphenation.h - Add a
#include+caseinHyphenation.cpp(hyphenate_word) and anieqcheck indetect_language
The default X4 firmware uses two app partitions — app0 (at 0x10000) and app1 (at 0x650000). A small OTA data sector at 0xE000 records which one to boot. When you flash new firmware directly with esptool, the OTA data may still point to the other partition, causing the device to boot the old firmware — use switch_partition.py to fix that.
Replace COM5 with your port (/dev/ttyUSB0 on Linux, /dev/cu.usbserial-* on macOS).
# Full 16 MB flash
python -m esptool --chip esp32c3 --port COM5 read_flash 0x0 0x1000000 firmware_backup.bin
# app0 only (faster)
python -m esptool --chip esp32c3 --port COM5 read_flash 0x10000 0x640000 app0_backup.bin
# app1 only
python -m esptool --chip esp32c3 --port COM5 read_flash 0x650000 0x640000 app1_backup.bin# Full flash
python -m esptool --chip esp32c3 --port COM5 write_flash 0x0 firmware_backup.bin
# app0 only
python -m esptool --chip esp32c3 --port COM5 write_flash 0x10000 app0_backup.bin
# app1 only
python -m esptool --chip esp32c3 --port COM5 write_flash 0x650000 app1_backup.binpython tools/switch_partition.py app0 --port COM5 --flash
python tools/switch_partition.py app1 --port COM5 --flashThe plugin source lives in tools/calibre-plugin/. It bundles pyserial because Calibre's embedded Python doesn't include it.
cd tools/calibre-plugin
.\build.ps1 # packages __init__.py + serial/ into microreader.zip
# and copies it to %APPDATA%\calibre\plugins\Microreader.zip.\launch-debug.ps1 # equivalent to: calibre-debug -gAll print() calls in __init__.py appear in that terminal, prefixed with [Microreader].
& "C:\Program Files\Calibre2\calibre-debug.exe" -e test.pyOpens the serial port directly and verifies device detection and the CMND protocol without starting the full Calibre GUI.
GPL v2 — see LICENSE.






