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
63 lines
2.0 KiB
Python
Executable File
63 lines
2.0 KiB
Python
Executable File
#!/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()
|