MicroWriter is a BLE-keyboard writing firmware for e-paper devices, plus a small patch system that pairs it, at build time, into a dual-boot bundle with an e-reader of your choice — sharing the device's flash and switching between the two from a menu entry, no reflashing required.
This repo holds:
editor/— the writer itself, built on MicroSlate (via a personal US-International fork). For the full feature list, keybindings, and usage guide, seeeditor/README.md— it carries this project's own fixes at the top, followed by the complete, unedited original MicroSlate README (Bluetooth keyboard support, note management, writing modes, WiFi sync, auto-save, etc.), preserved here on purpose rather than just linked to the upstream repo: if MicroSlate changes or drops functionality upstream that this project hasn't picked up, the reference for what's actually running here stays intact regardless. Its own independent firmware, ownplatformio.ini, builds and flashes on its own.patches/— one subdirectory per supported reader (CrossPoint, CrossInk, CPR-vCodex). Each is a set of scripts that edit a checkout of that reader — provided by you, not carried in this repo — to add the dual-boot "call to the editor" and protect the reader's own firmware self-update from overwriting the editor's OTA slot.
There's no reader source in this tree. You pick a reader, point the
patch scripts at your own checkout of it, build both, and merge the two
resulting binaries into one flashable image. See patches/<reader>/README.md
for the exact steps and the upstream tag each was last verified against.
Stable release: MicroWriter 0.3. First release under this
structure — see editor/README.md for the editor's own changelog
(PgUp/PgDn, the browser file manager, mDNS/OTA rename to "MicroWriter").
The patches/ scripts themselves are new this version; see NOTICE.md
for exactly what changed and why.
This project first tried a single integrated firmware (reader + writer + BLE keyboard host in one binary, one input model). Hands-on testing found that NimBLE's own runtime heap footprint (~90-100KB, invisible to static size analysis) starves the reader's heap whenever it's initialized, causing intermittent crashes, broken cover thumbnails, and full page-render failures. Deferring BLE init to only run while the writer was open worked, but at that point the writer no longer needed anything from the reader's own codebase — so this settled on the dual-boot split instead: two independent, purpose-built firmwares, each free to use its full heap, switching over a warm reboot (a few seconds).
All three supported readers share the same ota_0/ota_1 partition
layout the editor uses (same CrossPoint-lineage ancestry), and each
already ships (or, for CPR-vCodex, easily reuses) the low-level
otadata-write primitive needed to switch boot partitions on X4-class
hardware — esp_ota_set_boot_partition() itself fails there with a bogus
efuse-blk-rev verification error, so both sides write the otadata
partition directly instead, same trick the web flasher uses.
On top of that, every patches/<reader>/ set injects an OtaApps.h
(ported from the editor's own dual-boot code, itself adapted from
CrossInk's) that does three things:
registerOtaAppName(...)— each firmware writes its own display name to shared NVS (ota_names), keyed by which OTA slot it's running from, at boot.detectOtaApps(...)— scans the other OTA slot(s) and reads back whatever name is registered there, so a menu can show "MicroWriter" (or whichever reader's name) instead of a generic "OTA Slot N".switchToOtaApp(...)— writes the target slot into otadata and callsesp_restart().
Each patch set also adds a self-update guard: before that reader's own
firmware self-update writes anything, it compares the next-update
partition's embedded app descriptor against the running app's own and
refuses if they differ — protecting the editor's slot from being silently
overwritten by an unrelated firmware update. See NOTICE.md for how this
guard turned out to be the same literal patch across all three readers.
- Editor + patch system split from the old fixed reader+editor bundle
- Dual-boot call + self-update guard for CrossPoint, CrossInk, and CPR-vCodex
- Icons for the reader-switch shortcuts (currently placeholder text)
- Port to other e-paper hardware (Paper S3, LilyGO T5S3, X4 Pro) — the editor/patch split exists specifically to make this a matter of new build tags, not a new fork
- Third boot slot: a Game Boy emulator, driven by physical buttons only, no BLE — needs a 3-slot partition table, deferred
Verified so far on the Xteink X4 — ESP32-C3, 380KB RAM (no PSRAM), 800×480 1-bit e-ink, 5-way d-pad + power button, BLE 5.0, SD card.
# Editor
cd editor
IDF_COMPONENT_MANAGER=0 pio run -e xteink_x4
pio run -e xteink_x4 -t uploadFor a reader, pick one of patches/crosspoint/ (CrossPoint 1.4.1),
patches/crosspoint-1.5.0/ (CrossPoint v1.5.0, current stable),
patches/crossink/, or patches/cpr-vcodex/ and follow that directory's
own README.md — it clones the upstream reader at a known-good tag,
applies the patch scripts, and builds it.
To flash a complete dual-boot image from scratch:
esptool.py --chip esp32c3 merge_bin \
--flash_mode dio --flash_size 16MB \
-o dualboot-full.bin \
0x0 path/to/reader/bootloader.bin \
0x8000 path/to/reader/partitions.bin \
0xe000 path/to/boot_app0.bin \
0x10000 path/to/reader/firmware.bin \
0x650000 path/to/editor/firmware.bin
esptool.py --chip esp32c3 write_flash 0x0 dualboot-full.binUpdating a device that already has dual-boot on it: don't repeat the
full-image flash above — it rewrites boot_app0.bin at 0xe000, which
resets the OTA slot selector (otadata, same offset range) back to the
reader every time, discarding whichever firmware was actually active.
Write just the one slot that changed instead — reader at 0x10000,
editor at 0x650000 — which leaves otadata untouched and the device
keeps booting/waking into whatever was last active:
esptool.py --chip esp32c3 write_flash 0x650000 path/to/editor/firmware.bin
# or, updating the reader instead:
esptool.py --chip esp32c3 write_flash 0x10000 path/to/reader/firmware.binDon't want a reader at all — just the writer, standalone, on its own
device? Run the "Build MicroWriter Standalone" workflow from the
Actions tab (workflow_dispatch, no inputs required) — it builds
editor/ alone and publishes a ready-to-flash release, no dual-boot, no
reader pairing.
MIT — see LICENSE. Third-party attribution in NOTICE.md. Full project history and session-continuity notes (why things are structured this way, what's verified vs. not, ideas not yet acted on) in docs/DEVELOPMENT_LOG.md.