[AZ-390] [AZ-392] C8 FC/GCS adapter foundation + covariance projector

Adds the C8 foundation:
- FcAdapter / GcsAdapter / ReplaySink Protocols + contract DTOs in
  _types/fc.py (PortConfig, FcKind, FlightState, GpsStatus, Severity,
  TelemetryKind, FcTelemetryFrame, FlightStateSignal, GpsHealth,
  OperatorCommand, Subscription, Imu/Attitude samples).
- Disjoint FcAdapterError / GcsAdapterError trees with
  SourceSetSwitchNotSupportedError <: SourceSetSwitchError per AC-9.
- FcConfig + GcsConfig cross-cutting Config blocks with config-load
  validation (unknown strategy rejected at __post_init__).
- runtime_root/fc_factory.py: build_fc_adapter / build_gcs_adapter
  with BUILD_FC_*/BUILD_GCS_* flag gating + INFO log on load +
  single-writer outbound-thread binding.
- CovarianceProjector (helper, AZ-392): 6x6 -> 3x3 -> 2x2 ->
  sqrt(lambda_max) reduction; AP returns float m, iNav returns int mm
  with uint16 clamp + WARN + FDR record. Non-SPD / NaN / wrong-shape
  raise FcEmitError and emit an FDR ERROR record carrying frame_id.

Contracts:
- composition_root_protocol.md 1.1.0 -> 1.2.0 (added fc/gcs blocks +
  build_fc_adapter / build_gcs_adapter + outbound-thread binding).
- fc_adapter_protocol.md unchanged (this batch implements v1.0.0).

Tests: 410 pass / 2 skip / 0 fail (+53 new tests in batch 8).

AZ-391 (inbound subscription) deferred to batch 9 — pulls YAMSPy as
a new external dependency (iNav MSP2 decode).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 04:17:59 +03:00
parent e4ecdaf619
commit 362e93c626
22 changed files with 1909 additions and 59 deletions
@@ -0,0 +1,103 @@
# C8 FcAdapter / GcsAdapter Protocols + DTOs + Factories + Composition
**Task**: AZ-390_c8_adapter_protocol
**Name**: C8 `FcAdapter` + `GcsAdapter` Protocols + DTOs + errors + composition factories
**Description**: Define the public `FcAdapter` and `GcsAdapter` Protocols (PEP 544 `@runtime_checkable`), the C8 DTOs (`PortConfig`, `FcKind` enum, `FcTelemetryFrame`, `TelemetryKind` enum + payload union, `FlightStateSignal`, `FlightState` enum, `GpsHealth`, `GpsStatus` enum, `Severity` enum, `EmittedExternalPosition`, `OperatorCommand`), the error hierarchy (`FcAdapterError` family + `GcsAdapterError` family per the contract), and the composition-root factories `build_fc_adapter(...) -> FcAdapter` + `build_gcs_adapter(...) -> GcsAdapter` with strategy resolution (`config.fc.adapter`, `config.gcs.adapter`) and `BUILD_FC_<variant>` / `BUILD_GCS_<variant>` flag gating per ADR-002. Composition root binds C8 outbound (`emit_external_position`, `emit_status_text`, `request_source_set_switch`) to a single emit thread; C8 inbound (`subscribe_telemetry`) fires on the inbound decode thread. Shared helpers (`WgsConverter` AZ-279, `SE3Utils` AZ-277, `FdrClient` AZ-273, `Clock`) constructor-injected. Config schema extension for `fc.{adapter, port_device, port_baud, signing_key_source}` and `gcs.{adapter, port_device, port_baud, summary_rate_hz}`. No wire encoding, no signing logic, no telemetry decoding in scope here — pure scaffolding the seven downstream consumer tasks depend on.
**Complexity**: 3 points
**Dependencies**: AZ-263, AZ-269, AZ-270, AZ-273 (`FdrClient`), AZ-277 (`SE3Utils`), AZ-279 (`WgsConverter`), AZ-266
**Component**: c8_fc_adapter (epic AZ-261 / E-C8)
**Tracker**: AZ-390
**Epic**: AZ-261 (E-C8)
### Document Dependencies
- `_docs/02_document/contracts/c8_fc_adapter/fc_adapter_protocol.md` — the public contract this task implements.
- `_docs/02_document/components/10_c8_fc_adapter/description.md` — § 1 overview, § 2 interfaces, § 5 implementation details.
- `_docs/02_document/architecture.md` — ADR-001, ADR-002, ADR-009.
- `_docs/02_document/module-layout.md``c8_fc_adapter` Per-Component Mapping.
## Problem
Without this task, no concrete C8 adapter has a Protocol to register against; the runtime root cannot wire C8 to C1 / C5 (which receive `ImuWindow` / `AttitudeWindow` / `GpsHealth` / `FlightStateSignal` exclusively via the constructor-injected `FcAdapter` interface); the seven downstream consumer tasks (inbound subscription, covariance projector, AP outbound, iNav outbound, signing handshake, source-set switch, GCS adapter) have no shared DTO surface to encode/decode against.
## Outcome
- `src/gps_denied_onboard/components/c8_fc_adapter/interface.py``FcAdapter`, `GcsAdapter` Protocols with all methods per the contract.
- `src/gps_denied_onboard/components/c8_fc_adapter/__init__.py` — re-exports `FcAdapter`, `GcsAdapter`, `EmittedExternalPosition`.
- `src/gps_denied_onboard/_types/fc.py``PortConfig`, `FcKind`, `FcTelemetryFrame`, `TelemetryKind`, `FlightStateSignal`, `FlightState`, `GpsHealth`, `GpsStatus`, `Severity`, `EmittedExternalPosition`, `OperatorCommand` (all frozen + slots).
- `src/gps_denied_onboard/components/c8_fc_adapter/errors.py` — full error hierarchy.
- `src/gps_denied_onboard/runtime_root/fc_factory.py``build_fc_adapter(...)` + `build_gcs_adapter(...)`. Lazy-import per ADR-002.
- Composition-root extension: invoke `build_fc_adapter` AFTER C5; invoke `build_gcs_adapter` AFTER `build_fc_adapter`; bind outbound to ONE emit thread (single-writer invariant).
- Config schema extension for `fc.*` + `gcs.*` fields.
- INFO log on successful build: `kind="c8.adapter.strategy_loaded"` with `{fc_kind, gcs_kind}`.
## Scope
### Included
- Both Protocols with all methods.
- All DTOs + enums.
- Error hierarchy.
- Both factories + composition-root wiring.
- Single-writer thread enforcement for outbound.
- Config schema extension.
- Unit tests: Protocol conformance, DTO immutability + slots, factory rejection on unknown strategy + missing build flag, single-thread enforcement.
### Excluded
- Inbound MAVLink + MSP2 decoder bodies — owned by next task.
- `CovarianceProjector` — owned by next task.
- `PymavlinkArdupilotAdapter` outbound body — owned by AP outbound task.
- `Msp2InavAdapter` outbound body — owned by iNav outbound task.
- MAVLink 2.0 signing handshake — owned by signing task.
- D-C8-2 source-set switch body — owned by source-set task.
- `QgcTelemetryAdapter` body — owned by GCS task.
- C8-IT/PT/ST tests — deferred to E-BBT (AZ-262).
## Acceptance Criteria
**AC-1: Protocol conformance**`runtime_checkable` `isinstance` returns True for fakes implementing each Protocol's full method set.
**AC-2: DTOs frozen + slots**`FrozenInstanceError` on mutation; `__slots__` non-empty for every DTO.
**AC-3: Enum membership**`FcKind` has 2 values (ARDUPILOT_PLANE, INAV); `FlightState` has 5 (INIT/ARMED/IN_FLIGHT/ON_GROUND/FAILED); `GpsStatus` has 5 (NO_FIX/DEGRADED/STABLE/STABLE_NON_SPOOFED/SPOOFED); `Severity` has 3 (INFO=6, WARNING=4, ERROR=3 — values mirror MAVLink STATUSTEXT severities).
**AC-4: Factory rejects missing build flag**`config.fc.adapter = "ardupilot_plane"` with `BUILD_FC_ARDUPILOT_PLANE=OFF``FcAdapterConfigError("BUILD_FC_ARDUPILOT_PLANE is OFF...")`.
**AC-5: Factory rejects unknown strategy at config-load**`config.fc.adapter = "garbage"``FcAdapterConfigError` at config load (NOT at build time).
**AC-6: Single-writer thread for outbound** — composition root binds outbound to ONE thread; second binding raises `RuntimeError`.
**AC-7: GCS factory parallel coverage** — same set of acceptance behaviours for `build_gcs_adapter` against the `GcsAdapter` Protocol.
**AC-8: Public API re-exports**`from gps_denied_onboard.components.c8_fc_adapter import FcAdapter, GcsAdapter, EmittedExternalPosition` resolves; internal modules NOT in `__all__`.
**AC-9: Error hierarchy catchability** — every FC error caught by `except FcAdapterError`; every GCS error caught by `except GcsAdapterError`. `SourceSetSwitchNotSupportedError` is also a `SourceSetSwitchError` (sub-typed for iNav rejection).
**AC-10: INFO log on build** — successful build logs `kind="c8.adapter.strategy_loaded"` once per adapter with the strategy name + port device.
## Non-Functional Requirements
- `build_fc_adapter` p99 ≤ 50 ms.
- `build_gcs_adapter` p99 ≤ 50 ms.
## Constraints
- `@runtime_checkable` on both Protocols; DTOs `frozen=True, slots=True`.
- Lazy-import per ADR-002.
- Single-thread binding enforced for outbound (AC-6).
- Public API surface limited to the two re-export sets (per `module-layout.md`).
## Risks & Mitigation
- **Risk**: Protocol surface changes after consumer tasks land. *Mitigation*: this task ships first; downstream tasks reference the Protocol shape locked here. Any extension is additive (new method on the Protocol implies a default no-op fallback or a follow-up Protocol version bump documented in the contract).
- **Risk**: Single-thread binding bug breaks the multi-consumer (C1 + C5) inbound path. *Mitigation*: AC-6 covers ONLY outbound; inbound subscribe-callback semantics are documented as fire-on-decode-thread + consumer responsibility (Invariant 8).
## Runtime Completeness
- **Named capability**: C8 Protocols + DTOs + factories.
- **Production code**: real Protocols, real DTOs, real error hierarchy, real factories, real composition-root wiring.
- **Allowed external stubs**: test fakes only; no production code may import `FcAdapterStub` outside tests.
- **Unacceptable substitutes**: hardcoding the C8 strategy class in the runtime root (defeats ADR-009); skipping the Protocol surface.
## Contract
Implements `_docs/02_document/contracts/c8_fc_adapter/fc_adapter_protocol.md`.