"""Shared helpers for the per-scenario fixture builders (AZ-599). Both `build_p01_fixtures.py` (still-image FT-P-01) and `build_p02_fixtures.py` (Derkachi FT-P-02) shell out to the production `gps-denied-replay` CLI and write the same minimal `observer_*.json` config; the helpers below live here so there's one canonical implementation. Future per-scenario builders (FT-P-04 / FT-P-05 / FT-P-10 / …) should also import from this module. """ from __future__ import annotations import json import logging import subprocess from pathlib import Path from typing import Callable, Sequence _LOG = logging.getLogger(__name__) DEFAULT_CLI_BIN = "gps-denied-replay" def run_gps_denied_replay( video: Path, tlog: Path, fdr_out: Path, *, cli_bin: str = DEFAULT_CLI_BIN, time_offset_ms: int = 0, extra_args: Sequence[str] = (), _runner: Callable[[Sequence[str]], subprocess.CompletedProcess] | None = None, ) -> subprocess.CompletedProcess: """Run ``gps-denied-replay`` as a subprocess. The `time_offset_ms` defaults to 0 because both b78 (synthetic stationary tlog) and b79 (Derkachi real-motion tlog) intentionally bypass auto-sync — b78 because there's no take-off signal, b79 because the IMU CSV is already aligned with the video. Operators running this against truly independent tlog+video pairs SHOULD omit ``time_offset_ms`` and let the production auto-sync run. Raises ``subprocess.CalledProcessError`` on non-zero exit code. The default subprocess runner can be swapped via ``_runner`` for unit tests. """ fdr_out.parent.mkdir(parents=True, exist_ok=True) cmd: list[str] = [ cli_bin, "--video", str(video), "--tlog", str(tlog), "--time-offset-ms", str(time_offset_ms), "--fdr-out", str(fdr_out), *extra_args, ] _LOG.info("running: %s", " ".join(cmd)) runner = _runner or (lambda c: subprocess.run(c, check=True, capture_output=True, text=True)) return runner(cmd) def write_observer_fixture(output_path: Path) -> None: """Write minimal `observer__.json` so `get_observer` succeeds. Scenarios that only consume `wait_for_outbound` or `iter_records` still trigger `sitl_observer.get_observer(...)` for construction. Populate with safe defaults; scenarios that care about `read_gps_state` carry their own observer fixtures. """ payload = { "gps_state": { "primary_source": "MAV", "last_position_lat_deg": 0.0, "last_position_lon_deg": 0.0, "last_position_alt_m": 0.0, "fix_quality": 3, "horizontal_accuracy_m": 1.0, "last_update_age_ms": 0, }, "parameters": {}, } output_path.parent.mkdir(parents=True, exist_ok=True) output_path.write_text(json.dumps(payload, indent=2))