Files
gps-denied-onboard/_docs/02_tasks/done/AZ-390_c8_adapter_protocol.md
T
Oleksandr Bezdieniezhnykh 362e93c626 [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>
2026-05-11 04:17:59 +03:00

7.7 KiB

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.mdc8_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.pyFcAdapter, 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.pyPortConfig, 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.pybuild_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 conformanceruntime_checkable isinstance returns True for fakes implementing each Protocol's full method set.

AC-2: DTOs frozen + slotsFrozenInstanceError on mutation; __slots__ non-empty for every DTO.

AC-3: Enum membershipFcKind 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 flagconfig.fc.adapter = "ardupilot_plane" with BUILD_FC_ARDUPILOT_PLANE=OFFFcAdapterConfigError("BUILD_FC_ARDUPILOT_PLANE is OFF...").

AC-5: Factory rejects unknown strategy at config-loadconfig.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-exportsfrom 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.