# 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`.
