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.
This commit is contained in:
113
tools/re-capture/burst_probe.py
Executable file
113
tools/re-capture/burst_probe.py
Executable file
@@ -0,0 +1,113 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user