6.5 KiB
xteink-web-emulator — architecture & roadmap
Goal: an offline, static-asset web page where you upload an xteink X3/X4
firmware .bin and it runs client-side — real ESP32-C3 machine code, with the
e-ink screen on a canvas, on-screen buttons, and a virtual SD card. No server,
no proprietary engine.
Why QEMU (and why not the alternatives)
| Approach | Runs any .bin |
Open source | Offline in browser |
|---|---|---|---|
| Wokwi | ✅ | ❌ (closed engine, licensed) | ❌ |
| Recompile firmware → WASM (crosspoint-simulator + Emscripten) | ❌ source only | ✅ | ✅ |
| QEMU (espressif fork) → WASM | ✅ | ✅ | ✅ |
Only QEMU satisfies all three. espressif/qemu models the ESP32-C3 SoC and
executes real firmware; ktock/qemu-wasm shows QEMU compiling to WASM and
running in-browser from static files. The endgame merges those two.
Status
The native foundation and WASM compile phase are done. Unmodified
canonical firmware
auto-detects xteink,variant=x3 through its real I²C fingerprint and boots the
X3 home screen with SD support. variant=x4 omits those chips, so the same
firmware selects its SSD1677 driver and sends its command stream to a blank X4
panel stub. Reproduce:
make qemu # clone espressif/qemu @ pinned commit, patch, build
make firmware sdimage # 16 MB flash.bin + FAT32 sd.img from a pio build
make run # X3 (default)
make run VARIANT=x4 # X4 detection + blank panel stub
QMP screenshots confirmed the X3 home screen and 528×792 orientation; captured
X4 SPI traffic confirmed the firmware selected its SSD1677 driver.
make qemu-wasm produces the Emscripten module, WASM binary, and pthread worker
under dist/wasm/; Node smoke checks confirm xteink is registered and its
browser framebuffer/input bridge initializes. make web serves a dependency-free
UI for firmware, canvas, buttons, logs, SD files, and debug metrics. Real
firmware boots in WASM and drives the X3 panel.
Device selection (X3 vs X4)
X3 and X4 share one ESP32-C3 binary and pick a profile at runtime via an I²C
fingerprint (BQ27220 gauge + DS3231 RTC + QMI8658 IMU on SDA20/SCL0), with an
NVS override (cphw/dev_ovr: 1=X4, 2=X3). The emulator picks X3 vs X4 at
launch with one machine property: -machine xteink,variant=x3|x4. X3 attaches
minimal BQ27220/DS3231/QMI8658 targets; X4 leaves the I²C bus empty. This drives
both firmware detection and panel wiring without modifying the firmware.
The interfaces (native now → browser later)
Each peripheral is modelled behind a QEMU-native seam that the WASM front-end reuses unchanged — no bespoke abstraction layer:
| Peripheral | Native seam | Browser binding |
|---|---|---|
| e-ink panel | X3 UC8253 (528×792) or blank X4 SSD1677 stub (480×800) → QEMU graphical console | <canvas> from the same display surface |
| SD card | ssi-sd + sd-card-spi backed by QEMU block layer (-drive if=sd) |
QEMU vvfat over a JS-created MEMFS directory; future persistence can use OPFS |
| Buttons + battery | ADC QOM props adci[1]/adci[2]; GPIO QOM input-level[3] for Power |
DOM buttons → qom-set equivalent |
| Firmware image | flash via -drive if=mtd |
uploaded .bin written to emulated flash |
Phase 1 device models (in qemu/patches/0001-xteink-machine.patch)
esp32c3.adc— SAR ADC: oneshot completes immediately; per-channel value is settable live via QOM (adci[*]). Feeds battery and the two resistor-ladder button groups. Replaced the original always-done register shim.esp32_gpio— real OUT/ENABLE/IN registers with 32 named input + output lines and liveinput-level[*]QOM controls. Outputs read high while a pin is input-configured (matches the board's pull-ups).ssi.esp32c3.spi2— the general-purpose SPI2 controller (CPU-buffer transactions on an SSI bus). DMA path deliberately unimplemented until needed.xteink-x3-eink— UC8253 panel: decodes DTM1/DTM2 planes (52 272 B each), drives BUSY, renders to a QEMU console. Protocol fromchip/eink-x3.chip.c.ssi-sdfix — track card idle state so CMD58 reports ready after ACMD41 (the 9.2 fork hardcoded idle, which SdFat rejects).esp32.i2c+ fingerprint targets — C3 opcode support and minimal BQ27220/DS3231/QMI8658 register responses for stock X3 detection.xteinkmachine —variant=x3|x4controls fingerprint presence and panel type; shared panel/SD wiring stays in one machine.- X4 panel stub — blank 480×800 console with X4 idle-low BUSY; accepts the SSD1677 command stream without implementing it.
Remaining phases
- Browser SD editing/persistence — add file controls and optionally OPFS; the current 32 GiB logical card is writable and ephemeral.
- Full X4 panel — implement SSD1677 RAM/window/update commands when visible X4 rendering is needed; the selection, dimensions, wiring, and stub exist.
Repo layout
| Path | Role |
|---|---|
flake.nix |
espressif-qemu release pkg (nix run .#run-upstream) + full QEMU build devShell |
Makefile |
qemu (source build), firmware, sdimage, run, chip |
scripts/build-qemu.sh |
clone pinned espressif/qemu, apply patch, build riscv32-softmmu |
qemu/patches/ |
xteink devices plus the qemu-wasm host port, applied onto pinned Espressif QEMU |
qemu/wasm/Dockerfile |
Minimal pinned Emscripten dependency environment |
scripts/build-qemu-wasm.sh |
X3/X4-only WASM build → dist/wasm/ |
chip/eink-x3.chip.c |
legacy Wokwi model — the reverse-engineered X3 e-ink protocol reference |
web/ |
Static frontend: firmware upload, canvas, variant, and buttons |
Debugging notes
- Headless capture: run with
-qmp unix:…and usescripts/qmp-screendump.py; the console is 528×792 (physical portrait), rotate not needed. - Inject a button:
scripts/qmp-qom.py <sock> set /machine/adc "adci[1]" 2760(ladder-1 midpoints: Back 3512 / Confirm 2694 / Left 1493 / Right ~5; ladder-2 Up 2242 / Down ~5). Value > 3900 = no button. - Symbolize a panic PC:
llvm-addr2line -f -C -e firmware.elf 0x4210d286. - Flash must be 16 MB — the partition table spans 16 MB even though the app fits under 4 MB. Espressif QEMU only accepts 2/4/8/16 MB mtd images.
- Trace SD/panel: flip
DEBUG_SSI_SDinhw/sd/ssi-sd.cor add aninfo_reportinxteink_x3_eink.c(both off by default in the patch).