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

@@ -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: