Phase 1: extend sitl_observer with cursor-based `wait_for_outbound` returning `OutboundMessage` from `outbound_messages_<fc_kind>_<host>.json` fixtures. Three outcomes: message, TimeoutError (null entries), or RuntimeError (missing/malformed). Fix FT-P-01 + FT-P-05 scenarios to use `fc_kind=` kwarg. Phase 2: FT-P-01 vertical-slice fixture builder under `e2e/fixtures/sitl_replay_builder/`. Reuses the production `gps-denied-replay` CLI + `ReplayInputAdapter`: encode 60 stills as 1 fps MP4 + synthetic stationary tlog (pymavlink); run replay; project FDR outbound estimates into the schema. Avoids the 13+ cp of SUT-side frame-ingestion that a live-SITL-capture path would have required. Live execution remains a manual operator step. +35 unit tests (664 total, up from 637). K=3 cumulative review for b76-b78 documents the offline-replay arc convergence. Co-authored-by: Cursor <cursoragent@cursor.com>
3.1 KiB
SITL Replay Fixture Builder (AZ-598)
Produces the outbound_messages_<fc_kind>_<host>.json +
observer_<fc_kind>_<host>.json fixtures consumed by the b75
sitl_observer module in offline FDR-replay mode (b75/b78).
Vertical-slice scope (this batch)
Only the FT-P-01 still-image accuracy scenario is supported. Other scenarios (FT-P-02 Derkachi continuous flight, FT-N-04 blackout-spoof, etc.) need their own capture flows and will land as follow-up tickets.
Strategy
Rather than spinning up a SITL container, this builder reuses the
production gps-denied-replay CLI + ReplayInputAdapter:
- Encode the 60
AD0000NN.jpgstill images into a 1 fps MP4. - Generate a synthetic stationary tlog (zero-motion
RAW_IMU+ATTITUDEpairs at 200 Hz) — bypasses the AZ-405 take-off pre-validator without needing real flight data. - Run
gps-denied-replay --video stills.mp4 --tlog stationary.tlog --time-offset-ms 0 --fdr-out fdr.jsonl(auto-sync bypassed because the synthetic tlog has no take-off signal). - Read
fdr.jsonl, filter tokind == outbound_position_estimate, project each into theoutbound_messages_*schema. - Write the two fixture JSON files into
--output-dir.
This avoids needing new SUT-side frame-ingestion code (HTTP endpoint, file-watch source, etc.) which would otherwise be required to push individual stills to a running SUT container.
Usage
gps-denied-build-p01-fixtures \
--input-dir _docs/00_problem/input_data \
--output-dir e2e/fixtures/sitl_replay/p01 \
--fc-kind ardupilot \
--host sitl-host
The output directory will contain:
stills.mp4— the 60 images encoded at 1 fps.stationary.tlog— synthetic 120-s zero-motion tlog at 200 Hz.fdr.jsonl— the FDR JSONL stream from the replay run.outbound_messages_ardupilot_sitl-host.json— the consumed fixture.observer_ardupilot_sitl-host.json— the consumed fixture.
To activate the fixtures in a scenario run:
E2E_SITL_REPLAY_DIR=e2e/fixtures/sitl_replay/p01 \
pytest e2e/tests/positive/test_ft_p_01_still_image_accuracy.py
Limitations
- The synthetic tlog encodes zero motion — auto-sync MUST be bypassed
via
--time-offset-ms 0(the builder does this automatically). - The FDR record
kindis assumed to beoutbound_position_estimate— the--fdr-kindCLI flag overrides if the actual schema differs. - Per-image timeout handling: if the SUT emits fewer outbound estimates
than pushed frames, trailing image_ids are written as
nullentries (encoded as TimeoutError on scenario replay). - iNav adapter is NOT supported by this batch — only ArduPilot. iNav will land as a follow-up once the AP path is validated end-to-end.
Testing
Unit tests under e2e/_unit_tests/fixtures/test_sitl_replay_builder.py
mock all external dependencies (OpenCV, pymavlink, subprocess) so the
test suite runs without a real gps-denied-replay install. The actual
end-to-end run requires the SUT to be installed (pip install -e . at
repo root) and is documented as a manual step until CI infrastructure
catches up.