Files
Syplheed-Reborn/tools/re-capture/roll_axis.py
Claude (auto-RE) 0b2f004ad0 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).
2026-08-13 21:37:44 +00:00

102 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""Roll rate measured ABOUT THE FORWARD AXIS, so pitch cannot leak into it.
The previous attempt watched a non-forward row of the rotation matrix and got
numbers within a few per cent of the pitch run — because pitch moves that row as
much as roll does. The fix is to measure the rotation *in the plane perpendicular
to forward*: express the new up-vector in the OLD (up, right) basis and take
`atan2(u_new·w_old, u_new·u_old)`. Any component along forward — which is what
pitch produces — is dropped by construction.
Each phase is bracketed by HUD screenshots so the mission clock converts wall
seconds to game seconds within this run (the ratio has measured 1.260, 1.311 and
1.383 in three flights, so it cannot be assumed).
Usage: roll_axis.py <out.csv> [dwell_s]
"""
import json, math, os, struct, subprocess, sys, time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import speed_law
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)
def rows_at(fd, off, cfg):
base = off + cfg["rot_delta"]
out = []
for r in range(3):
v = struct.unpack(">3f", os.pread(fd, 12, base + r * cfg["rot_stride"]))
n = math.sqrt(sum(c * c for c in v)) or 1.0
out.append(tuple(c / n for c in v))
return out
def dot(a, b):
return sum(a[i] * b[i] for i in range(3))
def main():
out_csv = sys.argv[1]
dwell = float(sys.argv[2]) if len(sys.argv) > 2 else 8.0
cfg = json.load(open("/tmp/nav-live.json"))
w, off, nm = speed_law.find_player()
if not w:
sys.exit("player entity not found after retries")
print(f"# locked on {nm}")
f_i = cfg.get("fwd_row", 0)
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))):
# 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")
# 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:
time.sleep(0.05)
cur = rows_at(w.fd, off, cfg)
# roll = rotation of `up` within the OLD (up, right) plane
d = math.degrees(math.atan2(dot(cur[u_i], prev[w_i]), dot(cur[u_i], prev[u_i])))
swept += abs(d)
seq.append((round(time.time() - t0, 3), round(d, 4)))
prev = cur
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_neutral()
with open(out_csv, "w") as f:
f.write("phase,t,droll_deg\n")
for r in rows:
f.write(",".join(str(x) for x in r) + "\n")
print("# read HUD TIME off rollax_slow_a/b and rollax_fast_a/b for this run's clock ratio")
if __name__ == "__main__":
main()