mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 06:41:14 +00:00
[AZ-490] C5 set_takeoff_origin entrypoint + bounded-delta GPS gate
Add operator warm-start path to C5 StateEstimator Protocol and both
implementations (GtsamIsam2StateEstimator, EskfStateEstimator), plus
the third clause of the AZ-385 spoof-promotion gate.
- StateEstimator Protocol: set_takeoff_origin(origin, sigma_horiz_m,
sigma_vert_m) -> None.
- iSAM2: PriorFactorPose3 at origin with diagonal sigmas, single
isam2.update().
- ESKF: zero _nominal_pos, overwrite _P position block with sigma**2.
- SourceLabelStateMachine.process_gps_sample bounded-delta clause:
WgsConverter.horizontal_distance_m vs smoother estimate; reject
resets the dwell-time counter so AZ-385 cannot re-promote off bad
GPS.
- New EstimatorAlreadyStartedError (StateEstimatorConfigError
subclass) on late call after first add_*.
- C5StateConfig: spoof_promotion_bounded_delta_m=200,
default_takeoff_origin_sigma_horiz_m=5,
default_takeoff_origin_sigma_vert_m=10.
- New GpsSample DTO + WgsConverter.horizontal_distance_m helper.
- 4 new FDR kinds (cold_start_origin.{set,unavailable},
gps_bounded_delta.{accept,reject}) registered in AZ-272 schema.
- 33 new unit tests cover AC-1..AC-15; full repo 750 passed / 2
skipped (pre-existing CI tooling skips).
Docs synced: protocol contract, C5 component description,
architecture, glossary, system-flows, C10 provisioning description.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -40,7 +40,7 @@ The system is a **Jetson Orin Nano Super-hosted onboard companion** that deliver
|
||||
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 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.
|
||||
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_horiz_m, sigma_vert_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.
|
||||
|
||||
@@ -207,7 +207,7 @@ source repo
|
||||
| `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 |
|
||||
| `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_horiz_m, sigma_vert_m)` (AZ-490) | C12 → C10 Manifest → C5 |
|
||||
|
||||
**Key relationships**:
|
||||
|
||||
@@ -619,7 +619,7 @@ This decision is made on **technical grounds only**. Component licenses (BSD/Apa
|
||||
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`.
|
||||
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_horiz_m, sigma_vert_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` at `Pose3.Identity()` (the operator origin BECOMES the local-ENU (0,0,0) anchor) with diagonal sigmas `[5°, 5°, 5°, sigma_horiz_m, sigma_horiz_m, sigma_vert_m]`; ESKF seeds the nominal position to (0,0,0) and writes the position block of the error covariance to `diag(sigma_horiz_m², sigma_horiz_m², sigma_vert_m²)`. Defaults are `sigma_horiz_m = 5.0 m`, `sigma_vert_m = 10.0 m` from `C5StateConfig`.
|
||||
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.
|
||||
|
||||
@@ -634,7 +634,7 @@ This decision is made on **technical grounds only**. Component licenses (BSD/Apa
|
||||
|
||||
- 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.
|
||||
- C5 gains a `set_takeoff_origin(origin, sigma_horiz_m, sigma_vert_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.
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
| Method | Input | Output | Async | Error Types |
|
||||
|--------|-------|--------|-------|-------------|
|
||||
| `set_takeoff_origin` (AZ-490, ADR-010) | `origin: LatLonAlt, sigma_m: float` | `None` | No | `EstimatorConfigError`, `EstimatorFatalError` |
|
||||
| `set_takeoff_origin` (AZ-490, ADR-010) | `origin: LatLonAlt, sigma_horiz_m: float, sigma_vert_m: float` | `None` | No | `StateEstimatorConfigError`, `EstimatorAlreadyStartedError` |
|
||||
| `add_vio` | `VioOutput` | `None` | No | `EstimatorDegradedError`, `EstimatorFatalError` |
|
||||
| `add_pose_anchor` | `PoseEstimate` | `None` | No | `EstimatorDegradedError`, `EstimatorFatalError` |
|
||||
| `add_fc_imu` | `ImuWindow` | `None` | No | `EstimatorDegradedError` |
|
||||
@@ -77,7 +77,7 @@ C5 is bounded by design — no unbounded growth.
|
||||
|
||||
**State Management**:
|
||||
- iSAM2 graph + Values + Marginals lifecycle for the flight.
|
||||
- Cold-start ladder (ADR-010, AZ-490): `set_takeoff_origin(origin, sigma_m)` MUST be invoked before any `add_vio` / `add_fc_imu` / `add_pose_anchor` call. 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 `sigma_m^2`. The method is idempotent within `INIT` state (re-invocation overwrites the prior); once the estimator transitions to `TRACKING`, further calls raise `EstimatorConfigError`.
|
||||
- Cold-start ladder (ADR-010, AZ-490): `set_takeoff_origin(origin, sigma_horiz_m, sigma_vert_m)` MUST be invoked before any `add_vio` / `add_fc_imu` / `add_pose_anchor` call. The cold-start window closes on the first `add_*` call. iSAM2 attaches a `PriorFactorPose3` at `Pose3.Identity()` (operator origin BECOMES local-ENU (0,0,0)) with diagonal sigmas `[5°, 5°, 5°, sigma_horiz_m, sigma_horiz_m, sigma_vert_m]`; ESKF seeds the nominal position to (0,0,0) and writes the position-block of the error covariance to `diag(sigma_horiz_m², sigma_horiz_m², sigma_vert_m²)`. The method is **strictly idempotent on identical args** — re-invocation with byte-equal `(origin, sigma_horiz_m, sigma_vert_m)` is a no-op; re-invocation with **different** args raises `StateEstimatorConfigError`. Once the cold-start window closes, further calls raise `EstimatorAlreadyStartedError` (subclass of `StateEstimatorConfigError`). Defaults `default_takeoff_origin_sigma_horiz_m = 5.0`, `default_takeoff_origin_sigma_vert_m = 10.0` live in `C5StateConfig`.
|
||||
- Source-label state machine: tracks the AC-NEW-2 / AC-NEW-8 spoofing-promotion gate (≥10 s + visual consistency check + ≤ 200 m bounded-delta before re-promoting a previously-spoofed FC GPS source).
|
||||
- Last-anchor-age timer for AC-1.3 binning.
|
||||
|
||||
@@ -90,7 +90,7 @@ C5 is bounded by design — no unbounded growth.
|
||||
| Eigen | matches GTSAM | Lie-algebra math |
|
||||
|
||||
**Error Handling Strategy**:
|
||||
- `EstimatorConfigError`: `set_takeoff_origin` called after `TRACKING` state, OR called with a malformed `LatLonAlt` / non-positive `sigma_m`. Caller must surface to operator; takeoff blocked.
|
||||
- `StateEstimatorConfigError`: `set_takeoff_origin` called with a malformed `LatLonAlt` (out of WGS-84 bounds / non-finite) OR with non-positive / non-finite sigmas, OR re-called inside the cold-start window with conflicting args. `EstimatorAlreadyStartedError` (a `StateEstimatorConfigError` subclass): `set_takeoff_origin` called after the first `add_*` call sealed the cold-start window. Caller must surface to operator; takeoff blocked.
|
||||
- `EstimatorDegradedError`: factor add yielded poor convergence; covariance inflated; emit `EstimatorOutput` with degraded label.
|
||||
- `EstimatorFatalError`: iSAM2 numerical failure, KEYFRAME_LIMIT exceeded, etc.; emit no `EstimatorOutput` for this tick. AC-5.2 fallback (3 s no estimate → FC IMU-only) applies.
|
||||
- Spoof-promotion gate (Principle #11 amended, AZ-385 + AZ-490 follow-up): never re-introduce a previously-spoofed FC GPS source until ALL THREE hold — (i) FC `gps_health == STABLE_NON_SPOOFED` for ≥ 10 s, (ii) the next satellite-anchored frame agrees with the FC GPS within a configurable tolerance, AND (iii) the FC's reported position is within ≤ 200 m of the companion's last emitted `PoseEstimate`. The same gate is applied at takeoff when a Manifest `takeoff_origin` is present: an FC GPS reading that disagrees with the operator origin by > 200 m is logged as suspect and the operator origin wins. Document every reject in FDR + GCS STATUSTEXT.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 1. High-Level Overview
|
||||
|
||||
**Purpose**: build the **model-derived** pre-flight cache artifacts on top of an already-populated tile store, and verify them at takeoff. After C11 `TileDownloader` has fetched tiles into C6, C10 orchestrates: compile/deserialize TensorRT engines via C7 → batch each tile through C2's backbone for descriptors → atomically write FAISS HNSW index with SHA-256 sidecars (D-C10-3) → write Manifest with hash of (model + calibration + corpus + sector_class **+ takeoff_origin**) for D-C10-1 idempotence. The `takeoff_origin` is supplied by C12 (derived from `Flight.waypoints[0]` via the `FlightsApiClient`, ADR-010 + AZ-489); C10 treats it as one more identity field and bakes it into both the Manifest body and the manifest-hash. At F2 takeoff load, run `verify_manifest` (D-C10-3 SHA-256 content-hash gate) before allowing the system to arm; the verifier also surfaces `takeoff_origin` so the companion's composition root can pass it to `C5.set_takeoff_origin(origin, sigma_m)` before any sensor sample (AZ-490).
|
||||
**Purpose**: build the **model-derived** pre-flight cache artifacts on top of an already-populated tile store, and verify them at takeoff. After C11 `TileDownloader` has fetched tiles into C6, C10 orchestrates: compile/deserialize TensorRT engines via C7 → batch each tile through C2's backbone for descriptors → atomically write FAISS HNSW index with SHA-256 sidecars (D-C10-3) → write Manifest with hash of (model + calibration + corpus + sector_class **+ takeoff_origin**) for D-C10-1 idempotence. The `takeoff_origin` is supplied by C12 (derived from `Flight.waypoints[0]` via the `FlightsApiClient`, ADR-010 + AZ-489); C10 treats it as one more identity field and bakes it into both the Manifest body and the manifest-hash. At F2 takeoff load, run `verify_manifest` (D-C10-3 SHA-256 content-hash gate) before allowing the system to arm; the verifier also surfaces `takeoff_origin` so the companion's composition root can pass it to `C5.set_takeoff_origin(origin, sigma_horiz_m, sigma_vert_m)` before any sensor sample (AZ-490).
|
||||
|
||||
**C10 does NOT touch `satellite-provider`.** Tile I/O — both download (F1 inbound) and post-landing upload (F10) — lives in C11 (Tile Manager). C10 reads tiles from C6, writes engines + descriptors + manifest to filesystem and Postgres. The split is operational: C11 carries the operator-side network identity (TLS API key for download, per-flight signing key for upload) and the airborne-exclusion property (ADR-004); C10 carries the model identity and the takeoff-load verifier — neither of which need to leave the workstation/companion enclave at runtime.
|
||||
|
||||
|
||||
@@ -24,7 +24,12 @@ The shared `ImuPreintegrator` (AZ-276), `SE3Utils` (AZ-277), and `WgsConverter`
|
||||
@runtime_checkable
|
||||
class StateEstimator(Protocol):
|
||||
# AZ-490 / ADR-010: operator-provided warm-start. MUST be called before any add_*.
|
||||
def set_takeoff_origin(self, origin: LatLonAlt, sigma_m: float) -> None: ...
|
||||
def set_takeoff_origin(
|
||||
self,
|
||||
origin: LatLonAlt,
|
||||
sigma_horiz_m: float,
|
||||
sigma_vert_m: float,
|
||||
) -> None: ...
|
||||
|
||||
def add_vio(self, vio: VioOutput) -> None: ...
|
||||
def add_pose_anchor(self, pose: PoseEstimate) -> None: ...
|
||||
@@ -46,7 +51,7 @@ class StateEstimator(Protocol):
|
||||
8. **Spoof-rejection events ALWAYS land in FDR + GCS STATUSTEXT** — never silent (R07; C5-ST-01).
|
||||
9. **AC-5.2 fallback on 3 s no-estimate** — if `current_estimate()` would raise OR the keyframe window is empty for ≥3 s, downstream C8 emits FC IMU-only.
|
||||
10. **`covariance_6x6` is always SPD** — both strategies enforce; on numerical failure raise `EstimatorFatalError`.
|
||||
11. **`set_takeoff_origin(origin, sigma_m)` is a `INIT`-state-only entrypoint** (AZ-490, ADR-010). Calling it after the estimator has transitioned to `TRACKING` raises `StateEstimatorConfigError`. Inside `INIT` it is idempotent — re-invocation overwrites the prior with the new origin + sigma. `sigma_m` MUST be positive and finite; otherwise raise `StateEstimatorConfigError`. The origin is consumed as a Bayesian prior on the initial pose key (iSAM2: `PriorFactorPose3` with covariance = `diag(sigma_m^2, sigma_m^2, (2*sigma_m)^2, ...)` in ENU position + orientation order; ESKF: nominal-state seed + position-block covariance = `sigma_m^2 * I_3`).
|
||||
11. **`set_takeoff_origin(origin, sigma_horiz_m, sigma_vert_m)` is a cold-start-window-only entrypoint** (AZ-490, ADR-010). The cold-start window closes on the first `add_*` call; calling `set_takeoff_origin` after that raises `EstimatorAlreadyStartedError` (subclass of `StateEstimatorConfigError`). Inside the cold-start window the call is **strictly idempotent on identical args** (re-invocation with byte-equal `origin`/`sigma_horiz_m`/`sigma_vert_m` is a no-op); a re-invocation with **different** args raises `StateEstimatorConfigError`. Both sigmas MUST be positive and finite; the origin MUST be inside WGS-84 bounds (lat ∈ [-90, 90], lon ∈ [-180, 180], all components finite); otherwise raise `StateEstimatorConfigError`. The origin is consumed as a Bayesian prior on the first pose key (iSAM2: `PriorFactorPose3` at `Pose3.Identity()` with diagonal sigmas `[5°, 5°, 5°, sigma_horiz_m, sigma_horiz_m, sigma_vert_m]` in rotation+translation order; the operator origin BECOMES the local-ENU (0,0,0) anchor. ESKF: nominal-state position seeded to (0,0,0) + position block of error covariance set to `diag(sigma_horiz_m², sigma_horiz_m², sigma_vert_m²)`). One `c5.cold_start_origin.set` FDR record is emitted per estimator (with `source="manifest"` from this entrypoint, or `source="fc_ekf"` from the legacy first-frame path when no operator origin was supplied).
|
||||
12. **Spoof-promotion gate has THREE clauses, not two** — Principle #11 amended. Re-promote a previously-spoofed FC GPS source only when ALL of: (i) FC `gps_health == STABLE_NON_SPOOFED` for ≥ `spoof_promotion_min_stable_s`, (ii) the next satellite-anchored frame agrees with the FC GPS within `spoof_promotion_visual_consistency_tol_m`, AND (iii) the FC's reported position is within `spoof_promotion_bounded_delta_m` (default 200 m) of the companion's last emitted `PoseEstimate`. The bounded-delta clause also gates the takeoff path when a Manifest `takeoff_origin` is present.
|
||||
|
||||
### DTOs (in `_types/state.py`)
|
||||
@@ -135,10 +140,11 @@ Config schema additions:
|
||||
| 8 | Spoof-rejection logging | FDR + GCS STATUSTEXT both fire on every gate decision |
|
||||
| 9 | AC-5.2 timeout | 3 s no estimate → fallback signal emitted |
|
||||
| 10 | SPD covariance | every emitted `covariance_6x6` is SPD |
|
||||
| 11a | `set_takeoff_origin` after `TRACKING` | raises `StateEstimatorConfigError` |
|
||||
| 11b | `set_takeoff_origin` with `sigma_m <= 0` or non-finite | raises `StateEstimatorConfigError` |
|
||||
| 11c | `set_takeoff_origin` twice in `INIT` | second call wins; covariance updated to new sigma |
|
||||
| 11d | First `current_estimate` after `set_takeoff_origin` + no sensor samples | returns `EstimatorOutput` with `position_wgs84 == origin`, `covariance_6x6` reflecting `sigma_m^2` in the position block |
|
||||
| 11a | `set_takeoff_origin` after first `add_*` call | raises `EstimatorAlreadyStartedError` (subclass of `StateEstimatorConfigError`) |
|
||||
| 11b | `set_takeoff_origin` with non-positive / non-finite sigma OR out-of-bounds `LatLonAlt` | raises `StateEstimatorConfigError` |
|
||||
| 11c | `set_takeoff_origin` twice in cold-start window with **identical** args | second call is a no-op; no extra FDR record |
|
||||
| 11d | `set_takeoff_origin` twice in cold-start window with **different** args | raises `StateEstimatorConfigError` (names both prev/new args) |
|
||||
| 11e | First `current_estimate` after `set_takeoff_origin` + no sensor samples | returns `EstimatorOutput` with `position_wgs84 == origin`, position block of `covariance_6x6` reflecting `diag(sigma_horiz_m², sigma_horiz_m², sigma_vert_m²)` |
|
||||
| 12 | Bounded-delta gate | FC GPS frame with |Δ| > 200 m vs last emitted `PoseEstimate` is rejected even when stable + non-spoofed for ≥ 10 s + visual-consistent |
|
||||
|
||||
## Producer-task / consumer-task split
|
||||
|
||||
@@ -82,7 +82,7 @@ Terms are alphabetical. Each entry: one-line definition + parenthetical source.
|
||||
|
||||
**Suite Sat Service** — Synonym for `satellite-provider` used in earlier docs (problem.md, restrictions.md, solution_draft01/02). The actual implementation in the parent suite is the .NET 8 service; "Suite Sat Service" is the role name. (source: `restrictions.md`, parent-suite `satellite-provider/README.md`)
|
||||
|
||||
**Takeoff origin** — `LatLonAlt` baked into the C10 Manifest by C12 at build time from `Flight.waypoints[0]`. Consumed at boot by C5 via `set_takeoff_origin(origin, sigma_m)` (AZ-490) as a Bayesian prior on the initial pose — iSAM2 attaches a `PriorFactorPose3`; ESKF seeds the nominal position + position-block covariance. Primary cold-start trust anchor per ADR-010; FC EKF GPS is secondary. (source: ADR-010, AZ-490)
|
||||
**Takeoff origin** — `LatLonAlt` baked into the C10 Manifest by C12 at build time from `Flight.waypoints[0]`. Consumed at boot by C5 via `set_takeoff_origin(origin, sigma_horiz_m, sigma_vert_m)` (AZ-490) as a Bayesian prior on the initial pose — iSAM2 attaches a `PriorFactorPose3` at `Pose3.Identity()` (origin BECOMES the local-ENU (0,0,0) anchor); ESKF seeds the nominal position to (0,0,0) + position-block covariance to `diag(sigma_horiz_m², sigma_horiz_m², sigma_vert_m²)`. Primary cold-start trust anchor per ADR-010; FC EKF GPS is secondary. (source: ADR-010, AZ-490)
|
||||
|
||||
**Tier-1 / Tier-2** — Testing-environment split: Tier-1 = workstation Docker (fast/cheap); Tier-2 = Jetson hardware (AC-bound). Both appear in the deployment plan and CI matrix per finding F6. (source: `_docs/02_document/tests/environment.md`)
|
||||
|
||||
|
||||
@@ -235,7 +235,7 @@ sequenceDiagram
|
||||
FC-->>Companion: first telemetry frame
|
||||
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
|
||||
Companion->>Pipeline: C5.set_takeoff_origin(manifest.takeoff_origin, sigma_horiz_m, sigma_vert_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
|
||||
@@ -267,9 +267,9 @@ flowchart TD
|
||||
SignOk -->|no| RefuseTakeoff
|
||||
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 -->|yes ADR-010 AZ-490 primary| OperatorOrigin[C5.set_takeoff_origin manifest.takeoff_origin sigma_horiz_m sigma_vert_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 -->|yes AZ-419 secondary| FcOrigin[C5.set_takeoff_origin fc_gps_origin fc_gps_sigma_horiz fc_gps_sigma_vert]
|
||||
FcEkfGate -->|no| NoOrigin[Stay INITIALIZING and apply FT-P-11 takeoff-abort policy]
|
||||
OperatorOrigin --> WarmPipeline
|
||||
FcOrigin --> WarmPipeline
|
||||
@@ -290,7 +290,7 @@ flowchart TD
|
||||
| 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 | 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 |
|
||||
| 7 | Companion | C5 `StateEstimator` (AZ-490) | `set_takeoff_origin(origin, sigma_horiz_m, sigma_vert_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
|
||||
|
||||
Reference in New Issue
Block a user