Files
gps-denied-onboard/_docs/02_tasks/done/AZ-394_c8_inav_outbound.md
T
Oleksandr Bezdieniezhnykh 1e0be08e8a [AZ-393] [AZ-394] [AZ-395] C8 outbound chain + AP MAVLink2 signing
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>
2026-05-11 04:47:44 +03:00

6.8 KiB
Raw Blame History

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.pyMsp2InavAdapter class implementing FcAdapter.
  • Constructor: __init__(self, config, wgs_converter, covariance_projector, fdr_client, clock).
  • Body of emit_external_position — encode + emit MSP2_SENSOR_GPS via YAMSPy.
  • Body of emit_status_text — emit STATUSTEXT on the secondary MAVLink channel.
  • Body of open(...) — open MSP2 UART; raise FcAdapterConfigError on signing_key != None (RESTRICT-COMM-2).
  • Body of close() — close UART connection.
  • request_source_set_switch raises SourceSetSwitchNotSupportedError.
  • Internal sequence-number counter for MSP2 frames.
  • INFO log on first MSP2_SENSOR_GPS emit: kind="c8.inav.first_emit".
  • DEBUG log per emit: kind="c8.inav.emit" with {frame_seq, h_pos_accuracy_mm, source_label}.

Scope

Included

  • Msp2InavAdapter class implementing FcAdapter.
  • MSP2_SENSOR_GPS encoding from EstimatorOutput.
  • STATUSTEXT side-channel via secondary MAVLink (transitions only, rate-limited).
  • emit_status_text body.
  • open / close body.
  • Signing-key rejection (Invariant 2).
  • request_source_set_switch not-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-supportedrequest_source_set_switch()SourceSetSwitchNotSupportedError("iNav: no MAV_CMD_SET_EKF_SOURCE_SET equivalent").

AC-7: Smoothed output rejectedoutput.smoothed=TrueFcEmitError (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 oncekind="c8.inav.first_emit" INFO log exactly once per open(...) lifetime.

Non-Functional Requirements

  • emit_external_position p95 ≤ 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 driftMitigation: pinned versions; integration test against the targeted iNav firmware in IT-3 (separate SITL).
  • Risk: int32 lat/lon overflow at extreme valuesMitigation: WgsConverter clamps to ±90° latitude / ±180° longitude before scaling; documented.
  • Risk: Sequence number wrap-around mid-flightMitigation: sequence counter is uint8 per 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 CovarianceProjector usage.
  • Unacceptable substitutes: a MAVLink_GPS_INPUT_send against iNav (wrong wire format; defeats AC-1).

Contract

Implements _docs/02_document/contracts/c8_fc_adapter/fc_adapter_protocol.mdemit_external_position, emit_status_text for iNav; Invariants 2, 3, 4, 6, 8, 9.