#!/usr/bin/env python3 """Drive stick bursts while Canary samples the craft's transform ONCE PER FRAME. Why this exists. Every earlier rate measurement polled guest RAM from the host through /dev/shm. That read is unsynchronised with the guest: adjacent samples are separated by an unknown number of guest updates, so short windows alias -- `ramp_probe.py` got 3x swings between neighbouring 0.25 s windows and could not tell "the rate ramps up after the stick goes over" from "the sampler is lying". See docs/re/flight-speed-law.md, which names a Canary-side hook as the tool the residual needs. That hook now exists (`--frame_probe_log`, sampled in VdSwap, one line per guest frame). This script only has to point it at the player craft and drive the pad, recording when each hold starts and ends in the SAME clock the probe stamps its lines with, so the analysis can cut the log at the exact frame the stick moved. Usage: frame_burst.py [hold_s] [repeats] Env: XENIA_FRAME_PROBE control file (default /tmp/xenia_frame_probe.txt) """ import os import struct import sys import time sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import gmem # noqa: E402 import speed_law # noqa: E402 from axis_probe import pad_state # noqa: E402 PROBE = os.environ.get("XENIA_FRAME_PROBE", "/tmp/xenia_frame_probe.txt") # The transform block sits BELOW the position triple: three 16-byte-strided rows # starting at pos-112, position at pos+0 (nav-live.json: rot_delta -112, # rot_stride 16). One region covers both. ROT_DELTA = -112 REGION_LEN = 128 # Full deflection on each axis, plus the throttle settings the burst design uses. BURSTS = [ ("roll_cruise", {"lx": 32767}), ("pitch_cruise", {"ly": 32767}), ] def write_regions(regions): tmp = PROBE + ".tmp" with open(tmp, "w") as f: f.write("# written by frame_burst.py\n") for va, ln in regions: f.write(f"{va:08x} {ln}\n") os.replace(tmp, PROBE) def pos_at(fd, off): return struct.unpack(">3f", os.pread(fd, 12, off)) def main(): out_csv = sys.argv[1] hold = float(sys.argv[2]) if len(sys.argv) > 2 else 3.0 reps = int(sys.argv[3]) if len(sys.argv) > 3 else 2 w, off, nm = speed_law.find_player() if not w: sys.exit("player entity not found") base_off = off + ROT_DELTA va = gmem.primary_va(base_off) if va is None: sys.exit(f"file offset {base_off:#x} has no guest VA") print(f"# locked on {nm}: transform block at VA {va:#010x} (+{REGION_LEN})") write_regions([(va, REGION_LEN)]) # The probe only starts emitting once the emulator notices the control file, # which is at most one frame. Give it a moment, then prove the craft is # actually flying before spending the run on it. time.sleep(1.0) p0 = pos_at(w.fd, off) time.sleep(1.0) p1 = pos_at(w.fd, off) moved = sum((p1[i] - p0[i]) ** 2 for i in range(3)) ** 0.5 if moved < 1.0: sys.exit(f"craft is not moving ({moved:.3f}) — not in flight, or dead") print(f"# craft is flying ({moved:.1f} units/s)") rows = [] for rep in range(reps): for label, state in BURSTS: pad_state() time.sleep(5.0) t_pre = time.time() pad_state(**state) t_on = time.time() time.sleep(hold) pad_state() t_off = time.time() rows.append((rep, label, f"{t_pre:.6f}", f"{t_on:.6f}", f"{t_off:.6f}")) print(f"# rep{rep} {label}: held {t_on:.3f} -> {t_off:.3f}", flush=True) time.sleep(2.0) pad_state() with open(out_csv, "w") as f: f.write("rep,label,t_pre,t_on,t_off\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())