AZ-393 ArduPilot outbound: PymavlinkArdupilotAdapter encodes EstimatorOutput to MAVLink2 GPS_INPUT via gps_input_send; emits NAMED_VALUE_FLOAT(name="src_lbl") every frame and STATUSTEXT on source_label transition (1 Hz per-severity cap). Smoothed-output guard (Invariant 6), single-writer thread (Invariant 8), SPD propagation. Shared helper _outbound_provenance.py owns the canonical source-label-to-float table + transition rate-limiter. AZ-394 iNav outbound: Msp2InavAdapter encodes EstimatorOutput to hand-rolled MSP2_SENSOR_GPS (0x1F03, 52-byte LE payload via _msp2_sensor_gps_encoder.py + YAMSPy send_RAW_msg). Secondary unsigned MAVLink channel for STATUSTEXT transitions. open() rejects non-None signing_key (RESTRICT-COMM-2 / Invariant 2); request_source_set_switch raises SourceSetSwitchNotSupportedError (Invariant 9 verified: never calls setup_signing on secondary). AZ-395 AP MAVLink2 signing: ephemeral per-flight 32-byte key from secrets.token_bytes; pymavlink setup_signing handshake at open(); in-place bytearray zeroisation on close(); mid-flight signing-failure detection (ERROR log + WARNING STATUSTEXT + no raise; threshold configurable). Key never logged / persisted / serialised (regex-scanned by AC-4/AC-5). BUILD_DEV_STATIC_KEY=ON enables repeatable static-key dev path; rejected at open() when the build flag is absent. Shared: EstimatorOutput.smoothed (default False) added for the Invariant 6 gate at the C8 boundary; FcConfig extended with dev_static_signing_key + signing_failure_threshold (additive defaults; cross-field validation in __post_init__). Tests: 33 new AC tests (11 + 11 + 11) covering all 30 ACs; full suite 476 passing / 2 skipped / 0 failing (was 443). Contract surfaces unchanged at fc_adapter_protocol v1.0.0 and composition_root v1.2.0. Co-authored-by: Cursor <cursoragent@cursor.com>
6.8 KiB
C8 Msp2InavAdapter — outbound MSP2_SENSOR_GPS
Task: AZ-394_c8_inav_outbound
Name: C8 Msp2InavAdapter outbound — MSP2_SENSOR_GPS 5 Hz (D-C8-8 = (b))
Description: Implement the Msp2InavAdapter.emit_external_position(EstimatorOutput) body: encode EstimatorOutput into an MSP2 MSP2_SENSOR_GPS frame (lat/lon/alt × 1e7 in int32 per MSP2 convention; hPosAccuracy in mm from the injected CovarianceProjector.to_inav_h_pos_accuracy_mm; ground-speed/heading from C5 velocity sub-vector if present); write to the iNav UART via YAMSPy + INAV-Toolkit. Side-channel: emit STATUSTEXT (severity=INFO, "src=") on source_label transition (rate-limited) over the SECONDARY MAVLink telemetry channel (iNav supports MAVLink for telemetry but not for primary positioning per RESTRICT-COMM-2). Body of emit_status_text(msg, severity) writes to the secondary MAVLink channel. open(...) rejects any non-None signing_key per Invariant 2 (RESTRICT-COMM-2). request_source_set_switch raises SourceSetSwitchNotSupportedError (iNav has no equivalent). Smoothed-output guard (Invariant 6). SPD-violation propagates from projector (per Invariant 4). Single-writer-thread invariant (Invariant 8). Sequence-number management for MSP2 frames maintained internally.
Complexity: 3 points
Dependencies: AZ-390 (Protocol + DTOs + errors), AZ-392 (CovarianceProjector helper), AZ-279 (WgsConverter), AZ-273 (FDR), AZ-263, AZ-269, AZ-266, AZ-272 (FDR record schema)
Component: c8_fc_adapter (epic AZ-261 / E-C8)
Tracker: AZ-394
Epic: AZ-261 (E-C8)
Document Dependencies
_docs/02_document/contracts/c8_fc_adapter/fc_adapter_protocol.md— Invariants 2, 3, 4, 6, 8, 9._docs/02_document/components/10_c8_fc_adapter/description.md— § 3 External API (MSP2), § 5 implementation details._docs/02_document/architecture.md— § 5 RESTRICT-COMM-2 (iNav signing-asymmetry).
Problem
Without this task, iNav-targeted flights cannot run: no MSP2_SENSOR_GPS is emitted to the iNav EKF; the system is AP-only. RESTRICT-COMM-2 documents iNav as a supported FC for the program; this task implements the wire surface.
Outcome
src/gps_denied_onboard/components/c8_fc_adapter/msp2_inav_adapter.py—Msp2InavAdapterclass implementingFcAdapter.- Constructor:
__init__(self, config, wgs_converter, covariance_projector, fdr_client, clock). - Body of
emit_external_position— encode + emitMSP2_SENSOR_GPSvia YAMSPy. - Body of
emit_status_text— emit STATUSTEXT on the secondary MAVLink channel. - Body of
open(...)— open MSP2 UART; raiseFcAdapterConfigErroronsigning_key != None(RESTRICT-COMM-2). - Body of
close()— close UART connection. request_source_set_switchraisesSourceSetSwitchNotSupportedError.- Internal sequence-number counter for MSP2 frames.
- INFO log on first
MSP2_SENSOR_GPSemit:kind="c8.inav.first_emit". - DEBUG log per emit:
kind="c8.inav.emit"with{frame_seq, h_pos_accuracy_mm, source_label}.
Scope
Included
Msp2InavAdapterclass implementingFcAdapter.MSP2_SENSOR_GPSencoding fromEstimatorOutput.- STATUSTEXT side-channel via secondary MAVLink (transitions only, rate-limited).
emit_status_textbody.open/closebody.- Signing-key rejection (Invariant 2).
request_source_set_switchnot-supported.- iNav signing-asymmetry assertion (Invariant 9): NEVER set the MAVLink2 signed-flag on the side-channel.
- Unit tests: encoding fidelity (decode via INAV-Toolkit reference), signing-key rejection, signing-asymmetry assertion (capture wire bytes, no signed-flag), source-set-switch unsupported, smoothed-output rejected.
Excluded
- AP outbound — owned by AP task.
- GCS adapter — owned by GCS task.
- Inbound subscription — owned by inbound task.
- C8-IT/PT/ST tests — deferred to E-BBT.
Acceptance Criteria
AC-1: MSP2_SENSOR_GPS field fidelity — emit a known EstimatorOutput; decode the wire bytes via INAV-Toolkit reference decoder; assert lat/lon/alt match WgsConverter output × 1e7; hPosAccuracy matches CovarianceProjector.to_inav_h_pos_accuracy_mm(cov_6x6).
AC-2: MSP2_SENSOR_GPS every frame — drive 100 frames; assert 100 MSP2 frames on the wire with monotonically incrementing sequence numbers.
AC-3: STATUSTEXT secondary channel transition — drive 100 frames with source_label toggling every 10 frames; assert exactly 10 STATUSTEXT on the secondary MAVLink channel; never on the primary MSP2 channel.
AC-4: Signing key rejection (Invariant 2) — open(port, signing_key=b"...") → FcAdapterConfigError("iNav does not support MAVLink signing per RESTRICT-COMM-2").
AC-5: Signing-asymmetry assertion (Invariant 9) — capture every byte the iNav adapter writes to the secondary MAVLink channel for 60 s; assert no frame has the MAVLink2 signed-flag set. (This is the unit-level form of C8-IT-08.)
AC-6: Source-set-switch not-supported — request_source_set_switch() → SourceSetSwitchNotSupportedError("iNav: no MAV_CMD_SET_EKF_SOURCE_SET equivalent").
AC-7: Smoothed output rejected — output.smoothed=True → FcEmitError (parallel to AC-5 of the AP task).
AC-8: Non-SPD covariance rejected — propagated from projector → FcEmitError.
AC-9: Single-writer thread — second-thread emit raises RuntimeError.
AC-10: First emit logged once — kind="c8.inav.first_emit" INFO log exactly once per open(...) lifetime.
Non-Functional Requirements
emit_external_positionp95 ≤ 5 ms (C8-PT-01 budget; iNav side mirrors AP side).
Constraints
- YAMSPy + INAV-Toolkit at the project's pinned versions.
- Single-writer thread enforced.
- Secondary MAVLink telemetry channel MUST never set the MAVLink2 signed-flag (Invariant 9).
- MSP2 wire format requires explicit sequence numbers — counter persisted in adapter instance state for the flight lifetime.
Risks & Mitigation
- Risk: YAMSPy + INAV-Toolkit cross-version drift — Mitigation: pinned versions; integration test against the targeted iNav firmware in IT-3 (separate SITL).
- Risk: int32 lat/lon overflow at extreme values — Mitigation: WgsConverter clamps to ±90° latitude / ±180° longitude before scaling; documented.
- Risk: Sequence number wrap-around mid-flight — Mitigation: sequence counter is
uint8per MSP2; modular increment is the wire convention; no special handling needed.
Runtime Completeness
- Named capability: iNav outbound external-position emission.
- Production code: real YAMSPy encode + send; real
CovarianceProjectorusage. - Unacceptable substitutes: a
MAVLink_GPS_INPUT_sendagainst iNav (wrong wire format; defeats AC-1).
Contract
Implements _docs/02_document/contracts/c8_fc_adapter/fc_adapter_protocol.md — emit_external_position, emit_status_text for iNav; Invariants 2, 3, 4, 6, 8, 9.