mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 10:01:14 +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:
@@ -66,6 +66,8 @@ class BuildRequest:
|
||||
calibration_path: Path
|
||||
cache_root: Path
|
||||
key_path: Path # operator signing key per C10-ST-01
|
||||
takeoff_origin: LatLonAlt | None = None # ADR-010 + AZ-489: planned takeoff position from Flight.waypoints[0]; baked into Manifest body + build-identity hash
|
||||
flight_id: UUID | None = None # ADR-010: pass-through provenance of which Flight produced the build
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -105,6 +107,8 @@ class BuildReport:
|
||||
| CP-INV-5 | `cache_root` must already exist; `build_cache_artifacts` does NOT create the directory tree (operator workflow places it). | Avoids accidental builds in unintended paths. |
|
||||
| CP-INV-6 | No network calls (no `satellite-provider`, no Postgres TLS to a remote DB beyond the local instance, no metric push). | Epic § Architecture notes: C10 is workstation-local. |
|
||||
| CP-INV-7 | The operator key file at `request.key_path` is opened exactly once (via AZ-323's signer) and zeroized when out of scope; this contract does NOT cache the key in memory across calls. | Operator key hygiene. |
|
||||
| CP-INV-8 | `takeoff_origin` is treated as one more identity field by the build-identity hash. If the prior Manifest carries `takeoff_origin=A` and a new request carries `takeoff_origin=B != A` (with all other fields equal), the build is NOT idempotent and proceeds; the verifier (AZ-324) at boot then refuses any cache whose manifest origin disagrees with the manifest-on-disk's origin. | ADR-010: cache identity must include the origin or boot-time consistency breaks. |
|
||||
| CP-INV-9 | When `takeoff_origin` is None, the prior cold-start ladder (FC-EKF-GPS via AZ-419) remains the only origin source. C10 does not invent a default origin from the bbox; that decision is for C12. | Single-responsibility — C10 records, C12 decides. |
|
||||
|
||||
## Non-Goals
|
||||
|
||||
@@ -125,6 +129,7 @@ class BuildReport:
|
||||
| Version | Date | Notes | Author |
|
||||
|---------|------|-------|--------|
|
||||
| 1.0.0 | 2026-05-10 | Initial contract — produced by AZ-325 (E-C10 decomposition) | autodev |
|
||||
| 1.1.0 | 2026-05-11 | Additive: `BuildRequest.takeoff_origin` + `BuildRequest.flight_id` (defaults `None` for back-compat); CP-INV-8 + CP-INV-9. Consumer requires the Manifest hash to include `takeoff_origin` when set. ADR-010 + AZ-489. | autodev |
|
||||
|
||||
## Test Cases (consumer side)
|
||||
|
||||
@@ -143,3 +148,6 @@ class BuildReport:
|
||||
| CP-TC-11 | `compile_engines_for_corpus` directly callable for re-compile-only flows | Returns `tuple[EngineCacheEntry, ...]`; no descriptor / Manifest work |
|
||||
| CP-TC-12 | Cold build wall-clock benchmark on Tier-1 dev workstation, 1k tiles, 3 backbones | ≤ 12 min (NFR C10-PT-01) |
|
||||
| CP-TC-13 | Warm idempotent re-run benchmark | ≤ 1 min (NFR C10-PT-01) |
|
||||
| CP-TC-14 | Build with `takeoff_origin=A` → second build with same request + `takeoff_origin=A` | `outcome=IDEMPOTENT_NO_OP` |
|
||||
| CP-TC-15 | Build with `takeoff_origin=A` → second build with same request + `takeoff_origin=B (B != A)` | `outcome=SUCCESS` (re-build); new Manifest hash differs from prior |
|
||||
| CP-TC-16 | `BuildRequest.takeoff_origin=None` with no prior Manifest | `outcome=SUCCESS`; Manifest written without `takeoff_origin` field |
|
||||
|
||||
@@ -62,6 +62,8 @@ class VerifyFailReason(Enum):
|
||||
ARTIFACT_HASH_MISMATCH = "artifact_hash_mismatch"
|
||||
TILES_COVERAGE_MISMATCH = "tiles_coverage_mismatch"
|
||||
MANIFEST_SELF_HASH_MISMATCH = "manifest_self_hash_mismatch"
|
||||
TAKEOFF_ORIGIN_INVALID = "takeoff_origin_invalid" # ADR-010: schema check on the LatLonAlt block
|
||||
TAKEOFF_ORIGIN_OUT_OF_BBOX = "takeoff_origin_out_of_bbox" # ADR-010: origin must lie inside the cache bbox
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -79,6 +81,8 @@ class VerificationResult:
|
||||
fail_details: tuple[str, ...] # human-readable diagnostic per reason
|
||||
signing_public_key_fingerprint: str | None # populated when signature parses, even if untrusted
|
||||
per_artifact_checks: tuple[ArtifactCheck, ...]
|
||||
takeoff_origin: LatLonAlt | None # ADR-010 + AZ-490: passed through from Manifest body; None when Manifest carries no origin
|
||||
flight_id: UUID | None # ADR-010: provenance of which Flight produced the build
|
||||
elapsed_ms: int
|
||||
```
|
||||
|
||||
@@ -93,6 +97,8 @@ class VerificationResult:
|
||||
| MV-INV-5 | `tiles_coverage` mismatch is reported separately from `ARTIFACT_HASH_MISMATCH` because tiles are hashed in aggregate (per AZ-323). The verifier re-derives the aggregate hash from a `TileMetadataStore` query if available, OR (in airborne F2 mode) treats the recorded `tiles_coverage_sha256` as authoritative and only verifies the Manifest signature + non-tile artifacts. | Airborne C5 may not load 100k per-tile rows just to arm; the trust chain is signature → manifest_hash → tiles_coverage_sha256. C12 / operator mode does the full re-derivation. |
|
||||
| MV-INV-6 | The verifier never writes to disk, never opens network sockets, never calls C13. Telemetry is the caller's responsibility. | Read-only contract — composable in airborne C5 + operator C12 contexts without side-effect surprise. |
|
||||
| MV-INV-7 | `elapsed_ms` is recorded for every call (pass or fail) so operators and C5 can observe drift in verify cost on slow disks. | NFR for C10-PT-01's takeoff load budget. |
|
||||
| MV-INV-8 | When the Manifest body carries a `takeoff_origin`, the verifier checks: (a) the LatLonAlt block is well-formed (`-90 ≤ lat ≤ 90`, `-180 ≤ lon ≤ 180`, `alt` finite) → `TAKEOFF_ORIGIN_INVALID` otherwise, (b) the lat/lon falls inside the Manifest's `bbox` → `TAKEOFF_ORIGIN_OUT_OF_BBOX` otherwise. When the Manifest body carries no `takeoff_origin`, the field is absent from `VerificationResult` (None) and no origin check runs. | ADR-010: garbage / out-of-bbox origin must not silently propagate to `C5.set_takeoff_origin`. |
|
||||
| MV-INV-9 | `takeoff_origin` is surfaced on `VerificationResult` even on `FAIL` outcomes when the Manifest body parsed (so caller can inspect what was attempted), but the takeoff-arming gate only consumes it on `PASS`. | Diagnostics — operators can see "your origin was X and that's why we rejected it". |
|
||||
|
||||
## Non-Goals
|
||||
|
||||
@@ -112,6 +118,7 @@ class VerificationResult:
|
||||
| Version | Date | Notes | Author |
|
||||
|---------|------|-------|--------|
|
||||
| 1.0.0 | 2026-05-10 | Initial contract — produced by AZ-324 (E-C10 decomposition) | autodev |
|
||||
| 1.1.0 | 2026-05-11 | Additive: `VerificationResult.takeoff_origin` + `flight_id`; new `VerifyFailReason.TAKEOFF_ORIGIN_INVALID` + `TAKEOFF_ORIGIN_OUT_OF_BBOX`; MV-INV-8 + MV-INV-9. ADR-010 + AZ-490. | autodev |
|
||||
|
||||
## Test Cases (consumer side)
|
||||
|
||||
@@ -132,3 +139,7 @@ class VerificationResult:
|
||||
| MV-TC-13 | Tier-2 Tile-coverage check (operator mode with TileMetadataStore) | If recomputed `tiles_coverage_sha256` differs → `TILES_COVERAGE_MISMATCH`; if matches → that part passes |
|
||||
| MV-TC-14 | Empty `trusted_public_keys` | `outcome=FAIL`, `fail_reasons=(UNTRUSTED_PUBLIC_KEY,)` (every key is untrusted by definition) |
|
||||
| MV-TC-15 | Pristine Manifest verified inside 100 ms on Tier-2 (excludes per-tile re-walk) | `elapsed_ms ≤ 100` for the signature + non-tile artifact path |
|
||||
| MV-TC-16 | Manifest body carries no `takeoff_origin` | `outcome=PASS`; `VerificationResult.takeoff_origin is None` |
|
||||
| MV-TC-17 | Manifest body carries a well-formed `takeoff_origin` inside bbox | `outcome=PASS`; `VerificationResult.takeoff_origin` populated as `LatLonAlt` |
|
||||
| MV-TC-18 | Manifest body carries a malformed `takeoff_origin` (lat = 200) | `outcome=FAIL`, `fail_reasons` contains `TAKEOFF_ORIGIN_INVALID`; `takeoff_origin` field is populated for diagnostics |
|
||||
| MV-TC-19 | Manifest body carries `takeoff_origin` outside the recorded `bbox` | `outcome=FAIL`, `fail_reasons` contains `TAKEOFF_ORIGIN_OUT_OF_BBOX` |
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
# Contract: flights_api_client
|
||||
|
||||
**Component**: c12_operator_tooling
|
||||
**Producer task**: AZ-489 — `_docs/02_tasks/todo/AZ-489_c12_flights_api_client.md`
|
||||
**Consumer tasks**: AZ-326 (CLI app — wires `--flight-id` / `--flight-file` flags), AZ-328 (build-cache orchestrator — calls `fetch_flight` / `load_flight_file`, then `bbox_from_waypoints` + `takeoff_origin_from_flight`)
|
||||
**Version**: 1.0.0
|
||||
**Status**: draft
|
||||
**Last Updated**: 2026-05-11
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the operator-workstation ↔ parent-suite `flights` REST service boundary plus an offline fallback for the same DTO shape. C12 consumes a typed `FlightDto` (waypoints + altitudes) to derive the cache bbox and the takeoff origin per ADR-010. The companion never sees this contract; it lives entirely on the operator workstation.
|
||||
|
||||
The boundary is split into two sources that produce the **same DTO shape**:
|
||||
|
||||
- **Online**: `GET /flights/{id}` + `GET /flights/{id}/waypoints` against the parent-suite `flights` REST service.
|
||||
- **Offline**: a local JSON export in the same DTO shape (for operators without a workstation-to-flights path, e.g., field-deployed laptops).
|
||||
|
||||
## Shape
|
||||
|
||||
### DTOs
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Protocol, runtime_checkable
|
||||
from uuid import UUID
|
||||
from pathlib import Path
|
||||
|
||||
from gps_denied_onboard._types.geo import LatLonAlt, BoundingBox
|
||||
|
||||
|
||||
class WaypointObjective(Enum):
|
||||
TAKEOFF = "takeoff"
|
||||
WAYPOINT = "waypoint"
|
||||
LOITER = "loiter"
|
||||
LANDING = "landing"
|
||||
OTHER = "other"
|
||||
|
||||
|
||||
class WaypointSource(Enum):
|
||||
OPERATOR = "operator"
|
||||
IMPORT = "import"
|
||||
OTHER = "other"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class WaypointDto:
|
||||
ordinal: int # >= 0; defines the order of the waypoint inside the Flight
|
||||
lat_deg: float # -90 <= lat <= 90
|
||||
lon_deg: float # -180 <= lon <= 180
|
||||
alt_m: float # WGS84 ellipsoidal height; finite
|
||||
objective: WaypointObjective
|
||||
source: WaypointSource
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class FlightDto:
|
||||
flight_id: UUID
|
||||
name: str
|
||||
waypoints: tuple[WaypointDto, ...] # ORDERED by ordinal ascending; non-empty
|
||||
```
|
||||
|
||||
### Protocol
|
||||
|
||||
```python
|
||||
@runtime_checkable
|
||||
class FlightsApiClient(Protocol):
|
||||
"""Read a Flight from the parent-suite flights REST service or a local JSON export.
|
||||
|
||||
Pure read; no side effects beyond logging. Caller (C12 CacheBuildWorkflow)
|
||||
decides which source to use based on CLI flags (--flight-id vs --flight-file).
|
||||
"""
|
||||
|
||||
def fetch_flight(
|
||||
self,
|
||||
*,
|
||||
flight_id: UUID,
|
||||
base_url: str,
|
||||
auth_token: str,
|
||||
timeout_s: float = 10.0,
|
||||
) -> FlightDto: ...
|
||||
|
||||
def load_flight_file(self, *, path: Path) -> FlightDto: ...
|
||||
|
||||
def bbox_from_waypoints(
|
||||
self,
|
||||
waypoints: tuple[WaypointDto, ...],
|
||||
*,
|
||||
buffer_m: float = 1000.0,
|
||||
) -> BoundingBox: ...
|
||||
|
||||
def takeoff_origin_from_flight(self, flight: FlightDto) -> LatLonAlt: ...
|
||||
```
|
||||
|
||||
### Exceptions
|
||||
|
||||
| Exception | When raised | Caller action |
|
||||
|-----------|-------------|---------------|
|
||||
| `FlightsApiUnreachableError` | HTTPS timeout / connection refused / 5xx | Operator retries online when network recovers OR switches to `--flight-file` offline path |
|
||||
| `FlightsApiAuthError` | HTTP 401 / 403 | Operator refreshes suite credentials; never silently fall back to offline |
|
||||
| `FlightNotFoundError` | HTTP 404 for the given `flight_id` | Operator verifies the GUID in the Mission Planner UI |
|
||||
| `FlightsApiSchemaError` | Response body fails `FlightDto` validation (online) OR JSON file fails the same validation (offline) | Operator re-exports from the Mission Planner UI; bug in the schema map is a release-blocking defect |
|
||||
| `FlightFileNotFoundError` | `path` does not exist (offline) | Operator confirms the path |
|
||||
| `EmptyWaypointsError` | Resolved `FlightDto` carries zero waypoints — `bbox_from_waypoints` / `takeoff_origin_from_flight` cannot proceed | Operator re-plans in Mission Planner UI |
|
||||
| `WaypointSchemaError` | Individual `WaypointDto` is malformed (lat out of range, NaN alt, negative ordinal, gap in ordering) | Operator re-exports / re-plans |
|
||||
|
||||
The Protocol does NOT catch these inside the methods — it raises and lets `CacheBuildWorkflow` translate to `CacheBuildError` with the appropriate `failure_phase=flight_resolve`.
|
||||
|
||||
## Invariants
|
||||
|
||||
| ID | Invariant | Why |
|
||||
|----|-----------|-----|
|
||||
| FAC-INV-1 | The online (`fetch_flight`) and offline (`load_flight_file`) paths return DTOs with **the same shape and the same validation contract**. The only difference is where the bytes come from. | Caller can swap paths without conditional handling. |
|
||||
| FAC-INV-2 | `FlightDto.waypoints` is non-empty and is ordered by ascending `ordinal`. Implementations MUST sort + validate the ordering. | `waypoints[0]` is the takeoff origin per ADR-010; ordering is operationally meaningful. |
|
||||
| FAC-INV-3 | `bbox_from_waypoints` envelopes the lat/lon of every waypoint and inflates by the `buffer_m` parameter (default 1 km). The buffer is a horizontal-distance expansion, not a degree-space expansion — implementations use `WgsConverter` to inflate correctly at the Flight's latitude. | A degree-space buffer would be 1.5× too narrow at high latitudes and miss tiles near the poles. |
|
||||
| FAC-INV-4 | `takeoff_origin_from_flight` returns `LatLonAlt(waypoints[0].lat_deg, waypoints[0].lon_deg, waypoints[0].alt_m)` — no rounding, no projection. | The operator authored this point; we pass it through. |
|
||||
| FAC-INV-5 | `fetch_flight` issues at most ONE retry on transient 5xx (with 1 s backoff) and at most ONE retry on connection error. 401/403/404/schema failures are NOT retried. | Operator-visible failures should be loud; transient blips should not require operator intervention. |
|
||||
| FAC-INV-6 | `fetch_flight` is the ONLY method that makes network calls. `load_flight_file`, `bbox_from_waypoints`, `takeoff_origin_from_flight` are pure / filesystem-only. | Composability — the offline path is fully usable on an air-gapped operator workstation. |
|
||||
| FAC-INV-7 | `auth_token` is never logged; structured logs redact the field. | Operator credential hygiene. |
|
||||
| FAC-INV-8 | No write methods. This client is strictly read-only against the flights service. | Single responsibility — C12 does not author flights. |
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Flight authoring / editing — the Mission Planner UI (`suite/ui`) owns that.
|
||||
- Live updates / websockets — pre-flight only.
|
||||
- Caching the `FlightDto` across runs — every `build-cache` invocation re-fetches OR re-reads the file.
|
||||
- Posting build status back to the `flights` REST service — out of scope this cycle.
|
||||
|
||||
## Versioning
|
||||
|
||||
- v1.0.0 — initial Protocol surface (this document).
|
||||
- Breaking changes (changing DTO shape, removing a method) — bump major.
|
||||
- Additive changes (new optional kwarg, new enum value) — bump minor. Consumers MUST handle unknown enum values gracefully.
|
||||
- Patch — clarifications, doc edits.
|
||||
|
||||
| Version | Date | Notes | Author |
|
||||
|---------|------|-------|--------|
|
||||
| 1.0.0 | 2026-05-11 | Initial contract — produced by AZ-489 (ADR-010 cold-start origin via operator-planned mission) | autodev |
|
||||
|
||||
## Test Cases (consumer side)
|
||||
|
||||
| ID | Scenario | Expected Outcome |
|
||||
|----|----------|------------------|
|
||||
| FAC-TC-1 | Online happy path: valid `flight_id`, reachable service, 3 waypoints | Returns `FlightDto` with 3 ordered waypoints |
|
||||
| FAC-TC-2 | Online 404 (unknown flight_id) | Raises `FlightNotFoundError`; does NOT retry |
|
||||
| FAC-TC-3 | Online 401 (bad auth_token) | Raises `FlightsApiAuthError`; does NOT retry; does NOT log token |
|
||||
| FAC-TC-4 | Online 503 transient | Retries once with 1 s backoff; succeeds; returns DTO |
|
||||
| FAC-TC-5 | Online 503 persistent | Raises `FlightsApiUnreachableError` after one retry |
|
||||
| FAC-TC-6 | Online connection refused | Retries once; raises `FlightsApiUnreachableError` after one retry |
|
||||
| FAC-TC-7 | Online schema drift (response missing `lat`) | Raises `FlightsApiSchemaError` with field reference |
|
||||
| FAC-TC-8 | Offline happy path: well-formed JSON | Returns equivalent `FlightDto` |
|
||||
| FAC-TC-9 | Offline file missing | Raises `FlightFileNotFoundError` |
|
||||
| FAC-TC-10 | Offline JSON missing waypoints array | Raises `FlightsApiSchemaError` |
|
||||
| FAC-TC-11 | Empty waypoints | `fetch_flight` / `load_flight_file` accept but downstream `bbox_from_waypoints` raises `EmptyWaypointsError` |
|
||||
| FAC-TC-12 | Waypoint with `lat=200` | Raises `WaypointSchemaError` during DTO validation |
|
||||
| FAC-TC-13 | Waypoints out of ordinal order | Implementation sorts + validates; returns `FlightDto` with sorted tuple |
|
||||
| FAC-TC-14 | Waypoints with ordinal gap (0, 1, 3) | Raises `WaypointSchemaError` (gap implies missing waypoint, not robust to silent reorder) |
|
||||
| FAC-TC-15 | `bbox_from_waypoints(buffer_m=1000)` at mid-latitudes (~50°N) | Returned bbox extends ~1 km horizontally on all sides, NOT 1° in degree space |
|
||||
| FAC-TC-16 | `takeoff_origin_from_flight(flight)` with `waypoints[0] = (50.0, 36.2, 200.0)` | Returns `LatLonAlt(50.0, 36.2, 200.0)` exactly |
|
||||
| FAC-TC-17 | Conformance: `isinstance(impl, FlightsApiClient)` | `True` |
|
||||
| FAC-TC-18 | Online + Offline produce byte-identical `FlightDto` for the same source | `assert online_dto == offline_dto` |
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
**Owner**: c5_state (epic AZ-260 / E-C5)
|
||||
**Producer task**: AZ-381 (Protocol + DTOs + factory + composition + concrete `ISam2GraphHandle`)
|
||||
**Consumer tasks**: AZ-382 (iSAM2 + IncrementalFixedLagSmoother wiring), AZ-383 (Factor adds), AZ-384 (Marginals + outputs), AZ-385 (Source-label + spoof gate), AZ-386 (ESKF baseline), AZ-387 (Smoothed history → FDR), AZ-388 (AC-5.2 fallback), AZ-389 (Orthorectifier → C6).
|
||||
**Version**: 1.0.0
|
||||
**Consumer tasks**: AZ-382 (iSAM2 + IncrementalFixedLagSmoother wiring), AZ-383 (Factor adds), AZ-384 (Marginals + outputs), AZ-385 (Source-label + spoof gate), AZ-386 (ESKF baseline), AZ-387 (Smoothed history → FDR), AZ-388 (AC-5.2 fallback), AZ-389 (Orthorectifier → C6), AZ-490 (set_takeoff_origin — operator-provided warm-start).
|
||||
**Version**: 1.1.0
|
||||
**Status**: active
|
||||
**Last Updated**: 2026-05-11
|
||||
**Module-layout home**: `src/gps_denied_onboard/components/c5_state/interface.py`, `src/gps_denied_onboard/components/c5_state/__init__.py`, `src/gps_denied_onboard/runtime_root/state_factory.py`
|
||||
@@ -23,6 +23,9 @@ The shared `ImuPreintegrator` (AZ-276), `SE3Utils` (AZ-277), and `WgsConverter`
|
||||
```python
|
||||
@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 add_vio(self, vio: VioOutput) -> None: ...
|
||||
def add_pose_anchor(self, pose: PoseEstimate) -> None: ...
|
||||
def add_fc_imu(self, imu_window: ImuWindow) -> None: ...
|
||||
@@ -43,6 +46,8 @@ 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`).
|
||||
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`)
|
||||
|
||||
@@ -111,7 +116,10 @@ Config schema additions:
|
||||
- `config.state.keyframe_window_size` (int, default 15) — D-C5-3 K=10–20
|
||||
- `config.state.spoof_promotion_min_stable_s` (float, default 10.0) — AC-NEW-2
|
||||
- `config.state.spoof_promotion_visual_consistency_tol_m` (float, default 30.0) — AC-NEW-8
|
||||
- `config.state.spoof_promotion_bounded_delta_m` (float, default 200.0) — Principle #11 amended, ADR-010
|
||||
- `config.state.no_estimate_fallback_s` (float, default 3.0) — AC-5.2
|
||||
- `config.state.default_takeoff_origin_sigma_horiz_m` (float, default 50.0) — AZ-490 default horizontal sigma when caller omits
|
||||
- `config.state.default_takeoff_origin_sigma_vert_m` (float, default 100.0) — AZ-490 default vertical sigma when caller omits
|
||||
|
||||
## Test expectations summarised by Invariant
|
||||
|
||||
@@ -127,6 +135,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 |
|
||||
| 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user