Files
evan b22934fe0d Replace vvfat SD with browser-built raw FAT32 from OPFS
Browser SD files are persisted in OPFS (web/sdstore.js) and edited in the
Files tab. At boot the browser builds a deterministic 64 MiB FAT32 image
(web/fat32.js) with nested directories and VFAT long names, then mounts
it as a raw block device, bypassing QEMU's broken populated-vvfat WASM
coroutine path. Guest writes mutate only the ephemeral image.

Exposes a hidden #frame-generation indicator that increments on every
e-ink flush, so automation can wait on real display updates instead of
fixed sleeps. Adds tests/ with a Makefile runner: 'make test' for JS +
FAT32/mtools checks, 'make browser-test' for a headless-Firefox boot of
the official CrossPoint 1.4.1 firmware that navigates to
Test/example.txt. Bumps third_party/qemu to the SPI-SD/CMD13 fixes and
updates README/ROADMAP/ISSUES for the new architecture.
2026-07-21 07:43:07 -04:00

6.3 KiB
Raw Permalink Blame History

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) OPFS files → browser-built 64 MiB FAT32 image → raw MEMFS block device
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 live input-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 from chip/eink-x3.chip.c.
  • ssi-sd fix — 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.
  • xteink machinevariant=x3|x4 controls 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

  1. 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 use scripts/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_SD in hw/sd/ssi-sd.c or add an info_report in xteink_x3_eink.c (both off by default in the patch).