Model the ESP32-C3 I2C fingerprint path for stock firmware detection. Add a blank X4 panel stub, live GPIO inputs, and pin the exact QEMU revision.
6.1 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
Phase 1 (native foundation) is done for X3 and X4 selection. 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.
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 (phase 3) |
|---|---|---|
| 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) |
browser-supplied blockdev (File/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
- Compile the patched fork to WASM — combine Espressif's SoC models with
qemu-wasm's WASM TCG/TCI backend; build
qemu-system-riscv32with Emscripten. - Browser front-end (static) — uploaded flash, canvas, physical buttons, SD block backend, and pre-boot X3/X4 picker. Fully airgapped.
- 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 machine + device models (applied onto the pinned QEMU commit) |
chip/eink-x3.chip.c |
legacy Wokwi model — the reverse-engineered X3 e-ink protocol reference |
web/ |
(phase 3) static front-end: upload + canvas + 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).