# Component Fit Matrix — C8: MAVLink / MSP2 FC adapter

> Mode A Phase 2 — engine Step 7.5 (Component Applicability Gate, structured per-component candidate-selection table). Status vocabulary in [`00_summary.md`](00_summary.md). Backing fact cards: [`../02_fact_cards/C8_fc_adapter.md`](../02_fact_cards/C8_fc_adapter.md). Backing sources: [`../01_source_registry/C8_fc_adapter.md`](../01_source_registry/C8_fc_adapter.md).
>
> Index: [`00_summary.md`](00_summary.md). Sibling components: [C1 VIO](C1_vio.md), [C2 VPR](C2_vpr.md), [C3 Matchers](C3_matchers.md), [C4 Pose](C4_pose_estimation.md), [C5 State estimator](C5_state_estimator.md), [C6 Tile cache](C6_tile_cache_spatial_index.md), [C7 Inference runtime](C7_inference_runtime.md), [C10 Pre-flight provisioning](C10_preflight_provisioning.md). C9 dropped per 2026-05-08 restructure — see `../00_question_decomposition.md`. Cross-component gates: [`99_cross_component_gates.md`](99_cross_component_gates.md). Cross-cuts: [SQ6 fact cards](../02_fact_cards/SQ6_fc_external_positioning.md).

---

## C8 — MAVLink / MSP2 FC adapter

**Status**: **CLOSED at 3/N (batch 1 closed 2026-05-08)** at documentary level. Per-FC adapter design is locked at SQ6 closure; C8 batch 1 operationalizes it. **pymavlink → MAVLink GPS_INPUT (Cand 1) RECOMMENDED PRIMARY for ArduPilot Plane**; **MSP2_SENSOR_GPS via Python MSP V2 (Cand 2) RECOMMENDED PRIMARY for iNav**; **UBX impersonation via pyubx2 NAV-PVT (Cand 3) DEFERRED secondary for iNav** (comparative-improvement verdict per user's "significant-improvement-only" bar — does NOT clear the bar over Cand 2). User-pinned scope from `/autodev` AskQuestion 2026-05-08 (with mid-batch contradiction recovery): `c8_breadth=A` (top-1 per FC, narrowest) → `c8_inav_recovery=B` (REVISED to evaluate both MSP2_SENSOR_GPS + UBX impersonation as parallel iNav candidates after I caught the contradiction with locked SQ6 + AC-4.3 + restrictions.md verdicts) + `c8_overkill_options=A` (MAVProxy/mavp2p/ardupilot-router/full MAVSDK noted-and-rejected).

**Pinned mode** (per-FC external-positioning contract for the project's per-frame estimator output):

- inputs: `(estimator_pose: gtsam.Pose3 with covariance from C5; source_label: str ∈ {satellite_anchored, visual_propagated, dead_reckoned} from AC-1.3; covariance_2x2_horizontal: numpy.ndarray of shape (2,2) extracted from C5 6×6 marginalCovariance)` — single shared upstream contract for both FCs
- outputs (per-FC):
  - **AP path**: `master.mav.gps_input_send(time_usec, gps_id, ignore_flags, time_week_ms, time_week, fix_type, lat_deg_e7, lon_deg_e7, alt_m, hdop, vdop, vn_cmps, ve_cmps, vd_cmps, speed_accuracy_mps, horiz_accuracy_m, vert_accuracy_m, satellites_visible, yaw_cdeg)` at 5 Hz (per D-C8-5) over MAVLink (UART/USB/UDP per D-C8-1) to ArduPilot Plane EKF3 via `AP_GPS_MAV` driver (`GPS1_TYPE = 14`)
  - **iNav path Cand 2 (PRIMARY)**: `msp_v2_encode(MSP2_SENSOR_GPS=0x1F03, struct.pack(...))` at 5 Hz over MSP V2 UART/USB to iNav `mspGPSReceiveNewData()` (`USE_GPS_PROTO_MSP` enabled by default in iNav 9.0+)
  - **iNav path Cand 3 (SECONDARY)**: `pyubx2.UBXMessage('NAV', 'NAV-PVT', GET, **kwargs).serialize()` at 5 Hz over UART to iNav `gps_ublox.c` `gpsMapFixType()` validation gate (`flags & 0x01 = 1` AND `fixType ∈ {2,3}` required)
- runtime: Python 3.10+ on Jetson Orin Nano Super (companion side); ArduPilot Plane firmware (4.5+ on FC side for AP) + iNav 9.0+ firmware (FC side for iNav); MAVLink 2.0 dialect for AP; MSP V2 envelope (sync `0x24 0x58 0x3C` + flag + LE 16-bit cmd + LE 16-bit len + payload + CRC-8 DVB-S2) for iNav; UBX wire format (sync `0xB5 0x62` + class + ID + LE 16-bit len + payload + 8-bit Fletcher checksum) for iNav alternate

**Locked-in research-time defaults** (carried forward from C1 + C2 + C3 + C4 + C5 + C6 + C7 — and SQ6):
- D-C1-1 = (c) **keep both license tracks open** through Plan; final license decision deferred to post-Jetson-MVE.
- D-C1-2 = (b) **defer Jetson Orin Nano Super hardware MVE to a dedicated bring-up phase** between research and Plan; research closes with documentary ranking + per-candidate `Verify` gates.
- **SQ6 closure verdict** (locked at fact-card level): per-FC adapter design unavoidable; AP via `GPS_INPUT`, iNav via `MSP2_SENSOR_GPS` primary + UBX emulation alternate.
- **AC-4.3 wording** (locked at problem-doc level): WGS84 coordinates delivered to each supported FC via that FC's documented external-positioning interface — MAVLink `GPS_INPUT` for ArduPilot Plane, MSP2 `MSP2_SENSOR_GPS` for iNav.
- **C8-specific user-pinned scope** (from `/autodev` AskQuestion 2026-05-08):
  - **`c8_breadth = A`** — top-1 per FC; narrowest scope.
  - **`c8_inav_recovery = B`** (REVISED from c8_ubx_impersonation=A after contradiction caught) — evaluate BOTH MSP2_SENSOR_GPS AND UBX impersonation as parallel iNav candidates with comparative-improvement verdict.
  - **`c8_overkill_options = A`** — MAVProxy / mavp2p / ardupilot-router / full MAVSDK C++/Rust noted-and-rejected in one sentence (router-class vs adapter-class distinction; out-of-budget vs Python pymavlink).

**Interactions with prior C-row closures**:
- **C5 covariance contract**: both Cand 1 (AP) and Cand 2 (iNav primary) require honest covariance from C5 estimator. The C5 GTSAM `Marginals.marginalCovariance` path (Fact #89) produces a 6×6 pose covariance matrix; the C8 adapter extracts the 2×2 horizontal sub-matrix and computes the 95% confidence ellipse semi-major axis `sqrt(2.0 * 5.991 * λ_max)` for `horiz_accuracy` (m) for AP or `hPosAccuracy` (mm) for iNav. **Single shared companion-side covariance contract; only the unit + field-name change per FC** per D-C8-8.
- **C7 inference runtime**: C7 is upstream of the estimator that feeds C8; no direct dependency at the C8 layer. The C7 D-C7-6 per-model-family precision policy affects C5 input quality, but C8 sees only the post-estimator pose+covariance bundle.
- **C6 tile cache**: no direct dependency at C8 layer; C8 is downstream of the per-frame estimator.
- **C4 pose estimation**: no direct dependency; C4 → C5 → C8 chain.
- **Test Spec (greenfield Step 5) — SITL test harness ownership**: per the 2026-05-08 C9 / SQ7 restructure (user choice A in `../00_question_decomposition.md`), the SITL test environment for AC-NEW-2 spoof-promotion validation moves out of research scope and into Test Spec. SITL must run BOTH FCs (ArduPilot Plane SITL AND iNav SITL/HITL) per the "production param sets: pass = 95th percentile <3 s on both" wording.
- **AC-NEW-7 audit-trail integrity**: Cand 3 (UBX impersonation) is unambiguously a forgery posture — companion impersonates a u-blox receiver to iNav. Per D-C8-7, requires explicit FDR audit entry on every impersonation session start naming companion as the UBX source. Cand 2 (MSP2_SENSOR_GPS) has clean audit-trail posture — uses iNav's documented sensor-injection path.
- **Cross-suite consistency**: parent-suite components (sat-provider, GCS frontend, etc.) do not communicate with FCs directly — no cross-suite cascade for C8.

| # | Candidate | License | Per-mode verification | Status | Lead reason / disqualifier | Sub-matrix cite |
|---|---|---|---|---|---|---|
| 1 | **pymavlink → MAVLink `GPS_INPUT` (msg 232) primary** for ArduPilot Plane — companion-side `mavutil.mavlink_connection(...)` + `master.mav.gps_input_send(time_usec, gps_id, ignore_flags, time_week_ms, time_week, fix_type, lat, lon, alt, hdop, vdop, vn, ve, vd, speed_accuracy, horiz_accuracy, vert_accuracy, satellites_visible, yaw)` periodic injection over MAVLink (UART/USB/UDP per D-C8-1) at 5 Hz (per D-C8-5); FC-side `GPS1_TYPE=14` MAVLink + `EK3_SRC1_POSXY=3` GPS source-set drives EKF3 ingestion via `AP_GPS_MAV`; companion-driven `MAV_CMD_SET_EKF_SOURCE_SET` for AC-NEW-2 spoof promotion per D-C8-2 | **Clean throughout** — pymavlink **LGPL-3.0** (Source #106; LGPL §6 allows linking from Apache-2.0 app per project's standard requirements.txt deployment posture per D-C8-3 = (a)); MAVLink generated dialects **MIT** (transitive dependency); ArduPilot Plane firmware **GPL-3.0** (FC-side; project does not modify firmware so GPL-3.0 stays bounded to the firmware artifact) — **eligible on every D-C1-1 license-posture choice** since pymavlink LGPL-3.0 allows linking from both BSD/permissive-only-track and GPL-3.0-track companion applications | ✅ Mode enumeration via Source #106 ArduPilot pymavlink context7 lookup at `/ardupilot/pymavlink` (32 code snippets, Source Reputation High) supplemented by Source #107 ArduPilot Plane Non-GPS Position Estimation dev docs + Source #4 (SQ6) AP_GPS_MAV.cpp master ingestion-path source verification — `master.mav.gps_input_send(...)` send pattern verified against the canonical ArduPilot common.xml MAVLink dialect; `GPS1_TYPE=14` parameter requirement verified at Source #107; AP_GPS_MAV ingestion path including `ignore_flags` semantics verified at SQ6 Fact #1 + Fact #2; ✅ runnable example via `pip install pymavlink` + canonical `mavutil.mavlink_connection('udpout:127.0.0.1:14550')` + `master.mav.gps_input_send(...)`; ✅ **NATIVE covariance honesty contract** via `horiz_accuracy/vert_accuracy/speed_accuracy` fields wired through EKF3 quality chain per SQ6 Fact #2 (`EK3_GLITCH_RADIUS` PR #24135 soft de-weighting consumes honest covariance); ✅ **NATIVE source-set switching from companion** via `MAV_CMD_SET_EKF_SOURCE_SET` per SQ6 Fact #3 (works at firmware level; no GCS-UI path required for companion-driven switch — D-C8-2); ⚠️ `ODOMETRY`-velocity-only NOT supported on current AP per SQ6 Fact #4 — `visual_propagated` source label rides `GPS_INPUT` with widened `horiz_accuracy`, NOT a separate ODOMETRY channel (architectural constraint, NOT a Cand 1 disqualifier; preserved by design) | **Mandatory primary for ArduPilot side + RECOMMENDED PRIMARY** (engine Component Option Breadth rule role for ArduPilot side); deployment-ready under every D-C1-1 license-posture choice (LGPL-3.0 transitive dependency does not block either BSD/permissive-only-track or GPL-3.0-track project posture); EMPIRICALLY-CONFIRMED architectural fit via SQ6 closure 2026-05-07 (the per-FC adapter design conclusion was reached on the basis of three independent rounds of L1 source-code search yielding the same architectural verdict) | **THREE CONVERGING POSITIVE structural advantages**: (i) **CANONICAL ArduPilot Python stack** — pymavlink is the ArduPilot-team-maintained Python MAVLink implementation; AP_GPS_MAV ingestion path is implemented in the same firmware repository as the pymavlink generator; (ii) **NATIVE covariance honesty contract** — three covariance fields (`horiz_accuracy/vert_accuracy/speed_accuracy`) align directly with C5 estimator output; AC-NEW-4 false-position-safety budget is wired through to EKF3 quality chain natively; (iii) **NATIVE source-set switching** — `MAV_CMD_SET_EKF_SOURCE_SET` works at firmware level despite no GCS-UI path; companion can drive AC-NEW-2 spoof-promotion without GCS interaction. **TWO MITIGABLE structural findings**: (iv) **LGPL-3.0 license posture** for pymavlink — mitigated via D-C8-3 = (a) standard requirements.txt deployment with version pin; LGPL §6 allows linking from Apache-2.0 app without obligation beyond republishing modifications (project does not modify pymavlink); (v) **`MAV_CMD_SET_EKF_SOURCE_SET` not exposed in stock GCS UIs** per SQ6 Fact #3 caveat — mitigated: companion-driven switch is the project's canonical pattern (D-C8-2 = (b)). **NO HARD CAVEATS**: every project requirement on the AP side is either Pass or Verify-with-specific-validation-step (per AC sub-matrix in Fact #97). Final ranking confirmed at Plan-phase Jetson MVE per D-C1-2 + D-C8-1 (transport choice) + D-C8-2 (source-set strategy) + D-C8-8 (cross-FC covariance contract) | Fact #97 in [`../02_fact_cards/C8_fc_adapter.md`](../02_fact_cards/C8_fc_adapter.md) (per-mode entry + per-numbered-Restriction × per-numbered-AC sub-matrix block) |
| 2 | **`MSP2_SENSOR_GPS` (id 7939 / 0x1F03) via Python MSP V2 primary** for iNav — companion-side `msp_v2_encode(MSP2_SENSOR_GPS, struct.pack('<BHIBBHHHHIIIiiiHHHBBBBB', instance, gps_week, ms_tow, fix_type, sats_in_view, h_pos_acc_mm, v_pos_acc_mm, h_vel_acc_cmps, hdop_x100, lon_deg_e7, lat_deg_e7, msl_alt_cm, ned_n_cmps, ned_e_cmps, ned_d_cmps, ground_course_deg_x100, true_yaw_deg_x100, year, month, day, hour, min_, sec))` periodic injection over MSP V2 UART/USB at 5 Hz (per D-C8-5) using YAMSPy or INAV-Toolkit `msp_v2_encode` (per D-C8-4); FC-side `mspGPSReceiveNewData()` direct passthrough (no validation gate beyond the data parse itself) | **Clean throughout** — YAMSPy + INAV-Toolkit Python implementations **MIT** (Source #112; eligible on every D-C1-1 license-posture choice; clean dual-use compatible); iNav firmware **GPL-3.0** (FC-side; project does not modify firmware so GPL-3.0 stays bounded to firmware artifact); `USE_GPS_PROTO_MSP` build flag is **enabled by default in iNav target/common.h** per SQ6 Source #13 (so stock iNav firmware reaches this path without a custom build) | ✅ Payload structure verified at Source #111 (iNav `docs/development/msp/README.md` master, lines 2999-3031) — full 36-byte payload spec including all AC-NEW-4 covariance honesty fields (`hPosAccuracy/vPosAccuracy/hVelAccuracy`) + AC-NEW-8 graceful degrade enum (`gpsFixType_e` 6-level: `GPS_NO_FIX/GPS_FIX_2D/GPS_FIX_3D/...`); ✅ MSP V2 envelope spec verified at Source #113 (`msp_protocol_v2_sensor.h` master) — `0x1F03` command ID + sensor-message-range 0x1F00-0x1FFF + MSP V2 framing (`0x24 0x58 0x3C` + flag + LE 16-bit cmd + LE 16-bit len + payload + CRC-8 DVB-S2); ✅ Python implementation via Source #112 — YAMSPy (community-blessed for iNav external-device comms per Issue #4465; MIT) OR INAV-Toolkit `inav_msp.py` (951-line MIT primary-source reference for `msp_v2_encode`/`msp_v2_decode`); ✅ runnable example via either library + Python `struct.pack`; ✅ **NATIVE covariance honesty contract** via `hPosAccuracy/vPosAccuracy/hVelAccuracy` fields aligned DIRECTLY with AP `GPS_INPUT.horiz_accuracy/vert_accuracy/speed_accuracy` — single shared companion-side covariance contract per D-C8-8; ✅ **iNav-side direct passthrough** — `mspGPSReceiveNewData()` does not implement an additional validation gate beyond data parse; companion-side honesty is mandatory per SQ6 Fact #8 ("iNav explicitly does NOT validate GPS for spoofing"); ⚠️ MSP V2 sensor-message-range support in YAMSPy + INAV-Toolkit may LAG upstream iNav — community libraries focus on MSP V2 baseline; if Plan-phase reveals the chosen library does not support `0x1F03` natively, project may need to extend OR use a thin custom encoder via the canonical `msp_v2_encode` primitive (mitigated: D-C8-4 picks the library AND allows option (c) thin custom encoder using struct.pack + CRC-8 DVB-S2 helper as a 50-line bespoke fallback) | **Mandatory primary for iNav side + RECOMMENDED PRIMARY** (engine Component Option Breadth rule role for iNav side; locked SQ6 closure lead per Fact #6 + locked AC-4.3 transport per restrictions.md); deployment-ready under every D-C1-1 license-posture choice (MIT throughout; clean dual-use compatible); EMPIRICALLY-CONFIRMED architectural fit via SQ6 closure 2026-05-07 + COVARIANCE-CONTRACT-CONSISTENCY with AP path | **FOUR CONVERGING POSITIVE structural advantages**: (i) **DOCUMENTED SENSOR-INJECTION PATH (NOT impersonation)** — MSP2_SENSOR_GPS is iNav's official MSP V2 sensor-plugin GPS injection interface; AC-NEW-7 audit-trail posture is clean (no forgery framing); (ii) **NATIVE covariance honesty contract** — three covariance fields align directly with AP `GPS_INPUT` field semantics; same companion-side covariance code path for both FCs (D-C8-8); (iii) **SIMPLE PROTOCOL SURFACE** — single message ID (0x1F03), 36-byte payload, CRC-8 DVB-S2 checksum; no startup handshake, no ACK/NAK protocol, no version-advertisement requirement (contrast: Cand 3 needs NAV-PVT + NAV-VER + CFG-* surface); (iv) **iNav-default build flag** — `USE_GPS_PROTO_MSP` enabled in iNav `target/common.h` so stock firmware reaches this path; no custom iNav build required. **TWO MITIGABLE structural findings**: (v) **community library maturity** — YAMSPy + INAV-Toolkit support MSP V2 baseline but MSP V2 sensor-message-range may need extension (mitigated: D-C8-4 = (a) YAMSPy with option (c) thin custom encoder fallback; ~50-line bespoke encoder using `struct.pack` + CRC-8 DVB-S2 if extension proves problematic); (vi) **iNav nav-stack covariance consumption AC-NEW-4 verify-cell** — need to confirm `mspGPSReceiveNewData()` actually USES `hPosAccuracy/vPosAccuracy/hVelAccuracy` for outlier handling (deferred to design phase per SQ6 follow-up; mitigated by SQ6 closure verdict that iNav `gps_msp.c` is the primary-source reference). **NO HARD CAVEATS**: every project requirement on iNav side via MSP2 is either Pass or Verify-with-specific-validation-step. **COMPARATIVE-IMPROVEMENT-VS-CAND-3 VERDICT** (per user's "significant-improvement-only" bar from session-start clarification): Cand 2's improvements over Cand 3 (cleaner audit-trail posture, simpler protocol surface, single-message contract, default-enabled build flag, direct field-name alignment with AP path) are MATERIAL; Cand 3's improvements over Cand 2 (more mature library landscape via pyubx2, no dependency on USE_GPS_PROTO_MSP build flag) are NOT MATERIAL — pyubx2 maturity advantage is offset by community-library-extensibility for MSP V2 + Cand 2's complete primary-source reference (951 lines INAV-Toolkit). **Cand 2 IS RECOMMENDED PRIMARY for iNav side**. Final ranking confirmed at Plan-phase Jetson MVE per D-C1-2 + D-C8-4 (Python MSP V2 implementation choice) + D-C8-5 (MSP2 injection rate) + D-C8-8 (cross-FC covariance contract) | Fact #99 in [`../02_fact_cards/C8_fc_adapter.md`](../02_fact_cards/C8_fc_adapter.md) (per-mode entry + per-numbered-Restriction × per-numbered-AC sub-matrix block + comparative-improvement-vs-Cand-3 analysis) |
| 3 | **UBX impersonation via pyubx2 NAV-PVT secondary alternate** for iNav — companion-side `pyubx2.UBXMessage('NAV', 'NAV-PVT', GET, fixType=3, flags=0b00000001, **kwargs).serialize()` + UART write at 5 Hz to iNav's GPS port; iNav-side `gpsMapFixType()` validation gate requires `flags & 0x01 = 1` (gnssFixOK) AND `fixType ∈ {2,3}`; companion advertises u-blox version ≥ 15.0 via NAV-VER startup frame to drive iNav into NAV-PVT-only protocol mode per D-C8-6 = (a) | **Clean** — pyubx2 **BSD-3-Clause** (Source #108; clean dual-use compatible); u-blox NAV-PVT protocol spec is open vendor documentation (Source #109); iNav firmware **GPL-3.0** (FC-side; project does not modify firmware) — **eligible on every D-C1-1 license-posture choice** | ✅ Mode enumeration via Source #108 pyubx2 context7 lookup at `/semuconsulting/pyubx2` (139 code snippets, Source Reputation High, Benchmark Score 86.8) supplemented by `/websites/semuconsulting_pyubx2` (239 additional code snippets, Benchmark 85.2) — `UBXMessage(ubxClass, ubxID, mode, **kwargs)` constructor verified with three modes (`GET`, `SET`, `POLL`) + bitfield-attribute mode (`parsebitfield=1`) for individual flag bits + `serialize()` method for wire-format output; ✅ NAV-PVT canonical 92-byte payload spec verified at Source #109 (u-blox NEO-M9N + M8 official protocol specifications); ✅ iNav-side validation gate verified at Source #110 lines 215-220 + line 654 (`gps_ublox.c` master) — `gpsMapFixType(fix_status & NAV_STATUS_FIX_VALID, fix_type)` requires `flags & 0x01 = 1` AND `fix_type ∈ {FIX_2D=2, FIX_3D=3}`; ✅ runnable example via `pip install pyubx2` + canonical `UBXMessage('NAV', 'NAV-PVT', GET, **kwargs).serialize()` + serial UART write; ✅ **NATIVE covariance honesty contract** via NAV-PVT `hAcc/vAcc/sAcc` fields (mm / mm / mm/s units); ⚠️ **FORGERY POSTURE** — companion impersonates a u-blox receiver to iNav's stock GPS pipeline (AC-NEW-7 audit-trail nuance per D-C8-7); ⚠️ richer protocol surface than Cand 2 — companion must implement NAV-PVT periodic + NAV-VER startup advertisement + CFG-MSG/CFG-RATE ACK behaviour for iNav-side configuration polls (Source #110 lines 1024-1060 evidence) | **Documentary-evaluable alternate, DEFERRED secondary** for iNav side after comparative-improvement verdict (does NOT clear user's "significant-improvement-only" bar over Cand 2); deployment-ready under every D-C1-1 license-posture choice (BSD-3-Clause clean throughout); CONTINGENT on Plan-phase D-C8-6 UBX-version-advertisement strategy + D-C8-7 AC-NEW-7 audit-trail-posture decision; final ranking deferred to Jetson MVE phase per D-C1-2 — but only IF Cand 2 promotion criteria fail to materialize | **TWO POSITIVE structural advantages over Cand 2**: (i) **MORE MATURE LIBRARY LANDSCAPE** — pyubx2 is the canonical Python UBX/NMEA/RTCM3 parser; daily-active maintenance; 139+239 context7 code snippets vs YAMSPy/INAV-Toolkit's ~MSP V2 baseline coverage; (ii) **NO BUILD-FLAG DEPENDENCY** — UBX path is always available in iNav (no `USE_GPS_PROTO_MSP` requirement); Cand 2 depends on iNav target/common.h default which is enabled but could theoretically be disabled in custom builds. **FOUR NEGATIVE structural findings**: (iii) **RICHER PROTOCOL SURFACE** — NAV-PVT periodic + NAV-VER startup + CFG-MSG/CFG-RATE ACK behaviour vs Cand 2's single-message MSP2_SENSOR_GPS contract; ~3-5× the implementation surface for the same architectural outcome; (iv) **STRICTER iNAV-SIDE VALIDATION GATE** — `gpsMapFixType()` requires both `flags & 0x01 = 1` AND `fix_type ∈ {2,3}`; companion-side bug that fails either condition silently zeroes out the position. Cand 2's `mspGPSReceiveNewData()` direct passthrough catches malformed frames at MSP V2 framing level only, NOT at semantic validation level — but the project considers both contracts safe (companion-side is the integrity owner regardless of FC-side validation depth); (v) **AC-NEW-7 FORGERY POSTURE** — UBX impersonation is unambiguously a forgery operation; companion pretends to be a u-blox receiver to iNav. Per D-C8-7 mitigation requires explicit FDR audit entry on every impersonation session start naming companion as the UBX source — this is a code obligation Cand 2 does not have; (vi) **DIVERGENT FROM AP PATH** — Cand 2's covariance fields align name-for-name with AP `GPS_INPUT.horiz_accuracy/vert_accuracy/speed_accuracy`; Cand 3 introduces the NAV-PVT-specific `hAcc/vAcc/sAcc` field-name divergence (cross-FC consistency cost). **COMPARATIVE-IMPROVEMENT-VS-CAND-2 VERDICT**: Cand 3's "more mature library" advantage IS REAL but the project's MSP V2 sensor-message-range needs are well-bounded (single message ID, 36-byte payload); the canonical `msp_v2_encode` primitive in INAV-Toolkit (951 lines of primary-source reference) is sufficient. Cand 3's downsides (richer protocol surface, stricter validation gate, AC-NEW-7 forgery posture, AP-path field-name divergence) outweigh the upside. **Cand 3 promotion criteria** (DEFERRED-secondary for re-evaluation): re-evaluate IF (a) Plan-phase Jetson MVE reveals YAMSPy + INAV-Toolkit + thin custom encoder ALL fail to support MSP V2 sensor-message-range adequately AND project chooses NOT to commit to extending one of them, OR (b) Jetson MVE reveals `mspGPSReceiveNewData()` does NOT use the covariance fields per AC-NEW-4 verify-cell (effectively making MSP2_SENSOR_GPS no better than `MSP_SET_RAW_GPS` for outlier rejection) AND project needs the stricter `gpsMapFixType()` validation contract for runtime sanity-checking, OR (c) the project re-opens AC-NEW-7 framing and decides UBX impersonation is preferred for some yet-to-be-identified safety reason | Fact #98 in [`../02_fact_cards/C8_fc_adapter.md`](../02_fact_cards/C8_fc_adapter.md) (per-mode entry + per-numbered-Restriction × per-numbered-AC sub-matrix block + comparative-improvement-vs-Cand-2 analysis) |

### C8 — Plan-phase deliverables raised by candidate closures

(Detailed Plan-phase Choose blocks D-C8-1..8 documented in [`99_cross_component_gates.md`](99_cross_component_gates.md). Cross-component gates with C5 covariance contract: D-C8-8.)

| Decision | Owner | Phase | Disposition (research-time recommendation) |
|---|---|---|---|
| **D-C8-1** — pymavlink connection-string transport choice (UDP / serial / TCP / env-driven default-UART) | Plan-phase architect | Plan | **(d) all three configurable, default UART for production** RECOMMENDED |
| **D-C8-2** — `MAV_CMD_SET_EKF_SOURCE_SET` companion-driven switch ownership pattern | Plan-phase architect + AC-NEW-2 owner | Plan | **(b) companion publishes to source-set 2 + auto-switches FC to set 2 on first valid fix** RECOMMENDED |
| **D-C8-3** — pymavlink LGPL-3.0 license-posture verification | Plan-phase architect + license owner | Plan | **(a) bundle pymavlink unmodified + publish requirements.txt with version pin** RECOMMENDED ~standard LGPL §6 compliance |
| **D-C8-4** — Python MSP V2 implementation choice (YAMSPy vs INAV-Toolkit primitive vs thin custom encoder) | Plan-phase architect | Plan | **(a) YAMSPy primary, with (c) thin custom encoder option as fallback if YAMSPy lacks MSP2_SENSOR_GPS support** RECOMMENDED |
| **D-C8-5** — MSP2_SENSOR_GPS injection rate (5 Hz / 10 Hz / variable) | Plan-phase architect | Plan | **(a) 5 Hz periodic** RECOMMENDED ~matches GPS_INPUT 5 Hz cadence on AP side, single-rate cross-FC consistency |
| **D-C8-6** — IF Cand 3 selected → UBX-version-advertisement strategy | Plan-phase architect | Plan (Cand-3-only-contingent) | **(a) advertise hwVersion ≥ M9 + swVersion ≥ 15.00; force iNav into NAV-PVT-only protocol surface** RECOMMENDED ~simplest |
| **D-C8-7** — IF Cand 3 selected → AC-NEW-7 audit-trail posture | Plan-phase architect + AC-NEW-7 owner | Plan (Cand-3-only-contingent) | **(a) explicit FDR audit entry on every impersonation session start** RECOMMENDED |
| **D-C8-8 (CROSS-COMPONENT with C5)** — covariance-honesty cross-FC enforcement | Plan-phase architect + AC-NEW-4 owner | Plan | **(b) per-FC covariance unit conversion: AP m vs iNav mm; same source covariance, formatted per-FC** RECOMMENDED |

### C8 — Cross-row dependencies + working summary

- **C5 covariance integration** — C5 GTSAM `Marginals.marginalCovariance` 6×6 matrix → C8 extracts 2×2 horizontal sub-matrix → 95% confidence ellipse semi-major axis `sqrt(2.0 * 5.991 * λ_max)` → emitted as `horiz_accuracy` (m) for AP / `hPosAccuracy` (mm) for iNav. Single shared upstream contract per D-C8-8.
- **AC-NEW-2 spoof-promotion latency cross-FC validation** — Test Spec (greenfield Step 5) owns the validation per the 2026-05-08 C9 / SQ7 restructure; both FCs (ArduPilot Plane SITL AND iNav SITL/HITL) in scope; pass = 95th percentile <3 s per AC-NEW-2.Validation.
- **AC-NEW-8 visual-blackout cross-FC behaviour** — both FCs map covariance>100 m / blackout>30 s thresholds to `fix_type` enum degrade levels; AP sentinel `horiz_accuracy=999.0` for "no fix"; iNav sentinel `fixType=GPS_NO_FIX` (0) for "no fix".
- **Test Spec (greenfield Step 5) — SITL + replay test harness ownership** — per the 2026-05-08 C9 / SQ7 restructure, owns the SITL test harness for both FCs; AC-NEW-2 + AC-NEW-8 validation paths; production param sets per FC.
- **AC-NEW-3 FDR audit trail** — C8 emits all MAVLink GPS_INPUT frames + MSP2_SENSOR_GPS frames + (if Cand 3 selected) UBX NAV-PVT frames to the local FDR; raw stream capture is trivial via pymavlink (tlog) + INAV-Toolkit / YAMSPy frame logging + pyubx2 raw-bytes capture.
- **AC-NEW-7 audit-trail integrity** — Cand 2 (MSP2_SENSOR_GPS) clean; Cand 3 (UBX impersonation) requires D-C8-7 explicit FDR audit entry.

### Boundary check: C8 batch 1 saturation status

C8 batch 1 (3 of N candidate adapters) closed at the documentary level on 2026-05-08:
- **Cand 1 (Fact #97, ArduPilot pymavlink → GPS_INPUT)** — RECOMMENDED PRIMARY for AP side. Documentary verification ✅; per-AC sub-matrix complete; D-C8-1..3 + D-C8-8 raised.
- **Cand 2 (Fact #99, iNav MSP2_SENSOR_GPS via Python MSP V2)** — RECOMMENDED PRIMARY for iNav side. Documentary verification ✅; per-AC sub-matrix complete; D-C8-4..5 + D-C8-8 raised.
- **Cand 3 (Fact #98, iNav UBX impersonation via pyubx2 NAV-PVT)** — DEFERRED secondary for iNav side. Documentary verification ✅ via direct iNav source-code read of validation gate; per-AC sub-matrix complete; D-C8-6..7 raised (Cand-3-only-contingent).

**Saturation rationale**: SQ6 closure already established the per-FC adapter design; C8 batch 1 operationalized the candidates per the user-pinned scope. No additional candidates surfaced that haven't been considered-and-rejected per `c8_overkill_options=A` (MAVProxy/mavp2p/ardupilot-router are router-class not adapter-class; full MAVSDK C++/Rust SDKs are out-of-budget vs Python pymavlink; iNav has no third inbound transport beyond MSP2 + UBX in 9.0 master). Mid-batch contradiction recovery via `c8_inav_recovery=B` honored locked SQ6 + AC-4.3 + restrictions.md verdicts (MSP2_SENSOR_GPS as iNav primary) while still letting the user evaluate UBX impersonation as parallel candidate.

C8 batch 1 closure is gated only on the eight Plan-phase decisions D-C8-1..8 + the cross-component C5 covariance contract integration D-C8-8.
