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).
This commit is contained in:
2026-08-13 21:37:44 +00:00
parent faa7b3d611
commit 0b2f004ad0
2 changed files with 42 additions and 13 deletions

View File

@@ -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 04-after-A
pad tap A 0.20; sleep 3; shot 05-after-A2 pad tap A 0.20; sleep 3; shot 05-after-A2
say "waiting for flight (up to 240s) — snapshotting when the guest settles" # Selecting a stage lands on the mission BRIEFING (A: Continue), which leads to the
for i in $(seq 1 240); do # READY ROOM (TAKE OFF / BRIEFINGS / HANGAR / ...) with BRIEFINGS pre-highlighted.
sleep 1 # A snapshot taken at the briefing yields ZERO unit-definition objects — they are
[ $((i % 30)) -eq 0 ] && shot "06-wait-$i" # instantiated at stage load proper — so the run has to reach flight.
done say "clearing the briefing"
shot 07-final 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) SHM=$(ls /dev/shm/xenia_memory_* 2>/dev/null | head -1)
if [ -n "$SHM" ]; then if [ -n "$SHM" ]; then
OUT="$HOME/snap-stage$STAGE.bin" OUT="$HOME/snap-stage$STAGE.bin"

View File

@@ -18,8 +18,27 @@ import json, math, os, struct, subprocess, sys, time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import speed_law import speed_law
def pad(*a): PAD_FILE = os.environ.get("XENIA_PAD_FILE", "/tmp/xenia_pad.txt")
subprocess.run(["vgamepad", *a], capture_output=True)
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): def shot(n):
subprocess.run(["screenshot", f"/sylph-home/re/shots/{n}.png"], capture_output=True) 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] u_i, w_i = [r for r in (0, 1, 2) if r != f_i]
rows = [] rows = []
for label, (trig, tv) in (("slow", ("LT", 1.0)), ("fast", ("RT", 1.0))): 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") # settle at the target speed with the stick CENTRED (5 s: 2 s was shown
pad("trig", trig, str(tv)) # wrong twice this session)
pad_state(**{trig.lower(): 255})
time.sleep(5.0) time.sleep(5.0)
shot(f"rollax_{label}_a") 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 t0, prev, swept = time.time(), rows_at(w.fd, off, cfg), 0.0
seq = [] seq = []
while time.time() - t0 < dwell: while time.time() - t0 < dwell:
@@ -63,13 +84,13 @@ def main():
swept += abs(d) swept += abs(d)
seq.append((round(time.time() - t0, 3), round(d, 4))) seq.append((round(time.time() - t0, 3), round(d, 4)))
prev = cur prev = cur
pad("axis", "LX", "0.0") pad_state(**{trig.lower(): 255})
wall = seq[-1][0] wall = seq[-1][0]
shot(f"rollax_{label}_b") shot(f"rollax_{label}_b")
rows += [(label, *s) for s in seq] rows += [(label, *s) for s in seq]
print(f"# {label:<5} wall {wall:5.2f}s roll swept {swept:7.1f}deg " print(f"# {label:<5} wall {wall:5.2f}s roll swept {swept:7.1f}deg "
f"rate {swept / wall:6.1f} deg/wall-s") 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: with open(out_csv, "w") as f:
f.write("phase,t,droll_deg\n") f.write("phase,t,droll_deg\n")
for r in rows: for r in rows: