From 0b2f004ad04bbe1f4cab266741a6170864547457 Mon Sep 17 00:00:00 2001 From: "Claude (auto-RE)" Date: Thu, 13 Aug 2026 21:37:44 +0000 Subject: [PATCH] re(flight): port the roll probe to the file pad, and drive fly_stage.sh to flight roll_axis.py was written last session to re-measure roll ABOUT THE FORWARD AXIS -- the fix for the withdrawn result, whose defect was the MEASUREMENT (a non-forward matrix row sees any rotation that moves it, and pitch moves it as much as roll) and not the input device. It was never run, and it still drove vgamepad, which is retired for leaking to the host. Ported to the file pad. The important difference is not the device but the shape: the file pad is a SNAPSHOT, not independent channels -- each write replaces the whole state -- so holding a trigger while deflecting a stick has to be one write. pad_state(**kw) does that. The values are also exact rather than whatever a virtual stick quantises to, which is what makes a single-axis hold (lx=32767 with everything else exactly 0) trustworthy for an axis-separation question. fly_stage.sh now drives the rest of the route it previously stopped short of: briefing (A: Continue) -> READY ROOM -> up to TAKE OFF -> flight, and notes why it must get there at all (a snapshot at the briefing yields zero unit-definition objects; they are instantiated at stage load proper). --- tools/re-capture/fly_stage.sh | 20 ++++++++++++++------ tools/re-capture/roll_axis.py | 35 ++++++++++++++++++++++++++++------- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/tools/re-capture/fly_stage.sh b/tools/re-capture/fly_stage.sh index 91f657d..e84cf12 100755 --- a/tools/re-capture/fly_stage.sh +++ b/tools/re-capture/fly_stage.sh @@ -76,12 +76,20 @@ sleep 1; shot 03-selected pad tap A 0.20; sleep 3; shot 04-after-A pad tap A 0.20; sleep 3; shot 05-after-A2 -say "waiting for flight (up to 240s) — snapshotting when the guest settles" -for i in $(seq 1 240); do - sleep 1 - [ $((i % 30)) -eq 0 ] && shot "06-wait-$i" -done -shot 07-final +# Selecting a stage lands on the mission BRIEFING (A: Continue), which leads to the +# READY ROOM (TAKE OFF / BRIEFINGS / HANGAR / ...) with BRIEFINGS pre-highlighted. +# A snapshot taken at the briefing yields ZERO unit-definition objects — they are +# instantiated at stage load proper — so the run has to reach flight. +say "clearing the briefing" +for _ in $(seq 1 10); do pad tap A 0.20; sleep 2.5; done +shot 06-readyroom +say "READY ROOM -> TAKE OFF (one up from the pre-highlighted BRIEFINGS)" +pad dpad up 0.06; sleep 1; shot 07-takeoff-hl +pad tap A 0.20; sleep 8 +for _ in $(seq 1 6); do pad tap A 0.20; sleep 4; done +say "waiting for flight to settle" +sleep 30 +shot 08-flight SHM=$(ls /dev/shm/xenia_memory_* 2>/dev/null | head -1) if [ -n "$SHM" ]; then OUT="$HOME/snap-stage$STAGE.bin" diff --git a/tools/re-capture/roll_axis.py b/tools/re-capture/roll_axis.py index 9315309..ff241b9 100644 --- a/tools/re-capture/roll_axis.py +++ b/tools/re-capture/roll_axis.py @@ -18,8 +18,27 @@ import json, math, os, struct, subprocess, sys, time sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import speed_law -def pad(*a): - subprocess.run(["vgamepad", *a], capture_output=True) +PAD_FILE = os.environ.get("XENIA_PAD_FILE", "/tmp/xenia_pad.txt") + + +def pad_state(**kw): + """Write the WHOLE pad state at once. + + The file pad is a snapshot, not a set of independent channels: each write + replaces everything, so holding a trigger *while* deflecting a stick has to be + one write. (`vgamepad`'s per-channel calls are what this replaces; that device + also leaked to the host, see the driver header.) Values are exact — no virtual + stick quantisation — which is what makes a single-axis roll hold trustworthy. + """ + parts = [f"{k}={v}" for k, v in kw.items() if v is not None] + tmp = PAD_FILE + ".tmp" + with open(tmp, "w") as f: + f.write(" ".join(parts)) + os.replace(tmp, PAD_FILE) + + +def pad_neutral(): + pad_state() def shot(n): subprocess.run(["screenshot", f"/sylph-home/re/shots/{n}.png"], capture_output=True) @@ -48,11 +67,13 @@ def main(): u_i, w_i = [r for r in (0, 1, 2) if r != f_i] rows = [] for label, (trig, tv) in (("slow", ("LT", 1.0)), ("fast", ("RT", 1.0))): - pad("trig", "RT", "0.0"); pad("trig", "LT", "0.0"); pad("axis", "LX", "0.0") - pad("trig", trig, str(tv)) + # settle at the target speed with the stick CENTRED (5 s: 2 s was shown + # wrong twice this session) + pad_state(**{trig.lower(): 255}) time.sleep(5.0) shot(f"rollax_{label}_a") - pad("axis", "LX", "1.0") + # full left-stick X, everything else exactly zero, trigger still held + pad_state(**{trig.lower(): 255, "lx": 32767}) t0, prev, swept = time.time(), rows_at(w.fd, off, cfg), 0.0 seq = [] while time.time() - t0 < dwell: @@ -63,13 +84,13 @@ def main(): swept += abs(d) seq.append((round(time.time() - t0, 3), round(d, 4))) prev = cur - pad("axis", "LX", "0.0") + pad_state(**{trig.lower(): 255}) wall = seq[-1][0] shot(f"rollax_{label}_b") rows += [(label, *s) for s in seq] print(f"# {label:<5} wall {wall:5.2f}s roll swept {swept:7.1f}deg " f"rate {swept / wall:6.1f} deg/wall-s") - pad("trig", "RT", "0.0"); pad("trig", "LT", "0.0"); pad("reset") + pad_neutral() with open(out_csv, "w") as f: f.write("phase,t,droll_deg\n") for r in rows: