re(flight): axis probe -- lx is roll, ly drives one clean axis, rest not trustworthy
New probe (axis_probe.py) decomposes every held input into all THREE rotation
components at once, instead of measuring one axis at a time through a non-forward
matrix row -- the flaw that once made roll and pitch produce identical numbers.
For previous rows (f,u,w): roll = atan2(u.w_old, u.u_old), and forward's rotation
toward each of the other two rows gives the remaining pair.
Stage 02, file pad, full deflection on exactly one channel at a time:
lx+ roll 209.8 b 0.9 c 10.1 deg/wall-s
ly+ roll 0.0 b 154.1 c 0.0
rx+ roll 0.0 b 0.0 c 0.0
ry+ roll 161.1 b 87.0 c 37.1
LB/RB all zero
What this supports: lx = ROLL, cleanly (~0 on both other channels), agreeing with
the independent roll measurement. ly drives ONE axis, cleanly.
What it does NOT support, and I am not claiming:
- WHICH axis ly drives. entities2 measures row 2 = forward against velocity, but
rows 0 and 1 are labelled up/right by the D3D convention rather than by
evidence, and yaw/pitch SWAP if that is wrong. Roll is immune (rotation of
either non-forward row in their shared plane is roll either way).
- anything about rx/ry/LB/RB. The run ended on GAME OVER: full-deflection spin in
a live combat mission gets the craft destroyed, and the only symptom is "0
player candidates" AFTERWARDS, so late rows may be post-death. rx+ reading all
zeros and ry+ reading mixed are exactly what a dying craft would produce.
So "yaw: no input found" is NOT resolved. Both gaps are now written into the
probe's header with what would fix them: a liveness check between inputs, and
pinning up-vs-right against world Y.
This commit is contained in:
7
docs/re/captures/axis-probe-stage02.csv
Normal file
7
docs/re/captures/axis-probe-stage02.csv
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
input,roll_deg_s,yaw_deg_s,pitch_deg_s
|
||||||
|
lx+,209.8,0.87,10.07
|
||||||
|
ly+,0.0,154.12,0.0
|
||||||
|
rx+,0.0,0.0,0.0
|
||||||
|
ry+,161.07,86.98,37.06
|
||||||
|
LB,0.0,0.0,0.0
|
||||||
|
RB,0.0,0.0,0.0
|
||||||
|
131
tools/re-capture/axis_probe.py
Executable file
131
tools/re-capture/axis_probe.py
Executable file
@@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Which pad input drives which rotation axis — all three measured at once.
|
||||||
|
|
||||||
|
The control mapping was left with **yaw ❔ "no input found"**: `AV_Yaw_*` exists in
|
||||||
|
the definitions (45/25) but no stick appeared to yaw. That conclusion came from
|
||||||
|
probes that measured one axis at a time using a non-forward matrix row, which is
|
||||||
|
exactly the flaw that made roll and pitch produce identical numbers — a row like
|
||||||
|
that moves under *any* rotation, so "this input yaws" and "this input pitches"
|
||||||
|
cannot be told apart by it.
|
||||||
|
|
||||||
|
So decompose properly. For each small step, with the previous frame's orthonormal
|
||||||
|
rows `(f, u, w)` = forward, up, right:
|
||||||
|
|
||||||
|
roll = atan2(u_new . w_old, u_new . u_old) rotation of UP about forward
|
||||||
|
yaw = atan2(f_new . w_old, f_new . f_old) rotation of FORWARD about up
|
||||||
|
pitch = atan2(f_new . u_old, f_new . f_old) rotation of FORWARD about right
|
||||||
|
|
||||||
|
Each drops the components the other two produce, so one held input yields three
|
||||||
|
numbers and the axis it actually drives is whichever is large.
|
||||||
|
|
||||||
|
Every candidate is held with the file pad, which writes the WHOLE state at once —
|
||||||
|
so `rx` really means right-stick-X with every other channel exactly zero, which a
|
||||||
|
quantising virtual stick could not promise.
|
||||||
|
|
||||||
|
⚠️ TWO THINGS THIS PROBE DOES NOT YET HANDLE, both learned the hard way:
|
||||||
|
|
||||||
|
1. **The craft can DIE mid-run.** Holding a stick at full deflection spins the ship
|
||||||
|
at 150-200 deg/s in a live combat mission; two minutes of that and the first run
|
||||||
|
ended on GAME OVER. Inputs measured after that point are meaningless, and the
|
||||||
|
symptom is only "0 player candidates, best displacement 0.000" *afterwards* --
|
||||||
|
the earlier rows still look fine. Check the player is alive BETWEEN inputs, or
|
||||||
|
probe somewhere nothing shoots back.
|
||||||
|
2. **Which row is UP and which is RIGHT is not pinned.** `entities2` measures that
|
||||||
|
row 2 is forward (against velocity), but rows 0 and 1 are assigned by the D3D
|
||||||
|
convention, not by evidence. Roll is unaffected -- rotation of either non-forward
|
||||||
|
row within their shared plane is roll either way -- but YAW and PITCH swap if the
|
||||||
|
convention is wrong, so this probe's last two columns are named on an assumption.
|
||||||
|
|
||||||
|
Usage: axis_probe.py <out.csv> [dwell_s]
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import speed_law # noqa: E402
|
||||||
|
|
||||||
|
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.
|
||||||
|
CANDIDATES = [
|
||||||
|
("lx+", {"lx": 32767}),
|
||||||
|
("ly+", {"ly": 32767}),
|
||||||
|
("rx+", {"rx": 32767}),
|
||||||
|
("ry+", {"ry": 32767}),
|
||||||
|
("LB", {"press": "LB"}),
|
||||||
|
("RB", {"press": "RB"}),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def pad_state(**kw):
|
||||||
|
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 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 5.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")
|
||||||
|
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]
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
print(f"# {'input':<6} {'roll':>9} {'yaw':>9} {'pitch':>9} (deg over the dwell)")
|
||||||
|
for label, state in CANDIDATES:
|
||||||
|
pad_state() # neutral
|
||||||
|
time.sleep(2.0)
|
||||||
|
prev = rows_at(w.fd, off, cfg)
|
||||||
|
pad_state(**state)
|
||||||
|
t0 = time.time()
|
||||||
|
sw = [0.0, 0.0, 0.0]
|
||||||
|
while time.time() - t0 < dwell:
|
||||||
|
time.sleep(0.05)
|
||||||
|
cur = rows_at(w.fd, off, cfg)
|
||||||
|
roll = math.degrees(math.atan2(dot(cur[u_i], prev[w_i]), dot(cur[u_i], prev[u_i])))
|
||||||
|
yaw = math.degrees(math.atan2(dot(cur[f_i], prev[w_i]), dot(cur[f_i], prev[f_i])))
|
||||||
|
pitch = math.degrees(math.atan2(dot(cur[f_i], prev[u_i]), dot(cur[f_i], prev[f_i])))
|
||||||
|
for k, v in enumerate((roll, yaw, pitch)):
|
||||||
|
sw[k] += abs(v)
|
||||||
|
prev = cur
|
||||||
|
pad_state()
|
||||||
|
wall = time.time() - t0
|
||||||
|
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")
|
||||||
|
time.sleep(1.5)
|
||||||
|
|
||||||
|
with open(out_csv, "w") as f:
|
||||||
|
f.write("input,roll_deg_s,yaw_deg_s,pitch_deg_s\n")
|
||||||
|
for r in rows:
|
||||||
|
f.write(",".join(str(x) for x in r) + "\n")
|
||||||
|
print(f"# wrote {out_csv}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user