# 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_` / `BUILD_GCS_` 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`.