# C8 — Flight-Controller Adapter

## 1. High-Level Overview

**Purpose**: own the per-FC inbound + outbound communication. Inbound: subscribe to FC IMU/attitude/GPS-health/MAV_STATE telemetry; publish `ImuWindow` / `AttitudeWindow` / `GpsHealth` / `FlightStateSignal` for upstream consumers. Outbound: encode `EstimatorOutput` into the per-FC external-position contract (`GPS_INPUT` for ArduPilot Plane, `MSP2_SENSOR_GPS` for iNav) at 5 Hz periodic with honest 6×6 → 2×2 covariance projection. Owns MAVLink 2.0 message signing on the AP wired channel (D-C8-9 = (d)) and the per-flight key rotation. Also feeds the GCS link with downsampled telemetry (1–2 Hz per AC-6.1).

**Architectural Pattern**: Strategy — `FcAdapter` interface with two concrete implementations: `PymavlinkArdupilotAdapter`, `Msp2InavAdapter`. Plus a `GcsAdapter` (single concrete `QgcTelemetryAdapter` today). All selected at startup by config (ADR-001), build-time gating per `BUILD_*` flags (ADR-002, both adapters typically linked into the deployment binary so a single image can target both FCs by configuration), composition-root wired (ADR-009).

**Cycle-1 operational reality**: C8 is composed via a **separate registry path** from the rest of the strategy-selecting components — there is no `c8_fc_adapter` slot in the central `_STRATEGY_REGISTRY` populated by `register_airborne_strategies()` (AZ-591), and no `c8_*` row in `AIRBORNE_REQUIRED_PRE_CONSTRUCTED_KEYS`. Instead `runtime_root/fc_factory.py` owns two private registries — `_FC_REGISTRY` and `_GCS_REGISTRY` — and exposes `register_fc_adapter()` / `register_gcs_adapter()` so per-binary bootstrap modules (one per `BUILD_FC_<VARIANT>` / `BUILD_GCS_<VARIANT>` combination) can lazy-link the concrete adapter classes ahead of `build_fc_adapter()` / `build_gcs_adapter()`. Both calls run the build-flag gate against `_FC_BUILD_FLAGS = {"ardupilot_plane": "BUILD_FC_ARDUPILOT_PLANE", "inav": "BUILD_FC_INAV"}` and `_GCS_BUILD_FLAGS = {"qgc_mavlink": "BUILD_GCS_QGC_MAVLINK"}` and surface an OFF flag as `FcAdapterConfigError` / `GcsAdapterConfigError` naming the disabled flag (AC-4). The outbound single-writer invariant is enforced by `bind_outbound_emit_thread()` — called once per process before wiring outbound emit, a second call from any other thread raises `OutboundThreadAlreadyBoundError` (AC-6 / Invariant 8). Unit-test isolation uses `clear_strategy_registries()` + `clear_outbound_thread_binding()`. C8 takes **no** infrastructure dependency on `pre_constructed`; its `**deps` kwargs are populated by `compose_root`'s C5 / C13 wiring path (per-binary `main()` constructs the concrete adapter with the FC's port config and the C5 estimator's outbound `EstimatorOutput` stream).

**Upstream dependencies**:
- C5 StateEstimator → `EstimatorOutput` (5 Hz periodic emit driver).
- Hardware: UART/USB to FC; UART (or USB) to GCS (often shared or via FC mavlink-routing).

**Downstream consumers**:
- C5 StateEstimator (consumes `ImuWindow`, `AttitudeWindow`, `GpsHealth`, `FlightStateSignal`).
- C1 VIO (consumes `ImuWindow`).
- C13 FDR (consumes raw inbound + emitted outbound MAVLink/MSP2 streams; signing key rotation events; spoof-promotion events).
- C11 `TileUploader` does **NOT** depend on `FlightStateSignal` after Batch 44 (SRP refactor — the post-landing safety gate now lives in C12's `PostLandingUploadOrchestrator`, which reads the `flight_footer` FDR record's `clean_shutdown` field, not the live `FlightStateSignal`). The C11 `TileDownloader` likewise does not depend on `FlightStateSignal` — it runs pre-flight when the companion is plugged into the operator workstation.

## 2. Internal Interfaces

### Interface: `FcAdapter`

| Method | Input | Output | Async | Error Types |
|--------|-------|--------|-------|-------------|
| `open` | `port_config: PortConfig, signing_key: bytes (AP only)` | `None` | No | `FcOpenError`, `SigningHandshakeError` |
| `subscribe_telemetry` | `callback: Callable[[FcTelemetryFrame], None]` | `Subscription` | No | — |
| `emit_external_position` | `EstimatorOutput` | `None` | No | `FcEmitError` |
| `emit_status_text` | `string, severity: enum {INFO, WARN, ERROR}` | `None` | No | `FcEmitError` |
| `request_source_set_switch` (AP only) | `()` | `None` | No | `SourceSetSwitchError` |
| `current_flight_state` | `()` | `FlightStateSignal` | No | — |

### Interface: `GcsAdapter`

| Method | Input | Output | Async | Error Types |
|--------|-------|--------|-------|-------------|
| `emit_summary` | `EstimatorOutput` (downsampled to 1–2 Hz) | `None` | No | `GcsEmitError` |
| `subscribe_operator_commands` | `callback` | `Subscription` | No | — |

**Input/Output DTOs**:
```
PortConfig:
  device:                  string (e.g. /dev/ttyTHS1)
  baud:                    int
  fc_kind:                 enum {ardupilot_plane, inav}

FcTelemetryFrame:
  kind:                    enum {imu_sample, attitude, gps_health, mav_state}
  payload:                 union per kind
  received_at:             monotonic_ns
  signed:                  bool — true only for AP signed frames

EmittedExternalPosition:
  per_fc_payload:          enum (GPS_INPUT for AP / MSP2_SENSOR_GPS for iNav)
  horiz_accuracy_m:        float (AP) — projection of covariance_6x6 → 2x2 → equivalent_radius
  hPosAccuracy_mm:         int (iNav) — same source, mm units (D-C8-8 = (b))
  source_label:            STATUSTEXT / NAMED_VALUE_FLOAT (out-of-band)
```

## 3. External API Specification

C8 exposes no HTTP/gRPC API. Its **external** surfaces are:

- **MAVLink 2.0 messages** to/from ArduPilot Plane, signed (D-C8-9 = (d)). Specifically:
  - In: `RAW_IMU` / `SCALED_IMU2`, `ATTITUDE`, `GPS_RAW_INT`, `GPS2_RAW`, `HEARTBEAT`, `STATUSTEXT`.
  - Out: `GPS_INPUT` (5 Hz), `STATUSTEXT`, `NAMED_VALUE_FLOAT` (provenance label), `COMMAND_LONG MAV_CMD_SET_EKF_SOURCE_SET` (D-C8-2 source-set switch on F7).
- **MSP2 messages** to/from iNav over UART (unsigned, accepted residual risk):
  - In: `MSP2_INAV_ANALOG`, attitude+IMU stream.
  - Out: `MSP2_SENSOR_GPS` (5 Hz). MAVLink outbound for telemetry only (iNav speaks both).
- **MAVLink 2.0 messages** to/from QGroundControl on the GCS link (1–2 Hz downsampled summary out, operator commands in).

Refer to `architecture.md` § 5 External Integrations for the per-message rate/auth/failure-mode table; this spec inherits those values.

## 4. Data Access Patterns

C8 holds no persistent storage. All communication is in-memory streaming.

### Caching Strategy

| Data | Cache Type | TTL | Invalidation |
|------|-----------|-----|-------------|
| Per-flight signing key (AP) | in-memory, generated at takeoff | flight lifetime | new key on next takeoff |
| Last-emitted external position (for downsampling 5 Hz → 1–2 Hz GCS) | in-memory ring | flight lifetime | n/a |
| FC telemetry rolling window (for `ImuWindow`, `AttitudeWindow` consumers) | in-memory bounded ring | bounded by C1/C5 consumption | drop-oldest |

## 5. Implementation Details

**Algorithmic Complexity**: per-message encode/decode is `O(message_size)`; the per-frame work is dominated by the covariance projection (3×3 sub-matrix → equivalent_radius), which is constant-time.

**State Management**:
- AP path: holds the MAVLink 2.0 signing state (key, key generation timestamp, signature counter).
- iNav path: holds the MSP2 channel state (sequence numbers).
- Both paths: hold the bounded telemetry rings and a backpressure budget for the 5 Hz outbound emit.

**Key Dependencies**:

| Library | Version | Purpose |
|---------|---------|---------|
| pymavlink | bundled unmodified per D-C8-3 | AP MAVLink 2.0 + signing |
| YAMSPy | per project pin | iNav MSP2 |
| INAV-Toolkit | per project pin | iNav MSP2 message definitions for `MSP2_SENSOR_GPS` |
| pyserial | per project pin | UART transport |

**Error Handling Strategy**:
- `FcOpenError` at takeoff: refuse takeoff with explicit error.
- `SigningHandshakeError` at takeoff (AP): refuse takeoff. Mid-flight signing failure: FC ignores unsigned messages, AC-5.2 takes over (3 s no estimate → FC IMU-only). Companion logs the rotation/expiry event to FDR.
- `FcEmitError` mid-flight: log + continue; the FC's own staleness detection will eventually trigger AC-5.2.
- `SourceSetSwitchError` (AP only, F7): try once; on failure log + STATUSTEXT to GCS; the system continues to emit `GPS_INPUT` and the operator can manually switch via RC aux per D-C8-2-FALLBACK.
- iNav unsigned: NOT an error — accepted residual risk per Mode B Source #129. Documented in `tests/security-tests.md` NFT-SEC-03.

**Covariance projection** (D-C8-8 = (b)):
- Project 6×6 GTSAM `Marginals` covariance → 3×3 position sub-matrix → 2×2 horizontal sub-matrix → `equivalent_radius` per per-FC contract:
  - AP `horiz_accuracy` (m): `sqrt(0.5 * (sigma_xx + sigma_yy + sqrt((sigma_xx - sigma_yy)^2 + 4*sigma_xy^2)))` (largest eigenvalue of 2×2; documented as the "honest covariance projection" in IT-10).
  - iNav `hPosAccuracy` (mm): same value, converted to mm.

## 6. Extensions and Helpers

| Helper | Purpose | Used By |
|--------|---------|---------|
| `WgsConverter` | shared with C4, C5, C6 | C4, C5, C6, C8 |
| `CovarianceProjector` | 6×6 → 2×2 → equivalent_radius — keep inside C8 (per-FC unit conversion is variant-specific per coderule SRP) | C8 only |

## 7. Caveats & Edge Cases

**Known limitations**:
- iNav has no signing equivalent; accepted residual risk per Mode B Source #129. Plan-phase carryforward proposes an iNav firmware feature request — out of scope for this cycle.
- D-C8-2 source-set switch is firmware-supported but not deployed-precedent; ADR-008 makes IT-3 (ArduPilot SITL validation) the lock gate.

**Potential race conditions**:
- The 5 Hz outbound emit thread MUST not block on the 100–200 Hz inbound telemetry decode thread. Bounded rings + drop-oldest semantics on the inbound side; outbound-emit timer is independent.
- The AP signing key rotation MUST happen between flights, not mid-flight. The key generation is at takeoff load (F2).

**Performance bottlenecks**:
- pymavlink's signing handshake is sub-second on a wired UART/USB link. F2 budget is generous.

## 8. Dependency Graph

**Must be implemented after**: hardware UART config (operator workstation pre-flight stage).

**Can be implemented in parallel with**: C6, C13.

**Blocks**: C1 (FC IMU prior), C5 (factor inputs), F2 / F3 / F5 / F7 / F8 / F9 / F10.

## 9. Logging Strategy

| Log Level | When | Example |
|-----------|------|---------|
| ERROR | `FcOpenError`, `SigningHandshakeError`, persistent `FcEmitError` | `C8 AP signing handshake failed; refusing takeoff` |
| WARN | one-off `FcEmitError`; spoofing-promotion blocked event surfaced via STATUSTEXT | `C8 GPS_INPUT emit dropped; reason=uart_busy; will retry next tick` |
| INFO | adapter ready; key rotation event; F7 source-set switch executed | `C8 AP signing key rotated; flight_id=…; key_age_s=0` |
| DEBUG | per-emit timing | `C8 GPS_INPUT emit frame=12345 took=2.1ms; horiz_accuracy_m=4.2` |

**Log format**: structured JSON.
**Log storage**: stdout / journald / FDR via C13 (ERROR + WARN always; AP signing key rotation events ALWAYS to FDR; F7 source-set switch events ALWAYS to FDR + GCS STATUSTEXT; STATUSTEXT broadcasts ALWAYS mirrored to FDR).
