qemu: display + touch scenario, ADR-0006, docs

Phase 4+5 of the device sim:

- Display + touch verified end-to-end: virtio-gpu at 720x720 (fbdev
  emulation) renders the real WardenOS dashboard from the static LVGL
  fbdev+evdev UI build (flare-edge qemu-vm-support tools/build-ui-vm.sh);
  QMP input-send-event taps the Metrics tab and qemu/tests/ui-shot.sh
  asserts the repaint from screendumps. Two load-bearing QEMU flags found
  and documented: -global virtio-mmio.force-legacy=false (gpu/input are
  VERSION_1-only) and the 200ms press hold (an instantaneous press+release
  lands inside one LVGL indev poll and never clicks).
- qemu/tests/qmp.py: minimal QMP client (screendump, tap, quit).
- stage-2 init starts warden-ui when present and fb0 exists.
- docs/decisions/0006-qemu-device-sim.md: virt-not-custom-board, the
  enters-at-kernel boundary, fragment policy, naming, consequences.
- docs/architecture.md: new section 7 (device emulation), order-of-work
  item 7; modbus cross-reference to the bridge.
- qemu/README.md: emulated-vs-not table, scenarios, gotchas, host/runner
  requirements. docs/ci-cd.md: runner needs one-time qemu-system-arm
  install (fail-closed smoke until then, [maintainer]-gated). Repo README updated.

Final sweep on this commit: shellcheck clean, bridge 7/7 tests, boot smoke
PASS, portal scenario PASS (check-in + fw pull + signed .wfw download),
ui-shot PASS (touch navigates to Metrics) — all under the final flags.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HUayid7W5w7jBdb9Rrj1K
This commit is contained in:
BFE Engineering
2026-08-29 20:16:36 -06:00
co-authored by Claude Fable 5
parent c7e06514ad
commit 13b7d063a2
9 changed files with 347 additions and 29 deletions
+10 -6
View File
@@ -12,12 +12,14 @@ the rest of the firmware: tested, benchmarked, reproducible, and honest about
what runs on real silicon versus what we simulate. what runs on real silicon versus what we simulate.
> Status: **bringup.** The hardware **simulator** and its tests, the RV1106 kernel > Status: **bringup.** The hardware **simulator** and its tests, the RV1106 kernel
> forward-port as a reviewable `patches/` series, the hermetic kernel build, and two > forward-port as a reviewable `patches/` series, the hermetic kernel build, two
> Tier-1 drivers at 100% MC/DC are all in and CI-green on `main`. What > Tier-1 drivers at 100% MC/DC, and the **QEMU device sim** (`qemu/`, ADR-0006:
> remains before this is on the production build path: installing the self-hosted > boots the real kernel + real userspace on `-M virt` — check-in/OTA against the
> kernel-build runner (see `docs/ci-cd.md`) and having flare-edge consume warden-sdk > mock portal, watchdog, RS485-to-sim bridge, 720x720 display + touch, all
> as a dependency — both [maintainer]-gated. Until then, flare-edge still builds firmware from > emulation-verified) are in. What remains before this is on the production build
> the vendored SDK + `sdk-patches/`. > path: qemu-system-arm on the kernel-build runner (see `docs/ci-cd.md` §5) and
> having flare-edge consume warden-sdk as a dependency — both [maintainer]-gated. Until
> then, flare-edge still builds firmware from the vendored SDK + `sdk-patches/`.
## Why a new SDK ## Why a new SDK
@@ -90,6 +92,8 @@ than duplicate each other.
``` ```
sim/ the hardware simulator (Rust): membus/devmem, HPMCU, CRU, Modbus, RGA, NPU. sim/ the hardware simulator (Rust): membus/devmem, HPMCU, CRU, Modbus, RGA, NPU.
qemu/ the device simulator (ADR-0006): QEMU -M virt boots the real kernel and
real userspace; A/B disk layout, RS485 bridge into sim/, scenario tests.
drivers/ our own hardened drivers + their seams (relays, freshness; more migrate in). drivers/ our own hardened drivers + their seams (relays, freshness; more migrate in).
patches/ the RV1106 kernel forward-port delta onto pristine linux-6.18.46 (subsystem-split). patches/ the RV1106 kernel forward-port delta onto pristine linux-6.18.46 (subsystem-split).
kernel/ forward-port docs + provenance (rv1106-enablement/, PROVENANCE.md). kernel/ forward-port docs + provenance (rv1106-enablement/, PROVENANCE.md).
+31 -2
View File
@@ -71,7 +71,9 @@ supervisor logic runs in CI with no panel.
(CRC16 byte-identical to the master, FC 0x010x06/0x0F/0x10/0x11, exception replies, (CRC16 byte-identical to the master, FC 0x010x06/0x0F/0x10/0x11, exception replies,
and fault injection — silent-drop and forced-NAK) so `warden-modbus`'s master can be and fault injection — silent-drop and forced-NAK) so `warden-modbus`'s master can be
hardened to MC/DC against realistic device behaviour with no serial hardware. MEI hardened to MC/DC against realistic device behaviour with no serial hardware. MEI
(0x2B/0x0E) identification is the documented follow-up. (0x2B/0x0E) identification is the documented follow-up. The same slave also serves
as the QEMU device sim's field bus: `qemu/rs485-bridge/` feeds it from a serial
chardev so the guest's real master polls it over what it believes is /dev/ttyS4 (§7).
- **`npu` — NPU load model.** Done. `NpuSim` models `/proc/rknpu/load` (the exact - **`npu` — NPU load model.** Done. `NpuSim` models `/proc/rknpu/load` (the exact
"NPU load: N%" text the sysmon reads) behind the path seam, so the load-readout UI "NPU load: N%" text the sysmon reads) behind the path seam, so the load-readout UI
is host-testable. NPU *compute* is explicitly out of scope — no inference runs here. is host-testable. NPU *compute* is explicitly out of scope — no inference runs here.
@@ -152,7 +154,31 @@ kernel move as one matched boot+oem image, never a partial reflash.
series honest against pristine 6.18.46; provenance is in `patches/README.md` and series honest against pristine 6.18.46; provenance is in `patches/README.md` and
`kernel/rv1106-enablement/`. `kernel/rv1106-enablement/`.
## 7. Order of work ## 7. Device emulation (`qemu/`) — see ADR-0006
The third simulator, deliberately not named "sim": a QEMU VM (`-M virt,highmem=off`,
one Cortex-A7, 256M — the RV1106G3's shape) that boots the real forward-ported
kernel and real userspace, entering at `-kernel zImage` because everything below
(BootROM, idblock/DDR-init, U-Boot, the BCB A/B machinery) is closed blobs plus
mask ROM. The canonical RV1106 zImage boots virt unmodified; an additive kconfig
fragment (`qemu/configs/virt.fragment` via `WARDEN_KCONFIG_FRAGMENT`) adds the
scenario devices (PCI serial for RS485, i6300esb watchdog, WireGuard,
virtio-gpu/input for the 720x720 UI). The virtio disk carries the device's exact
12-partition `blkdevparts=` A/B layout and the `/dev/block/by-name/` contract.
Where the seams meet: the guest runs the *real* binaries (static musl flared,
the LVGL fbdev UI); the RS485 bridge (`qemu/rs485-bridge/`) connects a QEMU
serial chardev to `sim/`'s `ModbusSlave`, so the register-level models serve as
the VM's field bus — behavior lives in one place, `sim/`, and the VM consumes
it. Scenario tests: `qemu/tests/boot-smoke.sh` (CI, in kernel-build),
`portal-scenario.sh` (check-in + OTA offer download against flare-edge's mock
portal), `ui-shot.sh` (QMP screendump + touch injection). Division of labour
with the other sims: NPU/RGA/HPMCU *behavior* stays `sim/`; UI *rendering
development* stays `lvglsim`; the VM is where processes, the kernel, and the
network meet. §5 still applies — no behavioural sim, this one included, catches
memory-map faults; and "boots under emulation" is never on-silicon evidence.
## 8. Order of work
1. **Simulator core**`membus`, `hpmcu`, the `cru` reset ladder, `modbus`, plus the 1. **Simulator core**`membus`, `hpmcu`, the `cru` reset ladder, `modbus`, plus the
`rga`/`npu` models. **Done.** `rga`/`npu` models. **Done.**
@@ -163,3 +189,6 @@ series honest against pristine 6.18.46; provenance is in `patches/README.md` and
4. **Config-lint CI gates** (§5) — the brick-class of bug. **Done.** 4. **Config-lint CI gates** (§5) — the brick-class of bug. **Done.**
5. **Hermetic kernel build** (`build/build-kernel.sh` + the `patches-apply` gate). **Done.** 5. **Hermetic kernel build** (`build/build-kernel.sh` + the `patches-apply` gate). **Done.**
6. **Kernel 5.10→6.18.46 forward-port** (§6, ADR-0001). **Done** (hardware-verified). 6. **Kernel 5.10→6.18.46 forward-port** (§6, ADR-0001). **Done** (hardware-verified).
7. **QEMU device sim** (§7, ADR-0006) — boot smoke, A/B disk harness, RS485 bridge,
portal/watchdog/clock scenarios, display+touch. **Done** (emulation-verified;
booting the real flare-edge rootfs+oem image pair is a documented later milestone).
+8
View File
@@ -45,6 +45,14 @@ A **third** repo-scoped runner instance on `bfe-mpc-0640` (alongside `flare` and
Host build deps: `dtc bc flex bison libssl-dev` — already present on 0640. Host build deps: `dtc bc flex bison libssl-dev` — already present on 0640.
5. **`qemu-system-arm`** (for the boot-smoke step inside `kernel-build`, added
with the `qemu/` device sim — ADR-0006): one-time
`sudo apt-get install qemu-system-arm cpio` on 0640. The step is
**fail-closed** — a missing qemu binary fails the job with a message
pointing here; it never silently skips. NOT YET PROVISIONED (2026-08-29):
the first dispatched `kernel-build` after this lands will fail its smoke
step until the install is done ([maintainer]-gated sudo on 0640).
## Badges ## Badges
Static shields SVGs are committed by the `badges` job (private repo can't use Static shields SVGs are committed by the `badges` job (private repo can't use
+58
View File
@@ -0,0 +1,58 @@
# ADR 0006 — QEMU device sim: generic `-M virt`, entered at the kernel
**Status:** Accepted (2026-08-29).
## Context
The two existing simulators cannot test the *device*: `lvglsim` (flare-edge)
is an SDL rendering harness, and `sim/` models registers behind driver seams.
Init ordering, the daemons as real processes, networking/enrollment against
FLARE, OTA, and the watchdog were testable only on a bench panel — flare-edge's
fault suite marks five scenarios "HIL, human prompts", its Playwright e2e needs
a live panel on the LAN, and its OTA desk test stops at "reached APPLYING".
Two ways to emulate the panel were considered:
1. **A custom RV1106 QEMU board model.** Nothing exists upstream or in the
community, so this means writing VOP/CRU/GRF/eMMC/HPMCU device models from
scratch and maintaining them against QEMU — months of work that duplicates
what `sim/` already models in Rust. It still could not run the boot chain:
BootROM is mask ROM and the DDR-init/idblock stages are closed rkbin blobs.
2. **The generic `-M virt` machine, entering at `-kernel zImage`.** The
forward-ported 6.18.46 config is multi_v7-derived and already carries
`ARCH_VIRT` plus the full virtio set — the canonical RV1106 zImage boots
virt **unmodified** (verified 2026-08-29). Peripherals become virtio
substitutes; SoC-block behavior stays in `sim/`, bridged in (the RS485
chardev bridge) rather than re-modeled.
## Decision
Option 2. `qemu/` holds the harness: `qemu-system-arm -M virt,highmem=off
-cpu cortex-a7 -smp 1 -m 256M` (the RV1106G3's shape), one canonical kernel
image plus an optional additive config fragment (`qemu/configs/virt.fragment`
via the `WARDEN_KCONFIG_FRAGMENT` hook — PCI/pci-serial/i6300esb/WireGuard/
virtio-gpu/virtio-input; the RV1106 build is byte-identical with the variable
unset). The VM carries the device's real 12-partition `blkdevparts=` A/B
layout on a virtio disk and populates the `/dev/block/by-name/` contract.
The name is `qemu/`, not any variant of "sim" — the wikis already warn that
"sim" is two different things.
Notable mechanics: `highmem=off` because the non-LPAE 32-bit kernel cannot
reach virt's default 40-bit PCIe ECAM; `-global virtio-mmio.force-legacy=false`
because virtio-gpu/input are VERSION_1-only devices.
## Consequences
- Everything below the kernel is **out of scope**: BootROM, idblock, U-Boot,
the real BCB-driven A/B selection and bootcount auto-revert. The initramfs
`warden.slot=` switch emulates U-Boot's *choice*, not the mechanism. The
untested A/B rollback chain stays bench territory.
- Display, input, network, and storage are **substitutes** (virtio), not
models. "Boots/works under emulation" is never evidence of "works on
silicon"; the VM narrows which claims need a panel.
- The VM is the first environment that runs production userspace binaries on
a non-RV1106 physical memory map, which makes it a canary for baked-in
hardware assumptions (it immediately found flare-edge #106, a fatal SIGBUS
in flared's HPMCU probe, and #107, a Y2038 time_t truncation in the UI).
- The guest deliberately deviates from production in documented ways
(`WARDEN_FLARE_INSECURE=1` for the desk mock portal, `WARDEN_HPMCU=0`);
qemu/README.md carries the emulated-vs-not table.
- The kernel-build CI job gains a fail-closed qemu boot smoke; hosted runners
build (but cannot boot) the initramfs and disk image.
+75 -21
View File
@@ -2,46 +2,100 @@
A QEMU virtual machine that boots the real forward-ported kernel (`build/` + A QEMU virtual machine that boots the real forward-ported kernel (`build/` +
`patches/`) and real userspace, so the *device* — init, daemons, networking, `patches/`) and real userspace, so the *device* — init, daemons, networking,
OTA, watchdog — can be tested off-hardware. The third simulator in the stack, OTA, watchdog, display — can be tested off-hardware. The third simulator in
deliberately not named "sim": the stack, deliberately not named "sim":
- `lvglsim` (flare-edge) — SDL desktop build of the UI. Rendering only. - `lvglsim` (flare-edge) — SDL desktop build of the UI. Rendering only.
- `sim/` (this repo) — register-level Rust models of RV1106 blocks behind - `sim/` (this repo) — register-level Rust models of RV1106 blocks behind
driver seams. driver seams.
- `qemu/` (this) — the whole machine above the kernel entry point. - `qemu/` (this) — the whole machine above the kernel entry point, running
the real binaries.
Decision record: `docs/decisions/0006-qemu-device-sim.md`.
## The boundary (read this before trusting a green run) ## The boundary (read this before trusting a green run)
There is no RV1106 machine model in QEMU and everything below the kernel is There is no RV1106 machine model in QEMU and everything below the kernel is
closed rkbin blobs plus mask ROM, so the VM **enters at `-kernel zImage`** on closed rkbin blobs plus mask ROM, so the VM **enters at `-kernel zImage`** on
`-M virt` (generic ARMv7 machine, virtio peripherals). That means: `-M virt,highmem=off` (single Cortex-A7, 256M — the RV1106G3's shape).
- **Not emulated, not tested here:** BootROM, idblock/DDR-init, SPL, U-Boot, | Emulated / substituted | Not emulated (stays bench / `sim/` territory) |
the real BCB-driven A/B slot selection, bootcount auto-revert. The untested |---|---|
A/B rollback chain stays untested by this tool. | Kernel boot, init ordering, switch_root | BootROM, idblock/DDR-init, SPL, U-Boot |
- **Substituted, not modeled:** display (virtio-gpu, not VOP), input | A/B *outcome* (`warden.slot=` cmdline) | Real BCB A/B selection, bootcount auto-revert |
(virtio-tablet, not GT911), network (virtio-net, not GMAC), storage | Storage: virtio-blk with the device's exact `blkdevparts=` layout + `/dev/block/by-name/` contract | eMMC controller itself |
(virtio-blk, not eMMC). NPU/RGA/HPMCU behavior stays `sim/` territory. | Network: virtio-net (slirp, hostfwd 22/80/28443) | GMAC, AIC8800 wifi, usb0 gadget |
- "Boots under emulation" is **not** "works on silicon". On-device claims | Display: virtio-gpu 720x720 via fbdev emulation | VOP/RGB666 pipeline, CH32V003 panel init, RGA blits |
still need on-device evidence; the VM narrows which claims need a panel. | Touch: virtio-tablet (QMP `input-send-event`) | GT911 on I2C3 |
| Watchdog: i6300esb (PCI), `-action watchdog=reset` | DW watchdog @0xff5a0000, HPMCU supervisor |
| RS485: pci-serial chardev bridged to `sim/`'s `ModbusSlave` | Real UART4 timing/electrical behavior |
| RTC: PL031 (`--rtc` reproduces the no-RTC 2021-clock incident class) | The unpopulated backup-cell reality |
**"Boots/works under emulation" is never evidence of "works on silicon."**
The VM narrows which claims need a panel; on-device claims still need
on-device evidence. Conversely, the VM is the first environment that runs
production binaries on a non-RV1106 memory map — it found flare-edge #106
(fatal SIGBUS in flared's HPMCU probe) and #107 (Y2038 time_t truncation)
on its first two boots of real userspace.
Documented guest deviations from production, set by stage-2 init:
`WARDEN_FLARE_INSECURE=1` (the desk mock portal is plain HTTP) and
`WARDEN_HPMCU=0` (no mailbox SRAM on virt; flared >= flare-edge#106 fix
required, or the daemon dies of SIGBUS).
## Quick start ## Quick start
```sh ```sh
# 1. build the QEMU kernel variant (canonical RV1106 build + virt fragment) # 1. kernel: canonical build boots the VM as-is; the fragment variant adds
# the scenario devices (PCI serial, watchdog, WireGuard, virtio-gpu/input)
WORK=$HOME/kbuild-out CROSS_COMPILE=arm-linux-gnueabihf- \ WORK=$HOME/kbuild-out CROSS_COMPILE=arm-linux-gnueabihf- \
WARDEN_KCONFIG_FRAGMENT=qemu/configs/virt.fragment bash build/build-kernel.sh WARDEN_KCONFIG_FRAGMENT=qemu/configs/virt.fragment bash build/build-kernel.sh
# 2. build the initramfs (sha256-pinned static busybox + qemu/rootfs/) # 2. initramfs (sha256-pinned static busybox + qemu/rootfs/) and A/B disk
bash qemu/mkinitramfs.sh bash qemu/mkinitramfs.sh
bash qemu/mkimage.sh # options: --portal-url --state K=V --fw-version
# 3. smoke it # 3. run (see run.sh header for all flags)
bash qemu/tests/boot-smoke.sh $HOME/kbuild-out/linux-6.18.46/arch/arm/boot/zImage bash qemu/run.sh --kernel $HOME/kbuild-out/linux-6.18.46/arch/arm/boot/zImage --shell
``` ```
Host requirements: `qemu-system-arm` (Debian 13 ships QEMU 10), `curl`, `cpio`, Payload: drop static musl armv7 binaries into `qemu/payload/` (see its
`gcc-arm-linux-gnueabihf` for the kernel build. README) — `warden-flared`, `warden-modbus`, and `warden-ui` (the LVGL
fbdev+evdev build from flare-edge `tools/build-ui-vm.sh`) are started by
stage-2 init when present.
Status: bringup — boot smoke only. Disk/network harness, RS485 bridge to ## Scenario tests (`qemu/tests/`)
`sim/`, portal scenarios, and display/touch land in later phases (see
`docs/decisions/0006-qemu-device-sim.md` once written). - `boot-smoke.sh <zImage>` — sentinel-asserting boot; runs in CI inside the
kernel-build job.
- `portal-scenario.sh <zImage>` (needs `FLARE_EDGE=<checkout>`) — the real
flared in the VM against the desk mock portal: authenticated check-in,
firmware desired-state pull, and download of a real signed tier-1 `.wfw`
offer. Verify/stage/APPLYING run as a dry run (no `WARDEN_FW_ALLOW_APPLY`);
flipping it on inside the VM is the documented stretch — apply writes
`/dev/block/by-name/rootfs_b` inside disk.img, then `--slot _b` boots it.
- `ui-shot.sh <zImage>` — display+touch: boots headless with virtio-gpu,
QMP-screendumps the 720x720 UI, taps the Metrics tab via `input-send-event`
(a 200 ms hold — an instantaneous press+release lands inside one LVGL poll
and never clicks), and asserts the frame changed. `qmp.py` is the tiny QMP
client.
- Watchdog: `run.sh --watchdog`, arm `/dev/watchdog` in the guest, don't pet —
the VM resets ~30 s later (verified). Do NOT combine with a flared payload
expecting survival: flared pets only while the UI heartbeat is fresh.
## Gotchas that cost time (so they cost it once)
- AF_UNIX socket paths cap at ~108 chars — keep `--rs485`/`--qmp` paths short.
- A serial port that is closed discards incoming bytes: hold ONE fd open
across write and read when scripting the guest side of the RS485 bridge.
- `highmem=off` and `-global virtio-mmio.force-legacy=false` are load-bearing
(32-bit ECAM reach; virtio-1-only gpu/input) — both live in run.sh.
- Never pass `earlyprintk`: DEBUG_UART_PHYS is the RV1106's 0xff4c0000.
## Host requirements
`qemu-system-arm` (Debian 13 ships QEMU 10), `curl`, `cpio`, `mkfs.ext4`,
`gcc-arm-linux-gnueabihf` (kernel build), `python3` (+`cryptography` for the
portal scenario's `.wfw` signing). CI: the hosted `qemu-tools` job builds the
tooling; the boot smoke runs on the self-hosted kernel-build runner, which
needs a one-time `apt-get install qemu-system-arm` (see docs/ci-cd.md).
+8
View File
@@ -77,6 +77,14 @@ for d in /usr/bin/warden-flared /usr/bin/warden-modbus; do
fi fi
done done
# The UI (LVGL fbdev+evdev build from flare-edge tools/build-ui-vm.sh) needs
# virtio-gpu's fbdev: present only with run.sh --display on|headless AND the
# virt.fragment kernel.
if [ -x /usr/bin/warden-ui ] && [ -c /dev/fb0 ]; then
echo "init: starting warden-ui (fbdev)"
/usr/bin/warden-ui > /tmp/warden-ui.log 2>&1 &
fi
if grep -qw warden.shell /proc/cmdline; then if grep -qw warden.shell /proc/cmdline; then
echo "warden.shell: interactive shell (exit powers off)" echo "warden.shell: interactive shell (exit powers off)"
setsid cttyhack sh setsid cttyhack sh
+3
View File
@@ -78,6 +78,9 @@ fi
APPEND="console=ttyAMA0 rdinit=/init" APPEND="console=ttyAMA0 rdinit=/init"
ARGS=( ARGS=(
-M "virt,highmem=off" -cpu cortex-a7 -smp 1 -m 256M -M "virt,highmem=off" -cpu cortex-a7 -smp 1 -m 256M
# virtio-mmio defaults to the legacy (0.9) transport; virtio-gpu and
# virtio-input are VERSION_1-only devices and never bind without this.
-global "virtio-mmio.force-legacy=false"
-kernel "$KERNEL" -initrd "$INITRD" -kernel "$KERNEL" -initrd "$INITRD"
-netdev "user,id=n0,hostfwd=tcp:127.0.0.1:${SSH_PORT}-:22,hostfwd=tcp:127.0.0.1:${HTTP_PORT}-:80,hostfwd=tcp:127.0.0.1:${API_PORT}-:28443" -netdev "user,id=n0,hostfwd=tcp:127.0.0.1:${SSH_PORT}-:22,hostfwd=tcp:127.0.0.1:${HTTP_PORT}-:80,hostfwd=tcp:127.0.0.1:${API_PORT}-:28443"
-device "virtio-net-device,netdev=n0" -device "virtio-net-device,netdev=n0"
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""Tiny QMP client for the qemu/ device-sim tests.
qmp.py <socket> screendump <out.ppm>
qmp.py <socket> tap <x> <y> # absolute 0..32767 (virtio-tablet)
qmp.py <socket> quit
"""
import json
import socket
import sys
import time
def rpc(sock, obj):
sock.sendall((json.dumps(obj) + "\n").encode())
while True:
line = sock_file.readline()
if not line:
raise RuntimeError("QMP connection closed")
msg = json.loads(line)
if "return" in msg:
return msg["return"]
if "error" in msg:
raise RuntimeError(f"QMP error: {msg['error']}")
# asynchronous events are interleaved; skip them
def main():
if len(sys.argv) < 3:
sys.exit(__doc__)
path, cmd = sys.argv[1], sys.argv[2]
global sock_file
s = socket.socket(socket.AF_UNIX)
s.connect(path)
sock_file = s.makefile("r")
sock_file.readline() # greeting banner
rpc(s, {"execute": "qmp_capabilities"})
if cmd == "screendump":
rpc(s, {"execute": "screendump", "arguments": {"filename": sys.argv[3]}})
elif cmd == "tap":
x, y = int(sys.argv[3]), int(sys.argv[4])
press = [
{"type": "abs", "data": {"axis": "x", "value": x}},
{"type": "abs", "data": {"axis": "y", "value": y}},
{"type": "btn", "data": {"down": True, "button": "left"}},
]
release = [{"type": "btn", "data": {"down": False, "button": "left"}}]
rpc(s, {"execute": "input-send-event", "arguments": {"events": press}})
# Hold the press across several LVGL indev poll periods (33 ms each):
# an instantaneous press+release lands inside one poll and no click
# is ever registered.
time.sleep(0.2)
rpc(s, {"execute": "input-send-event", "arguments": {"events": release}})
elif cmd == "quit":
s.sendall(b'{"execute":"quit"}\n')
else:
sys.exit(f"unknown command {cmd}")
if __name__ == "__main__":
main()
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# Display + touch scenario: boot the VM headless with virtio-gpu, wait for the
# LVGL UI (fbdev build) to start, screendump over QMP, inject an absolute
# touch tap (virtio-tablet), screendump again. Asserts the first frame is
# non-blank; reports (does not assert) whether the tap changed pixels — the
# device-level analogue of flare-edge's Xvfb/xdotool sim-test.sh.
#
# FAILS CLOSED on missing prerequisites.
#
# Usage: ui-shot.sh <zImage-virt> [out-dir]
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # qemu/tests/
QDIR="$(cd "$HERE/.." && pwd)" # qemu/
ZIMAGE="${1:-}"
OUTDIR="${2:-$QDIR/out}"
if [ -z "$ZIMAGE" ] || [ ! -f "$ZIMAGE" ]; then
echo "FATAL: usage: $0 <zImage> [out-dir] — the virt.fragment kernel variant" >&2
exit 1
fi
[ -x "$QDIR/payload/warden-ui" ] || {
echo "FATAL: no qemu/payload/warden-ui — build it with flare-edge tools/build-ui-vm.sh" >&2
exit 1
}
command -v qemu-system-arm >/dev/null || {
echo "FATAL: qemu-system-arm not on PATH — see qemu/README.md" >&2
exit 1
}
# Short-named scratch: AF_UNIX socket paths are capped at ~108 chars.
WORK="$(mktemp -d /tmp/wqu.XXXXXX)"
QEMU_PID=""
cleanup() {
if [ -n "$QEMU_PID" ]; then kill "$QEMU_PID" 2>/dev/null || true; fi
rm -rf "$WORK"
}
trap cleanup EXIT
bash "$QDIR/mkinitramfs.sh"
bash "$QDIR/mkimage.sh"
PORT=$((21000 + RANDOM % 20000))
bash "$QDIR/run.sh" --kernel "$ZIMAGE" --display headless --qmp "$WORK/qmp.sock" \
--ssh-port "$PORT" --http-port $((PORT + 1)) --api-port $((PORT + 2)) \
> "$WORK/console.log" 2>&1 &
QEMU_PID=$!
deadline=$((SECONDS + 120))
while [ $SECONDS -lt $deadline ]; do
grep -aq 'init: starting warden-ui' "$WORK/console.log" && break
kill -0 "$QEMU_PID" 2>/dev/null || { echo "FATAL: VM exited early" >&2; tail -25 "$WORK/console.log" >&2; exit 1; }
sleep 2
done
grep -aq 'init: starting warden-ui' "$WORK/console.log" || {
echo "FATAL: warden-ui never started (no fb0? wrong kernel?)" >&2
tail -25 "$WORK/console.log" >&2
exit 1
}
sleep 8 # let LVGL render the first frames
qmp() { python3 "$HERE/qmp.py" "$WORK/qmp.sock" "$@"; }
qmp screendump "$WORK/shot1.ppm"
# Tap the "Metrics" tab: pixel (373,40) of 720x720 scaled to the QMP absolute
# range 0..32767 — switching tabs must repaint the content area.
qmp tap 16975 1820
sleep 3
qmp screendump "$WORK/shot2.ppm"
mkdir -p "$OUTDIR"
cp "$WORK/shot1.ppm" "$OUTDIR/ui-shot1.ppm"
cp "$WORK/shot2.ppm" "$OUTDIR/ui-shot2.ppm"
# Non-blank: more than one distinct pixel value in the raw PPM payload.
python3 - "$WORK/shot1.ppm" <<'EOF'
import sys
data = open(sys.argv[1], "rb").read()
# P6 header: magic, dims, maxval, then raw RGB
parts = data.split(b"\n", 3)
pixels = parts[3] if len(parts) == 4 else b""
distinct = len(set(pixels[i:i+3] for i in range(0, min(len(pixels), 3*720*720), 3)))
print(f"shot1: {len(pixels)} bytes of pixels, {distinct} distinct colors")
sys.exit(0 if distinct > 1 else 1)
EOF
if cmp -s "$WORK/shot1.ppm" "$WORK/shot2.ppm"; then
echo "FATAL: tapping the Metrics tab did not change the frame — touch is not reaching the UI" >&2
exit 1
fi
echo "tap on the Metrics tab repainted the frame (touch reached the UI)"
echo "UI-SHOT-PASS (screenshots in $OUTDIR/ui-shot{1,2}.ppm)"