re(flight): axis probe pins the rows and checks liveness; fly_stage waits, not sleeps

Both fixes the previous run's caveats asked for, plus one the run itself forced.

axis_probe.py now:
 - PINS which non-forward row is up and which is right, by comparing world-Y
   across the rows in level flight, and says CONFIDENT or WEAK. entities2
   measures row 2 = forward against velocity, but the other two were labelled by
   the D3D convention, and yaw/pitch SWAP if that is wrong -- so the previous
   run's last two columns were named on an assumption.
 - checks the craft is ALIVE between inputs, and ABORTS with a message instead of
   reporting the clean zeros a destroyed craft produces. The first run ended on
   GAME OVER and only said so afterwards.
 - measures the UNKNOWN inputs (rx, ry, LB, RB) first while the craft is healthy,
   keeping the established lx/ly as controls at the end.

fly_stage.sh now WAITS for the stage load instead of sleeping a fixed guess. The
fixed sleeps worked until they didn't: one load ran long, the script pressed START
into a black screen, and every later step went to nothing while the screenshots
recorded a plausible-looking sequence. It now polls for a non-black frame and
aborts with a pointer to the log if the load hangs (PhysicalHeap::Release
failures) rather than continuing blind.

The probe itself did not run this iteration -- the stage load hung -- so there is
no new axis data, and none is claimed.
This commit is contained in:
2026-08-13 22:22:19 +00:00
parent 5f3c618b51
commit dfa769420d
2 changed files with 75 additions and 3 deletions

View File

@@ -52,13 +52,16 @@ PAD_FILE = os.environ.get("XENIA_PAD_FILE", "/tmp/xenia_pad.txt")
# Candidates. Sticks at full deflection, shoulders as buttons. Triggers are the
# throttle (already settled) and are left out.
# Order matters: the craft can be shot down mid-run, so the UNKNOWN inputs go
# first while it is healthy and the already-established ones (lx = roll) go last
# as controls. A row measured after death looks like a clean zero.
CANDIDATES = [
("lx+", {"lx": 32767}),
("ly+", {"ly": 32767}),
("rx+", {"rx": 32767}),
("ry+", {"ry": 32767}),
("LB", {"press": "LB"}),
("RB", {"press": "RB"}),
("lx+", {"lx": 32767}),
("ly+", {"ly": 32767}),
]
@@ -84,6 +87,46 @@ def dot(a, b):
return sum(a[i] * b[i] for i in range(3))
def alive(w, off, cfg, secs=1.0):
"""Is the craft still flying? A destroyed craft stops moving, and every
subsequent input then measures a clean, meaningless zero — the failure mode
that invalidated this probe's first run (it ended on GAME OVER and only said
so afterwards)."""
p0 = struct.unpack(">3f", os.pread(w.fd, 12, off))
time.sleep(secs)
p1 = struct.unpack(">3f", os.pread(w.fd, 12, off))
return math.dist(p0, p1) > 1.0
def pin_rows(w, off, cfg):
"""Which non-forward row is UP and which is RIGHT?
`entities2` measures row 2 = forward against the velocity vector, but the other
two are assigned by the D3D convention rather than evidence — and yaw and pitch
SWAP if that is wrong, so naming them without this test is a guess.
Discriminator: in level flight the craft's up-vector points along world +Y and
its right-vector lies near the horizontal plane. Sample at neutral and compare
|world-Y| across the rows."""
f_i = cfg["fwd_row"]
acc = [0.0, 0.0, 0.0]
n = 0
for _ in range(20):
time.sleep(0.05)
rs = rows_at(w.fd, off, cfg)
for r in range(3):
acc[r] += rs[r][1] # world Y component
n += 1
ys = [a / n for a in acc]
cand = [r for r in (0, 1, 2) if r != f_i]
up_i = max(cand, key=lambda r: abs(ys[r]))
right_i = [r for r in cand if r != up_i][0]
print(f"# row world-Y means: {[round(y,3) for y in ys]} (forward = row {f_i})")
print(f"# -> up = row {up_i}, right = row {right_i}"
f" {'CONFIDENT' if abs(ys[up_i]) - abs(ys[right_i]) > 0.3 else 'WEAK — craft may not be level'}")
return up_i, right_i
def main():
out_csv = sys.argv[1]
dwell = float(sys.argv[2]) if len(sys.argv) > 2 else 5.0
@@ -93,7 +136,9 @@ def main():
sys.exit("player entity not found")
print(f"# locked on {nm}")
f_i = cfg["fwd_row"]
u_i, w_i = [r for r in (0, 1, 2) if r != f_i]
if not alive(w, off, cfg):
sys.exit("craft is not moving at the start — not in flight, or already dead")
u_i, w_i = pin_rows(w, off, cfg)
rows = []
print(f"# {'input':<6} {'roll':>9} {'yaw':>9} {'pitch':>9} (deg over the dwell)")
@@ -115,6 +160,10 @@ def main():
prev = cur
pad_state()
wall = time.time() - t0
if not alive(w, off, cfg):
print(f"# CRAFT STOPPED MOVING after {label} — everything from here is "
f"meaningless, aborting rather than reporting zeros")
break
rate = [v / wall for v in sw]
rows.append((label, *[round(v, 2) for v in rate]))
print(f"# {label:<6} {rate[0]:9.1f} {rate[1]:9.1f} {rate[2]:9.1f} deg/wall-s")