Anki-style spaced-repetition flashcards for the Xteink X4 e-reader.
The X4 is a $69, 77-gram pocket e-reader with a 4.26" e-ink screen, an ESP32-C3, and — crucially — a row of four buttons along the bottom edge that map perfectly onto Anki's Again / Hard / Good / Easy. xteinki is a custom firmware that turns it into a dedicated, offline flashcard device:
- FSRS-6 scheduling — a faithful C++ port of py-fsrs 6.3.1, validated against 48 generated golden scenarios (224 reviews reproduced exactly: due times to the millisecond, stability/difficulty to 1e-9).
- Imports real Anki decks — drop a legacy-format
.apkgon the microSD card; the device extracts, renders, and schedules it on-device. - Progress that survives everything — scheduling state lives on the SD card keyed by Anki note identity, so firmware updates, deck re-exports, power loss, and reflashing never lose a review. An append-only review log doubles as crash recovery and as optimizer input.
- Paced like Anki — daily new-card cap (default 20/deck), learning steps, learn-ahead, next-interval previews under each grade button.
- Fully offline — WiFi is used for a few seconds once to learn the time (NTP), then never again. Weeks of battery.
Working end-to-end on hardware and in daily use: import, review, grading,
persistence, deep sleep, battery indicator. Current limitations: plain-text
cards (non-ASCII folds to closest ASCII, CJK shows as ?), no cloze notes
(skipped at import and counted), no media, max 4000 cards per deck, max 8
decks, single-level decks (one .apkg = one deck).
An Xteink X4 bought through official channels (xteink.com or Xteink's official Amazon store — sold as a "Developer Edition" with unlocked firmware). Grey-market units can be USB-locked and may be permanently bricked by third-party firmware — do not flash those. No soldering, no disassembly: flashing happens over USB-C.
⚠️ Flashing custom firmware voids the Xteink warranty. Before your first flash, back up the stock firmware (once, ~20 minutes):pip install esptool esptool.py --baud 921600 read_flash 0 0x1000000 stock-x4-backup.binRestore any time with
esptool.py write_flash 0 stock-x4-backup.bin, or reflash stock/CrossPoint via the CrossPoint web flasher.
-
Build & flash (requires PlatformIO):
pio run -t upload pio device monitor # optional: 115200 baud serial log -
Export a deck from Anki with "Support older Anki versions" checked (in the export dialog, format "Anki Deck Package"). The modern default format is refused with an on-screen message — it contains a zstd-compressed database this device can't read. Unchecking "Include media" makes the file smaller (media is skipped anyway).
-
Copy the
.apkginto/decks/on the microSD card. From macOS, avoid Finder's invisible sidecar files with:cp -X MyDeck.apkg /Volumes/SD_CARD/decks/ && dot_clean /Volumes/SD_CARD -
Create
/xteinki/config.txtso the device can learn the time (once):wifi_ssid YourNetwork wifi_pass YourPassword2.4GHz networks only (ESP32-C3 limitation). WiFi runs for a few seconds at cold boot for NTP, then powers off; the clock is persisted to SD after every review, so one successful sync lasts indefinitely.
-
Review. Boot lands on the deck menu; Confirm opens a deck. Any bottom button reveals the answer, then the four buttons grade Again / Hard / Good / Easy with next-interval previews. Side buttons scroll long cards. Short power press: back to the menu (grades are saved instantly — backing out never loses anything). Hold power 1s: deep sleep; hold 1s again: wake. Finishing a deck shows "All caught up!" with the next due time — decks refill as cards come due.
retention 0.9 # FSRS desired retention (0.5-1.0)
new_per_day 20 # new cards introduced per deck per day (0 = none)
tz_offset_hours -8 # shifts the daily rollover from UTC to your midnight
weights 0.212 1.2931 ... # 21 personalized FSRS weights (see below)
The built-in default weights are trained on hundreds of millions of reviews and work well. After ~1,000 of your own reviews, you can personalize:
python3 -m venv .venv && .venv/bin/pip install "fsrs[optimizer]==6.3.1"
.venv/bin/python tools/optimize_params.py /Volumes/SD_CARD/xteinki/*/revlog.jsonlIt prints a weights line to paste into params.txt. The optimizer (which
needs PyTorch) runs on your computer — the device only ever runs the
lightweight scheduler.
/decks/<name>.apkg your Anki exports (input)
/xteinki/<name>/cards.bin rendered card content (rebuilt on import)
/xteinki/<name>/state.bin FSRS memory per card — keyed by Anki note guid,
survives re-imports and firmware updates
/xteinki/<name>/revlog.jsonl append-only review history
/xteinki/<name>/daycount.txt today's new-card counter
/xteinki/params.txt scheduling configuration (optional)
/xteinki/config.txt WiFi credentials for time sync (optional)
/xteinki/clock.txt persisted clock (managed by the device)
Backing up your progress = copying /xteinki/ off the card.
pio run # firmware build (ESP32-C3)
pio test -e native # 20 host-side tests: FSRS golden
# vectors, import pipeline, stores
python tools/generate_fsrs_vectors.py # regen FSRS vectors (pip install fsrs==6.3.1)
python tools/make_test_apkg.py # regen import fixtures (pip install genanki)The scheduler, importer, and storage are portable C++ tested natively; only
src/ (UI, WiFi, power) is device-specific. tools/host_import.cpp builds
a host-side import harness (see file header) for debugging real decks under
AddressSanitizer.
| Function | Pins |
|---|---|
| E-ink (GDEQ0426T82 / SSD1677, SPI) | SCLK 8, MOSI 10, CS 21, DC 4, RST 5, BUSY 6 |
| microSD (same SPI bus) | CS 12, MISO 7 |
| Bottom buttons (resistor ladder, ADC) | GPIO1: Back/Confirm/Left/Right |
| Side buttons (resistor ladder, ADC) | GPIO2: Up/Down |
| Power button / battery / USB detect | GPIO3 / GPIO0 / GPIO20 |
ESP32-C3 ROM symbol trap (hard-won lesson): the C3's mask ROM contains
an ancient miniz, and the IDF ROM linker scripts hard-assign those symbol
names (tinfl_decompress etc.), silently replacing same-named functions in
your firmware with decade-old ROM code whose struct layouts differ —
corrupting memory at a distance. Our vendored
lib/miniz/miniz.h renames all 17 colliding symbols. If
you add a library defining symbols listed in esp32c3.rom*.ld, it will
suffer the same fate.
- The open-x4-epaper community for the X4 pin maps, sample firmware, and the vendored InputManager/BatteryMonitor (MIT), and the CrossPoint community for proving the X4 is a hackable platform.
- Open Spaced Repetition for FSRS and py-fsrs, the reference for our scheduler port.
- GxEPD2, Sqlite3Esp32, miniz, cJSON, genanki.
- Anki® is a trademark of Ankitects Pty Ltd. This project is not affiliated
with or endorsed by Ankitects; it reads the documented
.apkgformat via its own independent implementation.
MIT (see LICENSE). Vendored components keep their original licenses in their directories.