[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:
Oleksandr Bezdieniezhnykh
2026-05-12 01:28:05 +03:00
parent db27e25630
commit e0be591b06
20 changed files with 875 additions and 221 deletions
+92 -42
View File
@@ -9,8 +9,8 @@
| # | Flow Name | Trigger | Primary Components | Criticality |
|---|-----------|---------|--------------------|-------------|
| F1 | Pre-flight cache provisioning | Operator runs C12 cache-build CLI on workstation | C12 (operator), C11 `TileDownloader`, [[`satellite-provider`]], C10, C6, C7 | High |
| F2 | Takeoff load | Companion boot detected by FC `MAV_STATE` ARMED OR companion process start with armed FC | C10, C7, C8 (signing handshake), C13 | High |
| F1 | Pre-flight cache provisioning | Operator runs C12 cache-build CLI on workstation with `--flight-id <Guid>` (online) or `--flight-file <path>` (offline). The flight was previously authored in the parent-suite Mission Planner UI (`suite/ui`) and persisted to the parent-suite `flights` REST service | C12 (operator), C12 `FlightsApiClient` (operator-side, AZ-489), [[`flights` REST service]], C11 `TileDownloader`, [[`satellite-provider`]], C10, C6, C7 | High |
| F2 | Takeoff load | Companion boot detected by FC `MAV_STATE` ARMED OR companion process start with armed FC | C10, C7, C8 (signing handshake), C5 `set_takeoff_origin` (operator-origin warm-start, AZ-490), C13 | High |
| F3 | Steady-state per-frame estimation | Nav camera frame received (3 Hz nominal) | C1, C2, C2.5, C3, C3.5, C4, C5, C8 (out), C13 | High |
| F4 | Mid-flight tile generation + local cache write | Successful satellite-anchored frame with quality metadata above threshold | C5, C6, C13 (no C8/C11 path — C11 `TileUploader` is not loaded in the airborne image) | High |
| F5 | Visual blackout + spoofed-GPS failsafe | Camera unusable AND/OR FC GPS reports denial/spoof | C1, C5, C8, C13 (degraded-mode escalation per AC-NEW-8) | High |
@@ -43,17 +43,19 @@
### Description
The operator builds (or refreshes) the per-mission cache before takeoff. F1 has **two phases** sequenced by C12 OperatorTool:
The operator builds (or refreshes) the per-mission cache before takeoff. F1 has **three phases** sequenced by C12 OperatorTool:
- **Phase 1Tile download (C11 `TileDownloader`)**: fetch tiles from `satellite-provider` for the operational area; apply sector-classified freshness rules (AC-NEW-6) and resolution gate (RESTRICT-SAT-4); write tile rows + JPEGs into C6.
- **Phase 2Cache artifact build (C10 CacheProvisioner)**: read the populated C6 store; compile/deserialize TRT engines via C7; batch-generate descriptors via the C2 backbone; atomically write the FAISS HNSW index with SHA-256 sidecars; write the Manifest hashing model + calibration + corpus + sector classification.
- **Phase 0Flight resolve (C12 `FlightsApiClient`, AZ-489)**: read the operator-authored `Flight` (ordered waypoints + altitudes) either from the parent-suite `flights` REST service (`--flight-id <Guid>`) or from a local JSON export (`--flight-file <path>`). Compute the bounding box as the envelope of waypoint lat/lon plus a configurable buffer (default 1 km). Extract `Flight.waypoints[0].(lat, lon, alt)` as the **takeoff origin**. Both are passed downstream as `BuildRequest` fields.
- **Phase 1Tile download (C11 `TileDownloader`)**: fetch tiles from `satellite-provider` for the bbox computed in Phase 0; apply sector-classified freshness rules (AC-NEW-6) and resolution gate (RESTRICT-SAT-4); write tile rows + JPEGs into C6.
- **Phase 2 — Cache artifact build (C10 CacheProvisioner)**: read the populated C6 store; compile/deserialize TRT engines via C7; batch-generate descriptors via the C2 backbone; atomically write the FAISS HNSW index with SHA-256 sidecars; write the Manifest hashing model + calibration + corpus + sector classification **+ takeoff origin** (D-C10-1 idempotence; ADR-010).
This flow is offline and not time-critical. **Only Phase 1 reaches `satellite-provider`**and it runs on the operator workstation, which is the only host that holds the TLS + service-internal API key. The companion never reaches `satellite-provider` directly.
This flow is offline and not time-critical. **Only Phase 0 reaches `flights` REST and Phase 1 reaches `satellite-provider`**both run on the operator workstation, which is the only host that holds TLS + service-internal credentials. The companion never reaches either service directly (Principle #9 — denied-environment operation).
### Preconditions
- Operator workstation has network reach to `satellite-provider` (TLS + service-internal API key).
- Operator has classified the operational area (`active_conflict | stable_rear`) — drives the freshness threshold (AC-8.2 / AC-NEW-6).
- **Mission already authored in the parent-suite Mission Planner UI (`suite/ui`)** and persisted to the parent-suite `flights` REST service. Operator knows the `Flight` GUID (online path) OR has a JSON export of the same DTO shape on disk (offline path).
- Camera calibration JSON for the deployed unit is available (`adti20.<unit-id>.json` from D-PROJ-1 hybrid).
- Companion is connected to the operator workstation (USB or Ethernet) and writable.
- Available cache budget on the companion's NVM is ≥ the projected `≤ 10 GB` per AC-8.3.
@@ -64,6 +66,8 @@ This flow is offline and not time-critical. **Only Phase 1 reaches `satellite-pr
sequenceDiagram
participant Operator
participant C12OperatorTool as C12 Operator Tool (workstation)
participant FlightsClient as C12 FlightsApiClient (workstation, AZ-489)
participant FlightsApi as [[flights REST service]] (.NET 8)
participant C11TileDownloader as C11 TileDownloader (workstation)
participant SatelliteProvider as [[satellite-provider]] (.NET 8)
participant C6TileStore as C6 TileStore + DescriptorIndex (Postgres + filesystem + FAISS)
@@ -71,20 +75,31 @@ sequenceDiagram
participant C7Inference as C7 InferenceRuntime
participant C2Backbone as C2 VPR backbone (TensorRT)
Operator->>C12OperatorTool: build_cache(area, sector_class, calibration_file)
Operator->>C12OperatorTool: build_cache --flight-id GUID [--flight-file PATH] sector_class calibration_file
alt online (flight-id)
C12OperatorTool->>FlightsClient: fetch_flight(GUID)
FlightsClient->>FlightsApi: GET /flights/{id} + GET /flights/{id}/waypoints
FlightsApi-->>FlightsClient: Flight DTO (waypoints, altitudes)
else offline (flight-file)
C12OperatorTool->>FlightsClient: load_flight_file(PATH)
FlightsClient->>FlightsClient: parse JSON into FlightDto
end
FlightsClient->>FlightsClient: bbox = envelope(waypoints.lat, waypoints.lon) + buffer
FlightsClient->>FlightsClient: takeoff_origin = waypoints[0].(lat, lon, alt)
FlightsClient-->>C12OperatorTool: (bbox, takeoff_origin, flight_id)
C12OperatorTool->>C11TileDownloader: download_tiles_for_area(bbox, zooms, sector_class)
C11TileDownloader->>SatelliteProvider: GET /api/satellite/tiles?bbox=&zoom=
SatelliteProvider-->>C11TileDownloader: Tile blobs + metadata (paged)
C11TileDownloader->>C11TileDownloader: filter by AC-NEW-6 freshness + RESTRICT-SAT-4 resolution
C11TileDownloader->>C6TileStore: write tiles to ./tiles/{zoomLevel}/{x}/{y}.jpg + Postgres rows (source='googlemaps')
C11TileDownloader-->>C12OperatorTool: DownloadBatchReport (counts, freshness summary)
C12OperatorTool->>C10Provisioner: build_cache_artifacts(bbox, zooms, sector_class, calibration)
C12OperatorTool->>C10Provisioner: build_cache_artifacts(bbox, zooms, sector_class, calibration, takeoff_origin, flight_id)
C10Provisioner->>C7Inference: load VPR backbone ONNX
C7Inference-->>C10Provisioner: TRT engine compiled (cached per SM/JP/TRT/precision tuple)
C10Provisioner->>C2Backbone: per-tile descriptor generation (batched on Jetson, reads tiles from C6)
C2Backbone-->>C10Provisioner: descriptor matrix (FP16/INT8 per D-C7-1)
C10Provisioner->>C6TileStore: faiss.write_index (HNSW) + atomicwrites + SHA-256 content-hash
C10Provisioner->>C10Provisioner: write Manifest (hash of model + calibration + corpus + sector_class)
C10Provisioner->>C10Provisioner: write Manifest (hash of model + calibration + corpus + sector_class + takeoff_origin)
C10Provisioner-->>C12OperatorTool: BuildReport (counts, hashes)
C12OperatorTool-->>Operator: PASS / FAIL summary
```
@@ -93,8 +108,12 @@ sequenceDiagram
```mermaid
flowchart TD
Start([Operator invokes C12 build]) --> Classify[Operator classifies sector active_conflict OR stable_rear]
Classify --> InvokeC11[C12 invokes C11 TileDownloader]
Start([Operator invokes C12 build with --flight-id or --flight-file]) --> ResolveFlight[C12 FlightsApiClient fetches Flight by GUID or reads JSON export]
ResolveFlight --> FlightOk{Flight resolved + at least 1 waypoint?}
FlightOk -->|no| RefuseBuild[Refuse build with explicit error to operator]
FlightOk -->|yes| ComputeBbox[Compute bbox as envelope of waypoint lat/lon + buffer; take waypoints[0] as takeoff origin]
ComputeBbox --> Classify[Operator classifies sector active_conflict OR stable_rear]
Classify --> InvokeC11[C12 invokes C11 TileDownloader with computed bbox]
InvokeC11 --> Download[C11 GET /api/satellite/tiles for bbox + zoom]
Download --> FreshnessFilter{Freshness ok per AC-8.2 + AC-NEW-6?}
FreshnessFilter -->|stale and stable_rear| RejectOrDowngrade[Reject or downgrade tile]
@@ -112,40 +131,51 @@ flowchart TD
ReuseEngine --> Descriptors
BuildEngine --> Descriptors[C10 batches each tile through C2 backbone for descriptors]
Descriptors --> WriteIndex[faiss.write_index HNSW + atomicwrites + SHA-256 content-hash]
WriteIndex --> WriteManifest[Write Manifest with hash of model + calibration + corpus + sector_class]
WriteIndex --> WriteManifest[Write Manifest with hash of model + calibration + corpus + sector_class + takeoff_origin]
WriteManifest --> ManifestHashCheck{Idempotence check D-C10-1: same manifest hash as last build?}
ManifestHashCheck -->|same| SkipRebuild[Skip rebuild and emit no-op report]
ManifestHashCheck -->|different| Done([Provisioning complete; cache + engines + manifest staged])
SkipRebuild --> Done
RefuseBuild --> Done
```
### Data flow
| Step | From | To | Data | Format |
|------|------|----|------|--------|
| 1 | Operator | C12 | (`bounding_box`, `zoom_levels`, `sector_class`, `calibration_path`) | CLI args / GUI form |
| 2 | C12 | C11 `TileDownloader` | `DownloadRequest` | in-process call |
| 3 | C11 | `satellite-provider` REST | `GET /api/satellite/tiles?bbox=…&zoom=…` | HTTPS query |
| 4 | `satellite-provider` | C11 | Paged tile blobs + metadata rows | JPEG + JSON metadata |
| 5 | C11 | C6 filesystem (over USB/Eth) | Tile JPEG bodies | `./tiles/{zoomLevel}/{x}/{y}.jpg` |
| 6 | C11 | C6 PostgreSQL | Tile metadata rows (`source='googlemaps'`) | SQL INSERT (mirror of `satellite-provider`'s `tiles` table) |
| 7 | C12 | C10 `CacheProvisioner` | `BuildRequest` | in-process call (operator-tool side); RPC over USB/Eth to companion runner |
| 8 | C10 → C7 | TRT engine cache | TRT engines | `.engine` files keyed by `(SM, JP, TRT, precision)` (D-C10-7) |
| 9 | C2 backbone (driven by C10) | C6 FAISS index | Descriptor matrix | `.index` (FAISS HNSW), atomicwrites, SHA-256 sidecar |
| 10 | C10 | filesystem | Manifest | YAML or JSON; carries hashes |
| 0a | Operator | C12 | (`flight_id` OR `flight_file`, `zoom_levels`, `sector_class`, `calibration_path`) | CLI args / GUI form |
| 0b | C12 `FlightsApiClient` (online) | `flights` REST | `GET /flights/{id}` + `GET /flights/{id}/waypoints` | HTTPS GET |
| 0c | `flights` REST | C12 `FlightsApiClient` | `Flight` + ordered `Waypoint[]` (lat / lon / alt / objective / source) | JSON DTOs |
| 0d | C12 `FlightsApiClient` (offline) | filesystem | `flight_file` JSON in the same DTO shape | JSON read |
| 0e | C12 `FlightsApiClient` | C12 | `(bbox, takeoff_origin, flight_id)` | in-process |
| 1 | C12 | C11 `TileDownloader` | `DownloadRequest(bbox, zoom_levels, sector_class)` | in-process call |
| 2 | C11 | `satellite-provider` REST | `GET /api/satellite/tiles?bbox=…&zoom=…` | HTTPS query |
| 3 | `satellite-provider` | C11 | Paged tile blobs + metadata rows | JPEG + JSON metadata |
| 4 | C11 | C6 filesystem (over USB/Eth) | Tile JPEG bodies | `./tiles/{zoomLevel}/{x}/{y}.jpg` |
| 5 | C11 | C6 PostgreSQL | Tile metadata rows (`source='googlemaps'`) | SQL INSERT (mirror of `satellite-provider`'s `tiles` table) |
| 6 | C12 | C10 `CacheProvisioner` | `BuildRequest(bbox, zoom_levels, sector_class, calibration_path, takeoff_origin, flight_id)` | in-process call (operator-tool side); RPC over USB/Eth to companion runner |
| 7 | C10 → C7 | TRT engine cache | TRT engines | `.engine` files keyed by `(SM, JP, TRT, precision)` (D-C10-7) |
| 8 | C2 backbone (driven by C10) | C6 FAISS index | Descriptor matrix | `.index` (FAISS HNSW), atomicwrites, SHA-256 sidecar |
| 9 | C10 | filesystem | Manifest (carries `takeoff_origin` + hashes) | YAML or JSON |
### Error scenarios
| Error | Where | Detection | Recovery |
|-------|-------|-----------|----------|
| `satellite-provider` unreachable | Step 3 | HTTP timeout / 5xx | C11 `TileDownloader` fails with explicit error; operator retries when network is available; takeoff blocked |
| Tile fails freshness | Step 4 (C11) | `tile.capture_timestamp` vs `sector_class` threshold | Reject (active_conflict) or downgrade-no-`satellite_anchored`-label (rear), per AC-NEW-6; counts surface in `DownloadBatchReport` |
| Resolution below 0.5 m/px | Step 4 (C11) | Tile metadata GSD check (RESTRICT-SAT-4) | Reject; report; takeoff blocked |
| Insufficient cache budget | Step 5 (C11) | Filesystem free-space check pre-write | Fail fast with explicit budget delta; no partial write |
| C6 missing tiles for requested bbox/zoom | Step 7 (C10) | C10's pre-build scan finds < expected tile count | Surface as `BuildReport.failure` instructing operator to re-run C11 `TileDownloader`; do **not** trigger network fetch from C10 |
| Engine compile failure | Step 8 | Polygraphy / trtexec exit code; no output `.engine` | Surface error to operator; takeoff blocked; **never silently fall back** |
| Descriptor generation OOM on Jetson | Step 9 | CUDA OOM | Halve batch size and retry once; if still OOM, surface to operator |
| Atomic-write or SHA-256 mismatch | Step 9 | `atomicwrites` rollback or content-hash sidecar mismatch | Mark cache invalid; rebuild from staged tiles; if persistent, surface to operator |
| `flights` REST unreachable (online path) | Step 0b | HTTP timeout / connection refused | Fail explicitly; instruct operator to retry online or use `--flight-file` offline path; takeoff blocked |
| `flights` REST 401/403 (online path) | Step 0b | HTTP 401/403 | Fail with explicit error; instruct operator to refresh suite credentials; takeoff blocked. Never silently fall back |
| `flights` REST 404 (online path) | Step 0b | HTTP 404 | Fail with explicit message naming the unknown `flight_id`; takeoff blocked |
| Flight file malformed (offline path) | Step 0d | JSON parse failure / schema mismatch | Fail with line / field reference; instruct operator to re-export from Mission Planner UI; takeoff blocked |
| Flight has zero waypoints | Step 0e | Post-fetch validation | Fail explicitly; cannot derive bbox or takeoff origin; takeoff blocked |
| Flight bbox exceeds cache budget | Step 0e | Pre-Phase-1 bbox area vs AC-8.3 budget projection | Fail with budget delta; operator must re-plan a smaller route in Mission Planner UI; takeoff blocked |
| `satellite-provider` unreachable | Step 2 | HTTP timeout / 5xx | C11 `TileDownloader` fails with explicit error; operator retries when network is available; takeoff blocked |
| Tile fails freshness | Step 3 (C11) | `tile.capture_timestamp` vs `sector_class` threshold | Reject (active_conflict) or downgrade-no-`satellite_anchored`-label (rear), per AC-NEW-6; counts surface in `DownloadBatchReport` |
| Resolution below 0.5 m/px | Step 3 (C11) | Tile metadata GSD check (RESTRICT-SAT-4) | Reject; report; takeoff blocked |
| Insufficient cache budget | Step 4 (C11) | Filesystem free-space check pre-write | Fail fast with explicit budget delta; no partial write |
| C6 missing tiles for requested bbox/zoom | Step 6 (C10) | C10's pre-build scan finds < expected tile count | Surface as `BuildReport.failure` instructing operator to re-run C11 `TileDownloader`; do **not** trigger network fetch from C10 |
| Engine compile failure | Step 7 | Polygraphy / trtexec exit code; no output `.engine` | Surface error to operator; takeoff blocked; **never silently fall back** |
| Descriptor generation OOM on Jetson | Step 8 | CUDA OOM | Halve batch size and retry once; if still OOM, surface to operator |
| Atomic-write or SHA-256 mismatch | Step 8 | `atomicwrites` rollback or content-hash sidecar mismatch | Mark cache invalid; rebuild from staged tiles; if persistent, surface to operator |
| Tampered cache (post-write, pre-takeoff) | (caught at takeoff in F2, not here) | F2 SHA-256 content-hash gate | F2 refuses takeoff (IT-7) |
### Performance expectations
@@ -203,11 +233,19 @@ sequenceDiagram
end
Companion->>FC: subscribe to FC IMU + attitude + GPS health (telemetry)
FC-->>Companion: first telemetry frame
Companion->>FC: query FC EKF last valid GPS + IMU-extrapolated pose (AC-5.1)
FC-->>Companion: warm-start pose
Companion->>Pipeline: warm with calibration + warm-start pose
Note over Companion,Pipeline: Cold-start ladder (ADR-010, AZ-490). Operator-origin from Manifest is primary; FC EKF GPS is secondary
alt Manifest carries takeoff_origin (AZ-490 primary path)
Companion->>Pipeline: C5.set_takeoff_origin(manifest.takeoff_origin, sigma_m) BEFORE any add_vio / add_fc_imu
else Manifest has no takeoff_origin AND FC EKF GPS is valid (AZ-419 secondary path)
Companion->>FC: query FC EKF last valid GPS + IMU-extrapolated pose (AC-5.1)
FC-->>Companion: warm-start pose
Companion->>Pipeline: C5.set_takeoff_origin(fc_gps_origin, fc_gps_sigma)
else No origin available
Companion-->>Companion: stay INITIALIZING; FT-P-11 takeoff-abort policy (AZ-419 amended)
end
Companion->>Pipeline: warm pipeline with calibration
Pipeline-->>Companion: ready (no estimate emitted yet)
Companion->>Fdr: open per-flight FDR record; log signing key rotation event
Companion->>Fdr: open per-flight FDR record; log signing key rotation event + chosen cold-start origin source
Note over Companion: Wait for first nav frame (F3 entry)
```
@@ -227,11 +265,19 @@ flowchart TD
FcDetect -->|iNav| InavOpen[Open MSP2 channel unsigned residual risk]
ApSign --> SignOk{Signing handshake OK?}
SignOk -->|no| RefuseTakeoff
SignOk -->|yes| WarmStart
InavOpen --> WarmStart[Query FC EKF last valid GPS + IMU-extrapolated pose AC-5.1]
WarmStart --> WarmPipeline[Warm C1 + C2 + C2.5 + C3 + C3.5 + C4 + C5 with calibration + warm-start pose]
WarmPipeline --> OpenFdr[C13 opens per-flight FDR; logs signing key rotation event]
SignOk -->|yes| OriginGate
InavOpen --> OriginGate{Manifest carries takeoff_origin?}
OriginGate -->|yes ADR-010 AZ-490 primary| OperatorOrigin[C5.set_takeoff_origin manifest.takeoff_origin sigma_m]
OriginGate -->|no| FcEkfGate{FC EKF reports valid non-spoofed GPS?}
FcEkfGate -->|yes AZ-419 secondary| FcOrigin[C5.set_takeoff_origin fc_gps_origin fc_gps_sigma]
FcEkfGate -->|no| NoOrigin[Stay INITIALIZING and apply FT-P-11 takeoff-abort policy]
OperatorOrigin --> WarmPipeline
FcOrigin --> WarmPipeline
NoOrigin --> Refuse2[Refuse takeoff with FDR record of missing origin]
WarmPipeline[Warm C1 + C2 + C2.5 + C3 + C3.5 + C4 + C5 with calibration]
WarmPipeline --> OpenFdr[C13 opens per-flight FDR; logs signing key rotation event + chosen origin source]
OpenFdr --> Ready([Ready; awaiting first nav frame])
Refuse2 --> RefuseTakeoff
```
### Data flow
@@ -239,12 +285,13 @@ flowchart TD
| Step | From | To | Data | Format |
|------|------|----|------|--------|
| 1 | Companion | C10 | (`manifest_path`) | filesystem read |
| 2 | C10 | filesystem | content-hash sidecars | SHA-256 hex digests |
| 2 | C10 | filesystem | content-hash sidecars + `takeoff_origin` | SHA-256 hex digests + `LatLonAlt` in Manifest |
| 3 | Companion | FAISS | `.index` mmap pointer | C++ FAISS API |
| 4 | Companion | C7 / TensorRT | `.engine` deserialize | TensorRT IRuntime |
| 5 | Companion | FC (AP) | signing seed + handshake | MAVLink 2.0 signing |
| 6 | FC | Companion | warm-start pose + IMU/attitude/GPS health | MAVLink (AP) / MSP2 + MAVLink outbound (iNav) |
| 7 | Companion | C13 FDR | startup record (config snapshot, signing key rotation event, content-hash digests) | FDR record |
| 7 | Companion | C5 `StateEstimator` (AZ-490) | `set_takeoff_origin(origin, sigma_m)` with origin = `manifest.takeoff_origin` (primary) OR FC-EKF GPS (secondary) | in-process Protocol method |
| 8 | Companion | C13 FDR | startup record (config snapshot, signing key rotation event, content-hash digests, chosen cold-start origin source) | FDR record |
### Error scenarios
@@ -255,8 +302,9 @@ flowchart TD
| TRT deserialize failure | Step 4 | TensorRT API error | Refuse takeoff; report mismatched `(SM, JP, TRT, precision)` tuple to operator |
| Signing handshake fail (AP) | Step 5 | Handshake timeout / signed-message rejection | Refuse takeoff; clear-text reason via STATUSTEXT (handshake never succeeded → unsigned STATUSTEXT is acceptable for this case only) |
| FC unreachable | Step 6 | UART/USB read timeout | Retry with backoff; after `N` retries refuse takeoff |
| EKF returns no warm-start pose | Step 6 | Empty `GLOBAL_POSITION_INT` and no IMU prior | Defer pipeline warm-up until first valid prior; bound the wait by AC-NEW-1 budget; if exceeded, refuse takeoff |
| FDR open failure | Step 7 | Filesystem write error | Refuse takeoff (per AC-NEW-3 every payload class must be present from t=0) |
| Manifest has no `takeoff_origin` AND EKF returns no warm-start pose | Step 7 (ADR-010, AZ-490) | Both primary (manifest) and secondary (FC EKF) origin paths unavailable | Refuse takeoff; FDR records "no cold-start origin available"; FT-P-11 takeoff-abort policy applies. Bound the wait by AC-NEW-1 budget before final refusal |
| Manifest has `takeoff_origin` but FC EKF GPS disagrees by > 200 m at takeoff | Step 7 (ADR-010 Principle #11 bounded-delta) | Operator origin vs FC GPS comparison after first FC telemetry frame | Operator origin wins; FC GPS is logged as suspect (likely spoofed-at-takeoff); proceed to warm pipeline with the operator origin |
| FDR open failure | Step 8 | Filesystem write error | Refuse takeoff (per AC-NEW-3 every payload class must be present from t=0) |
### Performance expectations
@@ -698,6 +746,8 @@ When the FC reports GPS denial/spoof while the companion estimate is healthy, th
This flow is a **hot path**: AC-NEW-2 ≤ 3 s p95 from spoof onset to companion estimate becoming primary. Status: D-C8-2 = (b) is `Selected with runtime gate` — IT-3 SITL validation is the lock gate (Mode B Fact #111).
**Reverse path — mid-flight FC GPS re-promotion (ADR-010, Principle #11 amended)**: when the FC's GPS subsequently recovers in flight, the companion does **not** auto-yield. The FC GPS is fused back into C5 via `add_pose_anchor` **only after** the three-part gate fires: (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 bounded-delta gate — it catches "FC reports stable GPS but the value is wrong". When the gate passes, the FC GPS becomes one more anchor source, not an override. The source-set switch back to set 1 happens through the existing AC-NEW-8 path.
### Preconditions
- ArduPilot Plane FC (D-C8-2 only applies to AP path).