2
0

qmp_drive.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. #!/usr/bin/env python3
  2. # Drive a running QEMU via its QMP unix socket to test the mjdev wayland desktop.
  3. # Screendumps boot frames, then reveals the bottom bar (pointer -> bottom edge) and the
  4. # control center (pointer -> right edge), dumping each, then quits the VM. PPM->PNG is
  5. # built in (no deps) so frames can be viewed with the Read tool.
  6. #
  7. # python3 qmp_drive.py /tmp/iso-test/qmp.sock [/out/dir]
  8. # Requires the VM launched with: -qmp unix:<sock>,server,nowait and a tablet:
  9. # -usb -device usb-tablet (absolute pointer; abs axes are 0..32767)
  10. import socket, json, sys, time, struct, zlib, os
  11. SOCK = sys.argv[1] if len(sys.argv) > 1 else "/tmp/iso-test/qmp.sock"
  12. OUT = sys.argv[2] if len(sys.argv) > 2 else "/tmp/iso-test"
  13. class QMP:
  14. def __init__(self, path):
  15. self.s = socket.socket(socket.AF_UNIX)
  16. for _ in range(120):
  17. try:
  18. self.s.connect(path); break
  19. except OSError:
  20. time.sleep(1)
  21. else:
  22. raise SystemExit("no QMP socket: " + path)
  23. self.f = self.s.makefile("rwb", buffering=0)
  24. self._read(); self.cmd("qmp_capabilities")
  25. def _read(self):
  26. while True:
  27. line = self.f.readline()
  28. if not line: raise EOFError("QMP closed")
  29. obj = json.loads(line)
  30. if "event" in obj: continue
  31. return obj
  32. def cmd(self, exe, **a):
  33. m = {"execute": exe}
  34. if a: m["arguments"] = a
  35. self.f.write((json.dumps(m) + "\n").encode()); return self._read()
  36. def dump(self, p): self.cmd("screendump", filename=p)
  37. def move(self, xf, yf):
  38. self.cmd("input-send-event", events=[
  39. {"type": "abs", "data": {"axis": "x", "value": int(xf*32767)}},
  40. {"type": "abs", "data": {"axis": "y", "value": int(yf*32767)}}])
  41. def ppm2png(ppm, png):
  42. d = open(ppm, "rb").read()
  43. if d[:2] != b"P6": raise ValueError("not P6")
  44. idx, vals = 2, []
  45. while len(vals) < 3:
  46. while d[idx] in b" \t\n\r": idx += 1
  47. s = idx
  48. while d[idx] not in b" \t\n\r": idx += 1
  49. vals.append(int(d[s:idx]))
  50. w, h, _ = vals; idx += 1
  51. raw = d[idx:idx+w*h*3]
  52. def ch(t, x): return struct.pack(">I", len(x)) + t + x + struct.pack(">I", zlib.crc32(t+x) & 0xffffffff)
  53. rows = bytearray()
  54. for y in range(h):
  55. rows.append(0); rows += raw[y*w*3:(y+1)*w*3]
  56. open(png, "wb").write(b"\x89PNG\r\n\x1a\n" + ch(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0))
  57. + ch(b"IDAT", zlib.compress(bytes(rows), 6)) + ch(b"IEND", b""))
  58. return w, h
  59. def variation(ppm):
  60. d = open(ppm, "rb").read(); body = d[15:15+60000]
  61. return (max(body) - min(body)) if body else 0
  62. def shot(q, name, t):
  63. ppm, png = f"{OUT}/{name}.ppm", f"{OUT}/{name}.png"
  64. q.dump(ppm)
  65. try:
  66. w, h = ppm2png(ppm, png)
  67. print(f"[{t:>4}s] {name}.png {w}x{h} var={variation(ppm)}", flush=True)
  68. except Exception as e:
  69. print(f"[{t:>4}s] {name}: {e}", flush=True)
  70. def main():
  71. os.makedirs(OUT, exist_ok=True)
  72. q = QMP(SOCK); print(">> QMP connected", flush=True)
  73. t0 = time.time(); el = lambda: int(time.time()-t0)
  74. for t in (60, 100, 140, 180, 220):
  75. while el() < t: time.sleep(1)
  76. shot(q, f"boot-{t:03d}s", el())
  77. q.move(0.5, 0.999); time.sleep(3); shot(q, "bottombar", el())
  78. q.move(0.999, 0.5); time.sleep(3); shot(q, "controlcenter", el())
  79. q.move(0.5, 0.5); time.sleep(2); shot(q, "final", el())
  80. try: q.cmd("quit")
  81. except Exception: pass
  82. print(">> done", flush=True)
  83. main()