# C8 D-C8-2 source-set switch (AP only) — MAV_CMD_SET_EKF_SOURCE_SET

**Task**: AZ-396_c8_source_set_switch
**Name**: C8 AP D-C8-2 source-set switch — `MAV_CMD_SET_EKF_SOURCE_SET` + spoof-recovery wiring (AC-NEW-2)
**Description**: Implement `PymavlinkArdupilotAdapter.request_source_set_switch()` per D-C8-2 (AP F7): send `MAV_CMD_SET_EKF_SOURCE_SET` with the configured source-set ID (from `config.fc.spoof_recovery_source_set`, default 1); wait for the FC `COMMAND_ACK` with timeout = `config.fc.source_set_switch_timeout_ms` (default 1500 ms); raise `SourceSetSwitchError` on timeout or non-success ACK. Idempotence per Invariant 11: re-entry within 1 s is no-op'd; re-entry after a successful switch logs INFO + STATUSTEXT but does not re-issue the command. Wired to the C5 spoof-recovery gate (AZ-385 spoof-promotion gate) via the runtime root: when C5 promotes a previously-spoofed FC GPS source per AC-NEW-2, the runtime root invokes `request_source_set_switch()` on the C8 AP adapter; assertion that this completes within 3 s end-to-end is C8-IT-07 (deferred to E-BBT). On `SourceSetSwitchError`, the system continues to emit `GPS_INPUT` and the operator can manually switch via RC aux per D-C8-2-FALLBACK; ERROR log + STATUSTEXT to GCS + FDR record `kind="c8.ap.source_set_switch_failed"`. Replaces the `NotImplementedError` placeholder in AZ-? (AP outbound task).
**Complexity**: 3 points
**Dependencies**: AZ-393 (`PymavlinkArdupilotAdapter` skeleton); AZ-390 (`SourceSetSwitchError`); AZ-385 (C5 spoof-promotion gate — runtime-root wiring sink), AZ-273 (FDR), AZ-272, AZ-263, AZ-269, AZ-266
**Component**: c8_fc_adapter (epic AZ-261 / E-C8)
**Tracker**: AZ-396
**Epic**: AZ-261 (E-C8)

### Document Dependencies

- `_docs/02_document/contracts/c8_fc_adapter/fc_adapter_protocol.md` — Invariant 11.
- `_docs/02_document/components/10_c8_fc_adapter/description.md` — § 5 error handling (D-C8-2-FALLBACK), § 9 logging.
- `_docs/02_document/architecture.md` — D-C8-2, ADR-008 (IT-3 gate); AC-NEW-2 (3 s spoofing-promotion latency).

## Problem

Without this task, AC-NEW-2 (spoofing-promotion latency < 3 s p95 via source-set switch) is not deliverable: there's no executable surface to switch the FC's EKF source set, and the runtime root has no sink to fire when C5's spoof gate promotes. Without the switch, the FC may continue to use a source set the spoof gate has just discredited.

## Outcome

- Body of `PymavlinkArdupilotAdapter.request_source_set_switch()`:
  - Send `MAV_CMD_SET_EKF_SOURCE_SET` via pymavlink `mav.command_long_send(...)`.
  - Wait for `COMMAND_ACK` with the configured timeout.
  - On success: INFO log `kind="c8.ap.source_set_switch_executed"` + FDR record + STATUSTEXT (severity=INFO).
  - On non-success ACK: `SourceSetSwitchError` raised + ERROR log + FDR + STATUSTEXT (severity=ERROR).
  - On timeout: `SourceSetSwitchError("ACK timeout after Xms")` + ERROR log + FDR + STATUSTEXT.
  - Idempotence (Invariant 11): re-entry within 1 s no-op'd; re-entry after successful switch logs INFO + STATUSTEXT but does NOT re-issue.
- Runtime-root extension: subscribe to C5's `spoof_promotion_recovered` signal (kind from AZ-385); on receipt, invoke `request_source_set_switch()` on the C8 AP adapter from the C8 outbound thread.
- Internal state: `_last_switch_attempt_ns`, `_last_switch_succeeded`.
- Unit tests: ACK success path, non-success ACK raises, timeout raises, idempotence within 1 s, idempotence after success, runtime-root signal triggers switch, ERROR log + FDR record on failure.

## Scope

### Included
- `request_source_set_switch()` body.
- Runtime-root wiring to C5 spoof-promotion gate (AZ-385).
- Idempotence per Invariant 11.
- Error path + STATUSTEXT.
- Unit tests.

### Excluded
- iNav source-set switch — not applicable (`SourceSetSwitchNotSupportedError` already in iNav adapter).
- C8-IT-07 end-to-end SITL test — deferred to E-BBT.
- Manual RC aux switch path (D-C8-2-FALLBACK) — operator-side; out of scope for the companion.

## Acceptance Criteria

**AC-1: ACK success path** — emit `MAV_CMD_SET_EKF_SOURCE_SET`; SITL ACKs with `MAV_RESULT_ACCEPTED`; assert INFO log + FDR `kind="c8.ap.source_set_switch_executed"` + STATUSTEXT (severity=INFO).

**AC-2: Non-success ACK raises** — SITL ACKs with `MAV_RESULT_FAILED`; assert `SourceSetSwitchError` raised + ERROR log + FDR `kind="c8.ap.source_set_switch_failed"` + STATUSTEXT (severity=ERROR).

**AC-3: ACK timeout raises** — SITL never ACKs; after 1500 ms (default) → `SourceSetSwitchError("ACK timeout after 1500ms")` + ERROR log.

**AC-4: Configurable timeout** — `config.fc.source_set_switch_timeout_ms = 500` → timeout AC fires at 500 ms.

**AC-5: Idempotence within 1 s** — call `request_source_set_switch()` twice within 1 s; second call no-op'd; only ONE `MAV_CMD_SET_EKF_SOURCE_SET` on the wire; INFO log on the suppressed call: `kind="c8.ap.source_set_switch_rate_limited"`.

**AC-6: Idempotence after successful switch** — after a success, call again; second call logs INFO + STATUSTEXT but does NOT re-emit the command.

**AC-7: Runtime-root spoof-recovery signal triggers switch** — runtime root subscribes to C5's spoof-promotion-recovered signal (AZ-385); inject the signal; assert `request_source_set_switch()` was invoked on the C8 AP adapter.

**AC-8: source_set_id from config** — `MAV_CMD_SET_EKF_SOURCE_SET.param1` = `config.fc.spoof_recovery_source_set` (default 1).

**AC-9: Replaces AP outbound `NotImplementedError`** — verify the AZ-393 AP outbound task's `NotImplementedError` placeholder is removed; the method body is now executable.

**AC-10: STATUSTEXT severity matrix** — success → INFO (6); failure → ERROR (3); rate-limited → INFO (6).

## Non-Functional Requirements

- End-to-end (signal → switch → ACK) latency p95 ≤ 2 s (within the 3 s AC-NEW-2 budget).
- `request_source_set_switch()` p95 ≤ 1.5 s (dominated by ACK round-trip).

## Constraints

- pymavlink bundled unmodified per D-C8-3.
- AP-only — `Msp2InavAdapter.request_source_set_switch` already raises `SourceSetSwitchNotSupportedError`.
- The runtime-root wiring MUST happen on the C8 outbound thread (single-writer invariant).
- D-C8-2-FALLBACK (manual RC aux) is operator-side; this task does NOT implement it.

## Risks & Mitigation

- **R-D-C8-2 (firmware-supported but not deployed-precedent)** — *Mitigation*: ADR-008 makes IT-3 (ArduPilot SITL validation) the lock gate; this task's surface is exercised in IT-3 (deferred to E-BBT).
- **Risk: ACK timeout under high MAVLink traffic** — *Mitigation*: AC-3 + AC-4 cover; 1.5 s default is generous; operator workflow has manual RC aux fallback.
- **Risk: idempotence rate-limit hides operator-initiated re-issues** — *Mitigation*: log every suppressed call at INFO; STATUSTEXT mirror so the operator sees suppression on GCS.

## Runtime Completeness

- **Named capability**: AP D-C8-2 source-set switch + spoof-recovery wiring.
- **Production code**: real pymavlink `command_long_send` + ACK loop; real runtime-root signal subscription; real idempotence.
- **Unacceptable substitutes**: skipping the ACK wait (defeats AC-1/AC-2); always returning success without sending (defeats AC-1).

## Contract

Implements `_docs/02_document/contracts/c8_fc_adapter/fc_adapter_protocol.md` — `request_source_set_switch` for AP; Invariant 11. Delivers AC-NEW-2 wire surface (end-to-end test deferred to C8-IT-07 / E-BBT).
