Closes out greenfield Step 6 (Decompose) for all 14 components (C1-C13 + cross-cutting helpers/replay). Covers tasks AZ-266..AZ-446 plus the _dependencies_table.md and component contract documents. State file updated to greenfield Step 7 (Implement), not_started. Co-authored-by: Cursor <cursoragent@cursor.com>
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.md—c8_fc_adapterPer-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,GcsAdapterProtocols with all methods per the contract.src/gps_denied_onboard/components/c8_fc_adapter/__init__.py— re-exportsFcAdapter,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_adapterAFTER C5; invokebuild_gcs_adapterAFTERbuild_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.PymavlinkArdupilotAdapteroutbound body — owned by AP outbound task.Msp2InavAdapteroutbound 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.
QgcTelemetryAdapterbody — 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_adapterp99 ≤ 50 ms.build_gcs_adapterp99 ≤ 50 ms.
Constraints
@runtime_checkableon both Protocols; DTOsfrozen=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
FcAdapterStuboutside 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.