mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 23:41:13 +00:00
[AZ-600] Batch 80: refactor sitl_replay_builder to strategy pattern
Replace per-scenario fixture builders with a parameterized strategy framework so future Derkachi-based scenarios compose existing pieces instead of duplicating ~200 lines of orchestration per scenario. New e2e/fixtures/sitl_replay_builder/builder.py: - VideoSource ABC + StillImagesSource, Mp4PassthroughSource - TlogSource ABC + SyntheticStationaryTlog, ImuCsvTlog - FdrProjection ABC + RawFdrPassthrough, OutboundMessagesProjection - FixtureBuilderConfig + build_fixtures(cfg) orchestrator - Consolidated MAVLink pack_raw_imu / pack_attitude helpers - Consolidated run_gps_denied_replay + write_observer_fixture build_p01_fixtures.py: 423 -> 107 lines (75% reduction). build_p02_fixtures.py: 292 -> 98 lines (66% reduction). _common.py: deleted (folded into builder.py). Tests reorganized: - test_sitl_replay_builder_builder.py (new, 33 strategy-level tests) - test_sitl_replay_builder.py (slimmed, 6 FT-P-01 integration) - test_sitl_replay_builder_p02.py (slimmed, 7 FT-P-02 integration) README documents the strategy framework + a worked example for adding FT-P-04 in ~30 lines (no new strategy code required). Regression gate: 700 passing (was 686; +14 from finer-grained coverage of new strategy classes and the build_fixtures orchestrator). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,53 +1,93 @@
|
||||
# SITL Replay Fixture Builder (AZ-598, AZ-599)
|
||||
# SITL Replay Fixture Builder (AZ-598, AZ-599, AZ-600)
|
||||
|
||||
Per-scenario fixture builders for the offline FDR-replay path used
|
||||
by the b75 `sitl_observer` module + FT-* blackbox scenarios. Each
|
||||
builder takes recorded flight inputs (still images / video / IMU
|
||||
CSV / tlog) and produces the artifacts a specific scenario needs.
|
||||
Parameterized fixture-builder framework for the offline FDR-replay path
|
||||
used by the b75 `sitl_observer` module + FT-* blackbox scenarios. A new
|
||||
scenario typically only writes a ~60-line config factory + CLI on top of
|
||||
the framework — no new strategy code required.
|
||||
|
||||
| Scenario | Builder | Inputs | Outputs |
|
||||
|----------|---------|--------|---------|
|
||||
| FT-P-01 (still-image accuracy) | `build_p01_fixtures.py` | 60 `AD0000NN.jpg` + coordinates CSV | `outbound_messages_<fc>_<host>.json` + `observer_<fc>_<host>.json` + `stills.mp4` + `stationary.tlog` + `fdr.jsonl` |
|
||||
| FT-P-01 (still-image accuracy) | `build_p01_fixtures.py` | 60 `AD0000NN.jpg` | `outbound_messages_<fc>_<host>.json` + `observer_<fc>_<host>.json` + `stills.mp4` + `stationary.tlog` + `fdr.jsonl` |
|
||||
| FT-P-02 (Derkachi drift) | `build_p02_fixtures.py` | `flight_derkachi.mp4` + `data_imu.csv` | `derkachi.tlog` + `fdr/fdr.jsonl` (FDR archive) + `observer_<fc>_<host>.json` |
|
||||
|
||||
Other scenarios (FT-P-03 / 04 / 05 / 07 / 08 / 10 / 11, FT-N-01 / 02 / 03 / 04)
|
||||
need their own capture flows and will land as follow-up tickets.
|
||||
Other scenarios (FT-P-03 / 04 / 05 / 07 / 08 / 10 / 11, FT-N-01..04) will
|
||||
land as follow-ups; each will reuse the strategies below.
|
||||
|
||||
## Shared helpers (`_common.py`)
|
||||
## Framework (`builder.py`)
|
||||
|
||||
Both builders shell out to the production `gps-denied-replay` CLI and
|
||||
write the same minimal `observer_<fc_kind>_<host>.json`. These two
|
||||
operations live in `_common.py`:
|
||||
Three strategy ABCs decompose the per-scenario variance:
|
||||
|
||||
* `run_gps_denied_replay(video, tlog, fdr_out, *, time_offset_ms=0, ...)`
|
||||
* `write_observer_fixture(output_path)`
|
||||
| Strategy | Concrete impls | Used by |
|
||||
|----------|----------------|---------|
|
||||
| `VideoSource` — materialize the MP4 the replay CLI consumes | `StillImagesSource(image_paths, fps)`, `Mp4PassthroughSource(mp4_path)` | b78 / b79 |
|
||||
| `TlogSource` — materialize the tlog the replay CLI consumes | `SyntheticStationaryTlog(duration_s, hz)`, `ImuCsvTlog(csv_path, schema=DEFAULT_DERKACHI_IMU_SCHEMA)` | b78 / b79 |
|
||||
| `FdrProjection` — translate the FDR JSONL into scenario fixture shape | `RawFdrPassthrough(verify_estimates=True)`, `OutboundMessagesProjection(image_ids, fdr_kind="outbound_position_estimate")` | b79 / b78 |
|
||||
|
||||
Future per-scenario builders should import from `_common.py` rather
|
||||
than re-implementing.
|
||||
The `build_fixtures(cfg: FixtureBuilderConfig)` orchestrator composes the
|
||||
three strategies plus the shared `run_gps_denied_replay` subprocess driver
|
||||
and `write_observer_fixture` helper.
|
||||
|
||||
## FT-P-01 (`build_p01_fixtures.py`)
|
||||
Shared helpers (in `builder.py`):
|
||||
|
||||
### Strategy
|
||||
* `run_gps_denied_replay(video, tlog, fdr_out, *, time_offset_ms=0, ...)` — shells out to the production CLI.
|
||||
* `write_observer_fixture(output_path)` — writes the minimal `observer_*.json` `sitl_observer.get_observer` requires.
|
||||
* `pack_raw_imu(time_usec, *, xacc=0, yacc=0, zacc=0, xgyro=0, ygyro=0, zgyro=0)` — parameterized RAW_IMU packer. Stationary callers pass `zacc=STATIONARY_Z_ACCEL_MG` (gravity).
|
||||
* `pack_attitude(time_boot_ms, *, roll=0.0, pitch=0.0, yaw=0.0)` — parameterized ATTITUDE packer.
|
||||
* `parse_fdr_for_outbound_estimates(fdr_path, *, fdr_kind, lat_key, lon_key)` — read FDR JSONL into per-image dicts.
|
||||
* `verify_fdr_has_estimates(fdr_path)` — assert ≥1 `record_type=="estimate"` record.
|
||||
* `hdg_centideg_to_rad(hdg_cdeg)` — utility for ATTITUDE yaw synthesis.
|
||||
|
||||
Rather than spinning up a SITL container, this builder reuses the
|
||||
production `gps-denied-replay` CLI + `ReplayInputAdapter`:
|
||||
## Adding a new scenario (worked example: FT-P-04)
|
||||
|
||||
1. Encode the 60 `AD0000NN.jpg` still images into a 1 fps MP4.
|
||||
2. Generate a synthetic stationary tlog (zero-motion `RAW_IMU` +
|
||||
`ATTITUDE` pairs at 200 Hz) — bypasses the AZ-405 take-off
|
||||
pre-validator without needing real flight data.
|
||||
3. 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).
|
||||
4. Read `fdr.jsonl`, filter to `kind == outbound_position_estimate`,
|
||||
project each into the `outbound_messages_*` schema.
|
||||
5. Write the two fixture JSON files into `--output-dir`.
|
||||
FT-P-04 (Derkachi frame-to-frame registration) reuses the same Derkachi MP4
|
||||
+ IMU CSV as FT-P-02 but consumes the FDR archive differently. With the
|
||||
framework in place, the new builder is purely a config factory:
|
||||
|
||||
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.
|
||||
```python
|
||||
# e2e/fixtures/sitl_replay_builder/build_p04_fixtures.py (sketch)
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from e2e.fixtures.sitl_replay_builder.builder import (
|
||||
DEFAULT_CLI_BIN,
|
||||
FixtureBuilderConfig,
|
||||
ImuCsvTlog,
|
||||
Mp4PassthroughSource,
|
||||
RawFdrPassthrough,
|
||||
build_fixtures,
|
||||
)
|
||||
|
||||
### Usage
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class P04BuilderConfig:
|
||||
derkachi_dir: Path
|
||||
output_dir: Path
|
||||
fc_kind: str = "ardupilot"
|
||||
host: str = "sitl-host"
|
||||
|
||||
|
||||
def build_p04_fixtures(cfg, **deps):
|
||||
mp4 = cfg.derkachi_dir / "flight_derkachi.mp4"
|
||||
csv_path = cfg.derkachi_dir / "data_imu.csv"
|
||||
builder_cfg = FixtureBuilderConfig(
|
||||
video_source=Mp4PassthroughSource(mp4_path=mp4),
|
||||
tlog_source=ImuCsvTlog(csv_path=csv_path),
|
||||
fdr_projection=RawFdrPassthrough(verify_estimates=True),
|
||||
output_dir=cfg.output_dir,
|
||||
fc_kind=cfg.fc_kind, host=cfg.host,
|
||||
tlog_filename="derkachi.tlog", fdr_subdir="fdr",
|
||||
)
|
||||
return build_fixtures(builder_cfg, **deps)
|
||||
```
|
||||
|
||||
Total new code: ~30 lines + argparse CLI. No new strategy class is needed
|
||||
because every Derkachi-based scenario consumes the same `Mp4PassthroughSource +
|
||||
ImuCsvTlog + RawFdrPassthrough` triple. A scenario that emits a *new* fixture
|
||||
shape (e.g. FT-P-13's "anchor-search-region" record extraction) writes a new
|
||||
`FdrProjection` subclass alongside.
|
||||
|
||||
## Per-scenario usage
|
||||
|
||||
### FT-P-01
|
||||
|
||||
```bash
|
||||
python -m e2e.fixtures.sitl_replay_builder.build_p01_fixtures \
|
||||
@@ -57,51 +97,14 @@ python -m e2e.fixtures.sitl_replay_builder.build_p01_fixtures \
|
||||
--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:
|
||||
Activation:
|
||||
|
||||
```bash
|
||||
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 `kind` is assumed to be `outbound_position_estimate`
|
||||
— the `--fdr-kind` CLI 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 `null` entries
|
||||
(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.
|
||||
|
||||
## FT-P-02 (`build_p02_fixtures.py`)
|
||||
|
||||
### Strategy
|
||||
|
||||
Same overall shape as FT-P-01 (drive `gps-denied-replay` against a
|
||||
video + tlog pair), with two differences:
|
||||
|
||||
1. Video is already MP4 — skip the OpenCV still-image encoding step.
|
||||
2. IMU is recorded telemetry (`data_imu.csv`, 10 Hz `SCALED_IMU2` +
|
||||
`GLOBAL_POSITION_INT`). A CSV → tlog conversion packs each row as
|
||||
a `RAW_IMU` + `ATTITUDE` MAVLink pair, with yaw synthesised from
|
||||
`GLOBAL_POSITION_INT.hdg` (centidegrees → radians) and roll/pitch
|
||||
= 0 (acceptable for the fixed-wing cruise data this represents).
|
||||
|
||||
Output is the SUT's natural FDR archive directory; the FT-P-02
|
||||
scenario reads it via `runner.helpers.fdr_reader.iter_records`.
|
||||
|
||||
### Usage
|
||||
### FT-P-02
|
||||
|
||||
```bash
|
||||
python -m e2e.fixtures.sitl_replay_builder.build_p02_fixtures \
|
||||
@@ -111,28 +114,35 @@ python -m e2e.fixtures.sitl_replay_builder.build_p02_fixtures \
|
||||
--host sitl-host
|
||||
```
|
||||
|
||||
Output:
|
||||
## Limitations
|
||||
|
||||
* `derkachi.tlog` — generated from `data_imu.csv`.
|
||||
* `fdr/fdr.jsonl` — the FDR archive from the replay run.
|
||||
* `observer_ardupilot_sitl-host.json` — minimal observer config.
|
||||
|
||||
### Limitations
|
||||
|
||||
* The synthesised ATTITUDE has roll/pitch = 0 — acceptable for
|
||||
fixed-wing cruise but unrealistic for aggressive manoeuvres.
|
||||
* RAW_IMU is packed from `SCALED_IMU2` columns as pass-through (no
|
||||
true scaled → raw unit conversion). If the SUT's tlog parser
|
||||
strictly demands true raw counts the builder will need a units
|
||||
conversion pass — surfaced as a follow-up after live-run.
|
||||
* Auto-sync is bypassed via `--time-offset-ms 0` because the
|
||||
Derkachi CSV is already aligned with the video.
|
||||
* The synthesised ATTITUDE has roll/pitch = 0 — acceptable for fixed-wing
|
||||
cruise but unrealistic for aggressive manoeuvres. Override the packer call
|
||||
inside a custom `TlogSource` when needed.
|
||||
* RAW_IMU is packed from `SCALED_IMU2` columns as pass-through (no true
|
||||
scaled → raw unit conversion). If the SUT's tlog parser strictly demands
|
||||
true raw counts the builder will need a units conversion pass — surfaced
|
||||
as a follow-up after the first live run.
|
||||
* Auto-sync (`time_offset_ms != 0`) is bypassed by every scenario currently;
|
||||
operators running this against truly independent tlog+video pairs should
|
||||
override `FixtureBuilderConfig.time_offset_ms`.
|
||||
* iNav adapter is NOT supported by the existing builders — ArduPilot only.
|
||||
* The FDR record `kind`/`record_type` schemas are assumed to match the
|
||||
production contract; overrides live on each projection class.
|
||||
|
||||
## 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.
|
||||
Unit tests under `e2e/_unit_tests/fixtures/`:
|
||||
|
||||
* `test_sitl_replay_builder_builder.py` — strategy-level tests
|
||||
(`VideoSource`, `TlogSource`, `FdrProjection` impls + shared helpers +
|
||||
`build_fixtures` orchestrator).
|
||||
* `test_sitl_replay_builder.py` — FT-P-01 scenario integration.
|
||||
* `test_sitl_replay_builder_p02.py` — FT-P-02 scenario integration.
|
||||
|
||||
All external dependencies (OpenCV, pymavlink, subprocess) are mocked via
|
||||
the underscore-prefixed `_runner` / `_video_writer_factory` / `_imread` /
|
||||
`_mavlink_writer_factory` injection points so the 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.
|
||||
|
||||
Reference in New Issue
Block a user