# C8 GcsAdapter — QgcTelemetryAdapter (1–2 Hz downsampled summary + operator commands)

**Task**: AZ-397_c8_qgc_telemetry_adapter
**Name**: C8 `QgcTelemetryAdapter` — downsampled 1–2 Hz summary out + operator command in (AC-6.1, AC-6.2)
**Description**: Implement `QgcTelemetryAdapter` (the single concrete `GcsAdapter` strategy in this cycle): open a MAVLink 2.0 channel to QGroundControl on the configured GCS UART; `emit_summary(EstimatorOutput)` is invoked at 5 Hz by the runtime root and is INTERNALLY downsampled to 1–2 Hz (configurable via `config.gcs.summary_rate_hz`, default 2 Hz; downsampling is rate-based — every Nth call). Each downsampled call emits one MAVLink summary frame containing lat/lon/alt + horiz_accuracy + source_label (full WGS84 from the injected `WgsConverter`; horiz_accuracy from the injected `CovarianceProjector.to_ardupilot_horiz_accuracy_m` since the QGC link uses the AP semantic). Body of `subscribe_operator_commands(callback)`: register a pymavlink message-handler that forwards standard MAVLink command messages (PARAM_REQUEST_LIST, PARAM_REQUEST_READ, COMMAND_LONG, REQUEST_DATA_STREAM, MISSION_REQUEST, etc.) to the registered callback as `OperatorCommand` DTOs. Body of `emit_status_text` mirrors STATUSTEXTs from C5 + C8 outbound to the GCS link (this is the canonical operator-facing surface). Body of `open(...)` opens the MAVLink connection; `close()` closes it. Single-writer thread for outbound (Invariant 8 mirror).
**Complexity**: 3 points
**Dependencies**: AZ-390 (`GcsAdapter` Protocol + `OperatorCommand` DTO), AZ-392 (`CovarianceProjector` helper), AZ-279 (`WgsConverter`), AZ-273 (FDR), AZ-263, AZ-269, AZ-266
**Component**: c8_fc_adapter (epic AZ-261 / E-C8)
**Tracker**: AZ-397
**Epic**: AZ-261 (E-C8)

### Document Dependencies

- `_docs/02_document/contracts/c8_fc_adapter/fc_adapter_protocol.md` — `GcsAdapter` Protocol; Invariant 12.
- `_docs/02_document/components/10_c8_fc_adapter/description.md` — § 1 GCS feed at 1–2 Hz; § 3 External API (QGC MAVLink 2.0).
- `_docs/02_document/architecture.md` — § 5 External Integrations (GCS link).

## Problem

Without this task, the operator has no GCS observability — no downsampled position summary on QGC, no STATUSTEXT mirror, no operator command path. AC-6.1 (1–2 Hz GCS stream), AC-6.2 (operator commands accepted), and AC-6.3 (WGS84 output) are unmet at the GCS surface.

## Outcome

- `src/gps_denied_onboard/components/c8_fc_adapter/mavlink_gcs_adapter.py` — `QgcTelemetryAdapter` class implementing `GcsAdapter`.
- Constructor: `__init__(self, config, wgs_converter, covariance_projector, fdr_client, clock)`.
- Body of `emit_summary` — downsampling counter + emit MAVLink summary every Nth call.
- Body of `emit_status_text` — pymavlink statustext_send.
- Body of `subscribe_operator_commands` — pymavlink message-handler registration; translate raw MAVLink commands to `OperatorCommand` DTOs.
- Body of `open` / `close`.
- INFO log on first emit_summary: `kind="c8.gcs.first_summary_emit"`.
- DEBUG log per emit: `kind="c8.gcs.summary_emit"` with `{seq, lat, lon, horiz_accuracy_m, source_label}`.
- Operator command receipt log: `kind="c8.gcs.operator_command"` (audit trail to FDR per § 9 — STATUSTEXT broadcasts ALWAYS mirrored to FDR).

## Scope

### Included
- `QgcTelemetryAdapter` class implementing `GcsAdapter`.
- Downsampling counter (5 Hz → 1–2 Hz).
- Summary frame encoding (MAVLink 2.0).
- STATUSTEXT mirror.
- Operator command subscription + DTO translation.
- `open` / `close` body.
- Unit tests: 5 → 2 Hz downsample (10 input → 4 output; assert mod-N), command subscription invokes callback, STATUSTEXT mirror on the wire, configurable rate (5 Hz / 1 Hz / 2 Hz / 5 Hz validated; out-of-range → `GcsAdapterConfigError`).

### Excluded
- FC adapters — owned by AP / iNav / inbound / signing / source-set tasks.
- C8-IT-04 / IT-05 / IT-06 end-to-end QGC SITL tests — deferred to E-BBT.
- Workflow logic on the operator side (E-C12) — out of scope per epic.

## Acceptance Criteria

**AC-1: 5 → 2 Hz downsampling** — drive 100 `emit_summary` calls; assert exactly 40 MAVLink summary frames on the wire (every 2.5th call rounded — implementation choice between mod-2 = 50 and mod-3 = 33; the implementation MUST document its choice; AC asserts the chosen rate).

**AC-2: Configurable rate** — `config.gcs.summary_rate_hz = 1` → 100 calls produce ~20 frames (every 5th); `summary_rate_hz = 2` → ~40 (every 2.5th); `summary_rate_hz = 5` → 100 (no downsample). Out-of-range (e.g., 10 Hz) → `GcsAdapterConfigError` at config load.

**AC-3: Summary frame fields** — capture a wire summary; decode via pymavlink; assert lat/lon/alt match `WgsConverter` × 1e7 (MAVLink convention); horiz_accuracy matches `CovarianceProjector.to_ardupilot_horiz_accuracy_m`; source_label encoded per the documented mapping.

**AC-4: STATUSTEXT mirror** — call `emit_status_text("hello", Severity.INFO)`; assert one MAVLink STATUSTEXT on the wire with text="hello" + severity=6.

**AC-5: Operator command subscription invokes callback** — register a callback; inject a PARAM_REQUEST_LIST from a fake QGC; assert callback invoked with an `OperatorCommand` DTO.

**AC-6: Operator command FDR audit trail** — every received operator command emits FDR `kind="c8.gcs.operator_command"` with the command type + source MAVLink id.

**AC-7: Single-writer thread for outbound** — second-thread `emit_summary` raises `RuntimeError`.

**AC-8: First emit logged once** — `kind="c8.gcs.first_summary_emit"` INFO log fires exactly once per `open(...)` lifetime.

**AC-9: AC-6.3 WGS84 round-trip** — emit a summary derived from a known local-tangent-plane position; round-trip via `WgsConverter` + decode wire frame; assert ≤ 1 cm position residual (defensive — backstops the helper that C4 + C8 both rely on; full IT lives in C8-IT-06).

**AC-10: GcsAdapterConfigError on bad config** — `summary_rate_hz` outside [0.5, 5.0] → `GcsAdapterConfigError` at config load.

## Non-Functional Requirements

- `emit_summary` p95 ≤ 5 ms (parallel to FC outbound).
- Operator-command callback dispatch p95 ≤ 1 ms (non-blocking enqueue).

## Constraints

- pymavlink bundled unmodified per D-C8-3.
- Single-writer thread for outbound enforced.
- Downsampling is rate-based (every Nth call), NOT selection-based (Invariant 12).
- The QGC link uses the AP MAVLink semantic for horiz_accuracy (m) — iNav-targeted flights still emit a meters-based summary on this link, since QGC speaks MAVLink natively.

## Risks & Mitigation

- **Risk: pymavlink message-handler thread re-entrance bugs** — *Mitigation*: callback dispatch is explicitly non-blocking (enqueue + drain pattern); tests cover concurrent inject + drain.
- **Risk: downsampling rate-config drift between code and operator UI** — *Mitigation*: `summary_rate_hz` is the single source of truth; documented in config schema + Public API.
- **Risk: STATUSTEXT flooding under cascading errors** — *Mitigation*: STATUSTEXT itself is rate-limited at the source (C5 / C8 outbound emit at most once per transition); GCS adapter just forwards.

## Runtime Completeness

- **Named capability**: GCS downsampled telemetry + operator command subscription.
- **Production code**: real pymavlink encode/decode; real downsampling counter; real callback dispatch.
- **Unacceptable substitutes**: a "fake QGC" that only logs without writing to the wire (defeats AC-3/AC-4 wire-level fidelity).

## Contract

Implements `_docs/02_document/contracts/c8_fc_adapter/fc_adapter_protocol.md` — `GcsAdapter` Protocol; Invariant 12.
