Files
Syplheed-Reborn/tools/re-capture/burst_probe.py
Claude (auto-RE) 92d9683f3a re(flight): short bursts confirm the rate-vs-speed SHAPE, without needing the clock
The design the sweep could not provide: settle the throttle, measure the settled
speed, pitch for ONE second so speed barely moves inside the burst. Three
throttles, two repeats, row pin CONFIDENT, fresh flight.

    LT min      burst speed ~105    rate 113.6, 109.5 deg/wall-s
    cruise                  ~383         100.2,  88.4
    RT max                 ~1483          52.2,  70.5

Rate falls monotonically with speed -- 111.5 -> 94.3 -> 61.4 -- at three KNOWN,
SETTLED speeds instead of smeared across a bleeding one.

The decisive comparison needs no clock. Absolute rates depend on the run's clock
ratio, but the min:max RATIO cancels it:

    measured min:max            = 1.82
    AV_PitchMinus_Min/Max 75/40 = 1.88   ->  3.0% apart
    AV_PitchPlus_Min/Max 150/70 = 2.14   -> 15.1% apart

Two conclusions, neither resting on a clock measurement:
 - _Min/_Max really do mean "at minimum / at maximum speed", with the rate
   interpolating between them: shape confirmed to 3%.
 - ly+ drives pitch-MINUS, not plus. The craft has asymmetric pitch authority
   (75/40 down vs 150/70 up) and the ratio picks the pair cleanly.

Absolute magnitudes remain open: this run did not bracket the HUD clock, so
deg/GAME-second cannot be computed from it, and picking a ratio that makes the
numbers fit would be circular. The probe now screenshots the clock at both ends.

Also: fly_stage.sh now waits for the TAKE-OFF load too. Guarding only the stage
load left a run pressing A into a black screen and then reporting "player entity
not found" from a game that never reached flight.
2026-08-13 23:29:23 +00:00

114 lines
4.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""Angular rate at a KNOWN, SETTLED speed — short bursts instead of one sweep.
Why not a sweep: turning bleeds speed hard (1 193 -> 589 in 8 s at full throttle),
so a long hold averages a rate over a moving speed and lands between two different
caps. A sweep *driven* by that bleed also cannot dwell at either extreme, which is
exactly where a speed-dependent law is most testable.
So: settle the throttle, measure the settled speed, then pitch for only ~1 second.
The speed barely moves inside a burst, so each burst is one honest `(speed, rate)`
point. Three throttle settings give three clean points at known speeds.
Row pinning is done ONCE while the craft is still level — the pin decides which
matrix ROW is up and which is right, which is a property of the layout, not of the
current attitude, so it stays valid after the craft has been thrown around.
Accumulation is windowed over the whole burst (never per-read): polling is faster
than the guest updates these fields, so a per-read delta is either zero or a whole
frame divided by a fraction of one — that aliasing once manufactured an entire
rate-vs-speed curve.
Usage: burst_probe.py <roll|pitch> <out.csv> [burst_s] [repeats]
"""
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
from axis_probe import alive, pad_state, pin_rows, rows_at, dot # noqa: E402
DRIVER = {"roll": {"lx": 32767}, "pitch": {"ly": 32767}}
THROTTLES = [("min", {"lt": 255}), ("cruise", {}), ("max", {"rt": 255})]
def pos_at(fd, off):
return struct.unpack(">3f", os.pread(fd, 12, off))
def measure(w, off, cfg, axis, u_i, w_i, f_i, hold, secs):
"""Accumulate path length and swept angle over one interval."""
prev_r, prev_p = rows_at(w.fd, off, cfg), pos_at(w.fd, off)
t0 = time.time()
path = swept = 0.0
while time.time() - t0 < secs:
time.sleep(0.02)
cur_r, cur_p = rows_at(w.fd, off, cfg), pos_at(w.fd, off)
path += math.dist(cur_p, prev_p)
if axis == "roll":
d = math.atan2(dot(cur_r[u_i], prev_r[w_i]), dot(cur_r[u_i], prev_r[u_i]))
else:
d = math.atan2(dot(cur_r[f_i], prev_r[u_i]), dot(cur_r[f_i], prev_r[f_i]))
swept += abs(math.degrees(d))
prev_r, prev_p = cur_r, cur_p
span = time.time() - t0
return path / span, swept / span
def main():
axis, out_csv = sys.argv[1], sys.argv[2]
burst = float(sys.argv[3]) if len(sys.argv) > 3 else 1.0
reps = int(sys.argv[4]) if len(sys.argv) > 4 else 3
cfg = json.load(open("/tmp/nav-live.json"))
w, off, nm = speed_law.find_player()
if not w:
sys.exit("player entity not found")
f_i = cfg["fwd_row"]
if not alive(w, off, cfg):
sys.exit("craft is not moving")
u_i, w_i = pin_rows(w, off, cfg) # once, while level
print(f"# locked on {nm}, {axis} in {burst}s bursts x{reps}")
# ⚠️ Gap in the first run of this probe: it did NOT bracket the HUD clock, so
# the absolute deg/GAME-second could not be computed and only the (clock-free)
# min:max ratio was usable. Screenshot the HUD at the start and end.
import subprocess
subprocess.run(["screenshot", "/sylph-home/re/shots/burst_clock_a.png"],
capture_output=True)
rows = []
for tname, tstate in THROTTLES:
for rep in range(reps):
pad_state(**tstate)
time.sleep(6.0) # settle the throttle, wings level-ish
settled, _ = measure(w, off, cfg, axis, u_i, w_i, f_i, None, 1.0)
pad_state(**tstate, **DRIVER[axis])
bspeed, rate = measure(w, off, cfg, axis, u_i, w_i, f_i, None, burst)
pad_state(**tstate)
rows.append((tname, rep, round(settled, 1), round(bspeed, 1), round(rate, 2)))
print(f"# {tname:<6} rep{rep} settled {settled:7.1f} "
f"during {bspeed:7.1f} rate {rate:6.1f} deg/wall-s")
if not alive(w, off, cfg):
print("# CRAFT STOPPED MOVING — aborting")
pad_state()
break
else:
continue
break
pad_state()
subprocess.run(["screenshot", "/sylph-home/re/shots/burst_clock_b.png"],
capture_output=True)
with open(out_csv, "w") as f:
f.write("throttle,rep,settled_speed,burst_speed,rate_deg_per_wall_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())