mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 16:41:13 +00:00
[AZ-489] [AZ-490] ADR-010 design pass: operator-mission as cold-start anchor
Architecture, contracts, and task amendments for the flight-route-driven preflight + cold-start origin feature (ADR-010). No source code touched in this commit; the implementation commits for AZ-489 / AZ-490 / AZ-419 land separately. * architecture.md: ADR-010, new Principle #14, amended Principle #11, external systems gain flights service + Mission Planner UI, data model gains Flight / Waypoint / TakeoffOrigin. * system-flows.md: F1 gains phase 0 (Flight resolve), F2 gains cold-start ladder, F7 gains mid-flight bounded-delta GPS gate. * glossary.md: Flight, Flights API, Mid-flight bounded-delta GPS gate, Mission Planner UI, Takeoff origin, Waypoint. * C10: description + cache_provisioner + manifest_verifier bumped to v1.1 carrying takeoff_origin + flight_id in the manifest hash. * C12: description updated + new flights_api_client.md contract v1.0. * C5: description + state_estimator_protocol bumped to v1.1 with set_takeoff_origin + 3-clause spoof-promotion gate. * AZ-323/324/325/326/328/419 amended in place. AZ-490 spec created (C5 set_takeoff_origin entrypoint). * Dependencies table: 142 tasks / 478 pts / 15 forward edges (2 new tasks, 2 backward deps, 2 forward deps from AZ-419). * Leftovers cleared: 2026-05-11 Jira transition entries for AZ-355 and AZ-386 are deleted (Jira reconnected; both already transitioned in their respective implementation commits). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -39,7 +39,8 @@ The system is a **Jetson Orin Nano Super-hosted onboard companion** that deliver
|
||||
8. **D-CROSS-LATENCY-1 hybrid**: K=3 baseline auto-degrades to K=2 + Jacobian covariance under Jetson thermal throttle, preserving AC-4.1 at +50 °C ambient at the cost of ~5–10 % accuracy loss (still inside AC-NEW-4).
|
||||
9. **Two execution tiers** (Tier-1 workstation Docker = fast/cheap; Tier-2 Jetson hardware = AC-bound) appear in the deployment plan and CI matrix per finding F6.
|
||||
10. **Camera intrinsics and full-altitude footage are calibration prerequisites**, not implementation gaps. Production accuracy claims are gated on D-PROJ-1 closure (hybrid factory + checkerboard refinement). Test fixtures use `adti26` calibration sourced from public/factory references.
|
||||
11. **Spoofed GPS never re-enters the estimator** unless FC GPS health is stable + non-spoofed for ≥10 s **AND** a visual/satellite consistency check has succeeded (AC-NEW-8).
|
||||
11. **Spoofed GPS never re-enters the estimator** unless the FC GPS report passes a three-part gate (AC-NEW-8 + AZ-490 follow-up): (a) FC GPS health stable + non-spoofed for ≥ 10 s, (b) a visual/satellite consistency check has succeeded on the next anchor frame, AND (c) the FC's reported position is within ≤ 200 m of the companion's last emitted `PoseEstimate`. The third clause is the **mid-flight bounded-delta gate** — even a "stable, non-spoofed" GPS frame is rejected if it disagrees with the companion's posterior by more than the configurable budget. Real GPS that passes the gate is fused via `add_pose_anchor` with the FC's covariance (treated as one more anchor source, never overriding the visual pipeline without the gate).
|
||||
14. **Operator-planned mission is the primary cold-start trust anchor**, not the FC EKF (AZ-490 follow-up). The operator authors the route in the parent-suite **Mission Planner UI** (`suite/ui`), the route persists in the parent-suite **`flights` REST service** (`suite/flights`), and C12 (operator tooling) reads the `Flight` from that service to: (a) derive the cache bbox as the envelope of the waypoint lat/lon plus a configurable buffer, (b) extract the first-ordered waypoint as the **takeoff origin** (lat / lon / alt), and (c) bake the takeoff origin into the C10 Manifest so the airborne C5 can warm-start from it via `set_takeoff_origin(origin, sigma_m)` **before** any FC IMU / VIO sample arrives. This unblocks the GPS-jammed-at-takeoff scenario the FC-EKF-only cold-start path (AZ-419 today) cannot handle. The FC EKF's last valid GPS becomes a **secondary** cold-start input — used only when the operator origin is missing from the Manifest OR when the FC EKF reading passes the same bounded-delta consistency check against the operator origin.
|
||||
12. **AC-4.5 is internal smoothing only.** GTSAM iSAM2 retroactively refines past keyframes onboard and emits the corrected current frame; the FC log is forward-time only — neither ArduPilot nor iNav supports FC-side retroactive correction (Mode B Fact #107).
|
||||
13. **Interface-first components with constructor-injected dependencies.** Every component is **defined as an interface (Python `Protocol` or `ABC`) before any concrete implementation exists**, lives in its **own folder under `src/components/<component>/`**, and is wired together via **constructor injection** at a single composition root. Components never reach out to a global registry, a singleton, or `import` a sibling component's concrete class directly — they receive their collaborators as `__init__` arguments typed against the sibling's interface. Multiple interchangeable implementations of the same interface MUST be supported by design (e.g., C1 has three `VioStrategy` implementations; C2 has UltraVPR + MegaLoc + MixVPR + … behind a single `VprStrategy`; C8 has two FC-adapter implementations behind a single `FcAdapter`). Selection happens once, at startup, by config; the composition root resolves config → concrete implementation → wires the graph; the rest of the runtime sees only interfaces. **Side benefit (NOTE)**: this design also gives the project **packaging optionality** — different combinations of `BUILD_*` flags can produce binaries tailored to specific deployment targets, customer bundles, or (if/when relevant later) end-product licensing strategies, **without any source-level change in application code**. That optionality is a *consequence* of the interface-first design, not a driver — the architectural decisions in this document are made on technical grounds; component licenses do not influence them. See ADR-002 § Consequences and ADR-009.
|
||||
|
||||
@@ -63,17 +64,21 @@ The system is a **Jetson Orin Nano Super-hosted onboard companion** that deliver
|
||||
|---|---|
|
||||
| Companion PC runtime (Jetson Orin Nano Super, JetPack 6.2) | Flight controller firmware (ArduPilot Plane, iNav) |
|
||||
| All onboard pose-estimation logic (C1–C8, C13) | Parent-suite `satellite-provider` (.NET 8 REST microservice) |
|
||||
| Pre-flight cache artifact build (C10 — engines + descriptors + manifest) | GCS (QGroundControl) |
|
||||
| Operator-side Tile Manager (C11 — pre-flight download + post-landing upload) | Nav camera hardware (`adti20`); AI-camera hardware |
|
||||
| Operator pre-flight tooling (C12) | UAV airframe / FC IMU / sensors |
|
||||
| FDR writer (C13) | Operator's workstation OS / authentication |
|
||||
| Camera calibration artifact format + loader | The act of calibration itself (operator runs checkerboard rig) |
|
||||
| Pre-flight cache artifact build (C10 — engines + descriptors + manifest) | Parent-suite `flights` REST service (.NET 8; owns the `Flight` + `Waypoint` DTOs) |
|
||||
| Operator-side Tile Manager (C11 — pre-flight download + post-landing upload) | Parent-suite Mission Planner UI (`suite/ui` — where operators plan the route) |
|
||||
| Operator pre-flight tooling (C12) | GCS (QGroundControl) |
|
||||
| FDR writer (C13) | Nav camera hardware (`adti20`); AI-camera hardware |
|
||||
| Camera calibration artifact format + loader | UAV airframe / FC IMU / sensors |
|
||||
| | Operator's workstation OS / authentication |
|
||||
| | The act of calibration itself (operator runs checkerboard rig) |
|
||||
|
||||
**External systems**:
|
||||
|
||||
| System | Integration Type | Direction | Purpose |
|
||||
|---|---|---|---|
|
||||
| `satellite-provider` (parent-suite .NET 8) | REST + filesystem (read), REST (post-landing write, D-PROJ-2) | Both | Pre-flight tile source; post-landing tile sink (planned) |
|
||||
| `flights` REST service (parent-suite .NET 8) | REST (read) over HTTPS | Inbound to C12 | Source of the operator-planned `Flight` (waypoints, ordering, altitudes). C12 derives bbox + takeoff origin from the Flight. **Operator workstation only** — never reached from the airborne companion |
|
||||
| Mission Planner UI (`suite/ui`) | Indirect via `flights` REST | Inbound (mediated) | Where the operator authors the route before C12 consumes it. Out of scope for this project, but the API contract it produces IS in scope |
|
||||
| ArduPilot Plane FC | MAVLink 2.0 over UART/USB (signed) | Both | Inbound: external position via `GPS_INPUT`. Outbound: IMU, attitude, GPS health, EKF source-set commands |
|
||||
| iNav FC | MSP2 over UART (unsigned), MAVLink outbound | Both | Inbound: external position via `MSP2_SENSOR_GPS` (companion is sole GPS source on iNav). Outbound: IMU/attitude/telemetry |
|
||||
| QGroundControl (GCS) | MAVLink 2.0 (link-bandwidth-limited) | Both | 1–2 Hz downsampled summary out (AC-6.1); operator commands in (AC-6.2) |
|
||||
@@ -197,9 +202,12 @@ source repo
|
||||
| `EmittedExternalPosition` | WGS84 + honest `horiz_accuracy` + per-FC encoding (MAVLink `GPS_INPUT` for AP, MSP2 `MSP2_SENSOR_GPS` for iNav) | C8 |
|
||||
| `FlightStateSignal` | `IN_AIR | ON_GROUND` boolean derived from FC `MAV_STATE` | C8 inbound side; published to C11 only post-landing |
|
||||
| `FdrRecord` | Estimates + IMU traces + emitted MAVLink + system health + tiles + thumbnails (≤ 64 GB / flight) | C13 |
|
||||
| `Manifest` | Hash of (model + calibration + corpus + sector classification) for D-C10-1 idempotence | C10 |
|
||||
| `Manifest` | Hash of (model + calibration + corpus + sector classification + takeoff origin) for D-C10-1 idempotence | C10 |
|
||||
| `EngineCacheEntry` | TRT engine + INT8 calibration cache keyed by SM/JP/TRT/precision tuple (D-C10-7) | C10, C7 |
|
||||
| `SectorClassification` | `active_conflict | stable_rear` per area, drives freshness threshold | C12 (operator-set) → C6, C10 |
|
||||
| `Flight` | Operator-planned mission: ordered `Waypoint` list + metadata, persisted in the parent-suite `flights` REST service. Read by C12 via `FlightsApiClient`; never reached from the airborne companion | External (`suite/flights`) → C12 |
|
||||
| `Waypoint` | Ordered `(lat, lon, alt, objective, source)` entry inside a `Flight`. C12 envelopes waypoint lat/lon → bbox; first-ordered waypoint → takeoff origin | External (`suite/flights`) → C12 |
|
||||
| `TakeoffOrigin` | `LatLonAlt` carried in the C10 Manifest; baked in by C12 at build time from `Flight.waypoints[0]`; consumed at boot by C5 via `set_takeoff_origin(origin, sigma_m)` (AZ-490) | C12 → C10 Manifest → C5 |
|
||||
|
||||
**Key relationships**:
|
||||
|
||||
@@ -601,3 +609,32 @@ This decision is made on **technical grounds only**. Component licenses (BSD/Apa
|
||||
- Build-time exclusion (ADR-002) becomes architectural: the deployment composition root *cannot* `import` a strategy whose file is not part of the deployment binary's CMake target. The same property scales to any future packaging variant — including, if/when product licensing strategy is decided, license-driven bundles (Principle #13 NOTE), without any source-level change in application code.
|
||||
- Per-component folders give each implementation a natural home for its own `tests/`, fixtures, and adapter-specific helpers — matching coderule.mdc's "logic specific to a platform, variant, or environment belongs in the class that owns that variant".
|
||||
- Adding a new C2 VPR backbone (e.g., a future foundation-model retrieval backbone via D-C2-12) is a folder-add + interface-conformance change; no other component is touched.
|
||||
|
||||
### ADR-010 — Operator-planned mission is the cold-start trust anchor; FC GPS is secondary
|
||||
|
||||
**Context**: The original cold-start design (AZ-419 / FT-P-11) assumed the FC EKF's last valid GPS fix is available at takeoff to seed C5. Field reality contradicts this: a UAV operating in a contested-EW environment may have GPS jammed **before** takeoff (the jamming radius reaches the launch site, the unit launches under a jammer's umbrella, etc.). In that case the FC EKF has no GPS fix to give, and the companion has nothing to anchor the initial pose to — the entire downstream pipeline (VIO bootstrap, VPR retrieval scope, satellite anchoring) collapses or runs blind. At the same time, the parent suite already requires the operator to author a route in the **Mission Planner UI** (`suite/ui`) and persist it to the **`flights` REST service** (`suite/flights`) before any flight runs. The waypoint ordering is operationally meaningful: waypoint[0] is the planned takeoff point. The operator therefore already declares the takeoff position with operationally relevant accuracy (typically a few tens of metres) hours before launch, in a context that has no dependency on GPS at all. This information is the natural cold-start trust anchor.
|
||||
|
||||
**Decision**:
|
||||
|
||||
1. **`Flight` is read pre-flight, not in-flight.** C12 (the operator-side tool, separate binary from the airborne companion — per ADR-002) calls the parent-suite `flights` REST service via a typed client (AZ-489 `FlightsApiClient`) when the operator runs `gps-denied-cli build-cache --flight-id <Guid>`. An offline path (`--flight-file <path>`) reads the same DTO shape from a JSON export so the workflow survives operator workstations that have no path to the flights service. The companion binary **never** depends on the flights service at runtime (Principle #9 — denied-environment operation).
|
||||
2. **C12 derives bbox + takeoff origin from the `Flight`.** The bbox is the envelope of waypoint lat/lon plus a configurable buffer (default 1 km, AZ-489 AC-3). The takeoff origin is `Flight.waypoints[0].(lat, lon, alt)` — the operator's authored launch point.
|
||||
3. **Both fields are baked into the C10 Manifest.** `BuildRequest` and `Manifest` carry `takeoff_origin: LatLonAlt | None` (AZ-323 / AZ-325 / AZ-324 amendments). The hash that drives D-C10-1 idempotence includes `takeoff_origin`, so a re-plan of the route produces a new cache identity and the verifier (AZ-324) rejects a mismatched cache at boot.
|
||||
4. **C5 consumes the origin before any sensor sample.** The companion's composition root reads `takeoff_origin` from the cache manifest at boot and invokes `set_takeoff_origin(origin, sigma_m)` on the active `StateEstimator` (AZ-490) **before** the first `add_vio` / `add_fc_imu` call. Both `GtsamIsam2StateEstimator` and `EskfStateEstimator` accept the origin as a Bayesian prior — iSAM2 attaches a `PriorFactorPose3` on the initial pose key with covariance derived from `sigma_m` (default 50 m horizontal, 100 m vertical); ESKF seeds the nominal position and writes the position block of the error covariance to match `sigma_m^2`.
|
||||
5. **FC GPS is a secondary, gated input.** If the FC EKF later produces a GPS reading (in-flight or at takeoff), it is fused through the existing `add_pose_anchor` machinery only after passing the three-part gate of Principle #11 — **including the ≤ 200 m bounded-delta check against the companion's last emitted `PoseEstimate`**. Real GPS that passes the gate is one more measurement, never an override.
|
||||
6. **Failure modes.** If the Manifest has no `takeoff_origin` AND the FC EKF has no usable GPS at takeoff, C5 stays in `INITIALIZING` and the FC adapter (C8) emits a non-fused source label; the FT-P-11 takeoff-abort policy (AZ-419 amended) applies. If the Manifest has `takeoff_origin` AND the FC EKF GPS is wildly inconsistent with it at takeoff (e.g., > 200 m), the operator origin wins and the FC GPS is logged as suspect — this is the GPS-spoofed-at-takeoff case and is the entire point of this ADR.
|
||||
|
||||
**Alternatives considered**:
|
||||
|
||||
1. **Keep FC EKF as primary** (status quo of AZ-419) — rejected: cannot survive GPS-denied takeoff, which is in scope per Principles #1 and #9. Field reports of pre-launch jamming make this a realistic, not edge-case, failure mode.
|
||||
2. **Operator types the origin into a CLI prompt at build-cache time** — rejected: duplicates information the Mission Planner UI already captures, drifts from the canonical route, and breaks if the operator re-plans without re-typing. The `Flight` DTO is the single source of truth.
|
||||
3. **Pull `Flight` from the companion at runtime over a back-channel** — rejected: violates Principle #9 (denied-environment operation; no egress from the companion to anything other than the FC). The flights service is an **operator-workstation** concern only.
|
||||
4. **Treat operator origin as a hard assignment instead of a prior** — rejected: a hard assignment cannot be fused with a later high-quality posterior, breaks ADR-003's "honest covariance" property, and prevents the `add_pose_anchor` fusion path from ever correcting the origin if it was authored with imprecision.
|
||||
|
||||
**Consequences**:
|
||||
|
||||
- AZ-419 (FT-P-11) is amended: the primary cold-start path is operator-origin-from-manifest; FC-EKF-GPS is the fallback path with its own sub-AC.
|
||||
- C10 contracts gain a `takeoff_origin` field in `BuildRequest`, `Manifest`, and the verifier's validation set (AZ-323 / AZ-325 / AZ-324). Contract version bumps to v1.1.0.
|
||||
- C5 gains a `set_takeoff_origin(origin, sigma_m)` method on the `StateEstimator` protocol (AZ-490). Protocol contract version bumps to v1.1.0.
|
||||
- C12 gains the `FlightsApiClient` boundary + offline `--flight-file` path (AZ-489).
|
||||
- Principle #11 (the spoofed-GPS gate) is extended with the bounded-delta clause; the gate now serves both takeoff and mid-flight.
|
||||
- The companion binary's network surface is unchanged — only C12 (operator-side, separate binary) talks to the flights service.
|
||||
Reference in New Issue
Block a user