#!/usr/bin/env python3 """What does Ⓑ do on the MAIN MENU, and then on the TITLE? `menu-navigation-semantics.md` has those two rows at 🟡 with **empty evidence cells** — "goes to the title, which re-draws PRESS Ⓐ after a beat" and "nothing" — while `Ⓑ on a submenu` is 4/4 measured. They are what the port still authors as `on_cancel`. Everything here is the harness validated in `tbm-submenu-not-reached.md`: the plate-pulse title detector, the glyph-327 menu detector, **delivery confirmed from `[RE-INPUT]` rather than from the pad**, and change detected rather than timed. Ⓑ is `kXInputPadB = 0x5801` (`ui/virtual_key.h:323`). b_from_menu.py LOG OUTDIR [wait_s] """ import os import re import subprocess import sys import time import numpy as np from PIL import Image LOG, OUT = sys.argv[1], sys.argv[2] WAIT = float(sys.argv[3]) if len(sys.argv) > 3 else 480 W, H = 1280, 720 NEED, CEIL, HOLD = 500, 2500, 12 MENU_LO, MENU_HI, MENU_HOLD = 250, 420, 6 PAD = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pad.py") def deliveries(vk): pat = re.compile((r"RE-INPUT\] XamInputGetKeystrokeEx -> user=\d+ vk=%s flags=0001" % vk).encode()) try: return len(pat.findall(open(LOG, "rb").read())) except FileNotFoundError: return 0 def press(btn, vk, tries=5): for k in range(tries): before = deliveries(vk) subprocess.run([sys.executable, PAD, "tap", btn, "0.5"], check=False) for _ in range(20): time.sleep(0.25) if deliveries(vk) > before: print(f"[{time.time()-T0:7.1f}s] {btn} delivered (attempt {k+1})", flush=True) return True print(f"[{time.time()-T0:7.1f}s] {btn} NOT delivered (attempt {k+1})", flush=True) return False def _open(): return subprocess.Popen( ["ffmpeg", "-loglevel", "error", "-f", "x11grab", "-draw_mouse", "0", "-video_size", f"{W}x{H}", "-i", ":98", "-r", "4", "-f", "rawvideo", "-pix_fmt", "rgb24", "-"], stdout=subprocess.PIPE, bufsize=W * H * 3 * 2) def glyph(a): r, g, b = a[:, :, 0], a[:, :, 1], a[:, :, 2] return int(((g > 130) & (g - r > 45) & (g - b > 45)).sum()) T0 = time.time() p, n, seg = _open(), W * H * 3, time.time() log = open(f"{OUT}/series.tsv", "w"); log.write("# t_s\tglyph\tmean\tphase\n") phase, streak, base, stable, mark = "wait", 0, None, 0, None while True: el = time.time() - T0 if el > WAIT: print(f"TIMEOUT in phase {phase}", flush=True); break if time.time() - seg > 30: p.kill(); p = _open(); seg = time.time() buf = p.stdout.read(n) if len(buf) < n: p.kill(); p = _open(); seg = time.time(); continue a = np.frombuffer(buf, np.uint8).reshape(H, W, 3).astype(int) c = glyph(a) log.write(f"{el:.3f}\t{c}\t{a.mean():.3f}\t{phase}\n"); log.flush() if phase == "wait": streak = streak + 1 if NEED <= c <= CEIL else 0 if streak >= HOLD: print(f"[{el:7.1f}s] TITLE (glyph {c})", flush=True) press("A", "5800"); phase, streak = "tomenu", 0 elif phase == "tomenu": streak = streak + 1 if MENU_LO <= c <= MENU_HI else 0 if streak >= MENU_HOLD: Image.fromarray(a.astype(np.uint8)).save(f"{OUT}/1-menu.png") base = a.astype(float) print(f"[{el:7.1f}s] MENU (glyph {c}) — pressing B", flush=True) time.sleep(2.0) press("B", "5801"); mark = time.time(); phase, stable = "afterB1", 0 elif phase == "afterB1": diff = float((np.abs(a - base).max(axis=2) > 12).mean()) if diff > 0.25: stable += 1 if stable >= 8: Image.fromarray(a.astype(np.uint8)).save(f"{OUT}/2-after-B-on-menu.png") print(f"[{el:7.1f}s] B ON MENU CHANGED THE SCREEN: {100*diff:.1f}% differ, glyph {c}", flush=True) base = a.astype(float); time.sleep(3.0) press("B", "5801"); mark = time.time(); phase, stable = "afterB2", 0 else: stable = 0 if time.time() - mark > 30: Image.fromarray(a.astype(np.uint8)).save(f"{OUT}/2-after-B-on-menu.png") print(f"[{el:7.1f}s] B ON MENU: NO CHANGE in 30 s ({100*diff:.1f}% differ, glyph {c})", flush=True) base = a.astype(float); press("B", "5801"); mark = time.time(); phase, stable = "afterB2", 0 elif phase == "afterB2": diff = float((np.abs(a - base).max(axis=2) > 12).mean()) if diff > 0.25: stable += 1 if stable >= 8: Image.fromarray(a.astype(np.uint8)).save(f"{OUT}/3-after-B-again.png") print(f"[{el:7.1f}s] SECOND B CHANGED THE SCREEN: {100*diff:.1f}% differ, glyph {c}", flush=True) break else: stable = 0 if time.time() - mark > 30: Image.fromarray(a.astype(np.uint8)).save(f"{OUT}/3-after-B-again.png") print(f"[{el:7.1f}s] SECOND B: NO CHANGE in 30 s ({100*diff:.1f}% differ, glyph {c})", flush=True) break p.kill(); log.close()