Files
Oleksandr Bezdieniezhnykh a61d2d3f4b [AZ-391] C8 inbound: MAVLink + MSP2 decoders + rings + bus + warm-start
Adds the C8 inbound producer side:
- TelemetryRing[T]: bounded drop-oldest ring; first-overflow INFO log
  + monotonic dropped_count.
- SubscriptionBus + SubscriptionHandle: synchronous fan-out, lock-
  released-before-callback to avoid deadlock; subscriber crash caught
  + DEBUG-logged so one bad subscriber cannot kill the decode loop.
- PymavlinkInboundDecoder: pymavlink-based AP decoder for RAW_IMU,
  SCALED_IMU2, ATTITUDE, GPS_RAW_INT, GPS2_RAW, HEARTBEAT, STATUSTEXT.
  Out-of-order drop (Invariant 7) per-kind WARN. STATUSTEXT spoofing
  sentinel promotes subsequent GPS to GpsStatus.SPOOFED within 5 s.
  AC-5.1 warm-start hint cached on first 3D+ fix; embedded into
  every FlightStateSignal.
- Msp2InavInboundDecoder: YAMSPy-based iNav polling decoder for IMU /
  attitude / GPS / flight-state. signed=False always (RESTRICT-COMM-2);
  GpsStatus.SPOOFED is unreachable on iNav.

Adds yamspy>=0.3.3 + pyserial>=3.5 to pyproject.toml.

Tests: 443 pass / 2 skip / 0 fail (+33 in batch 9).

Contract: no drift on fc_adapter_protocol.md v1.0.0; this batch
implements the inbound producer side without changing signatures.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 04:28:14 +03:00

5.5 KiB

Batch 09 — Cycle 1 Implementation Report

Batch: 9 of N Task landed: AZ-391 (C8 inbound subscription path — IMU / attitude / GPS health / MAV_STATE producer) Cycle: 1 Date: 2026-05-11

Scope

Task Component Purpose
AZ-391 C8 FC adapter (inbound) MAVLink 2.0 (pymavlink) decoder for ArduPilot's 5 inbound message types + iNav MSP2 (yamspy) polling decoder; bounded per-kind telemetry rings with drop-oldest semantics + first-overflow INFO log; multi-subscriber fan-out bus with crash isolation; AC-5.1 warm-start hint surfacing (cached on first 3D+ fix; embedded into every subsequent FlightStateSignal); out-of-order frame detection (Invariant 7) with one WARN per drop; corrupt-frame isolation with DEBUG log + continued decode.

Files added / modified

Added (prod)

  • src/gps_denied_onboard/components/c8_fc_adapter/_telemetry_rings.pyTelemetryRing[T] bounded drop-oldest ring with first-overflow INFO + monotonic dropped_count.
  • src/gps_denied_onboard/components/c8_fc_adapter/_subscription.pySubscriptionBus + SubscriptionHandle (concrete Subscription Protocol).
  • src/gps_denied_onboard/components/c8_fc_adapter/_inbound_mavlink.pyPymavlinkInboundDecoder (ArduPilot path) + MAVLinkSource Protocol.
  • src/gps_denied_onboard/components/c8_fc_adapter/_inbound_msp2.pyMsp2InavInboundDecoder (iNav path) + MspSource Protocol.

Added (tests)

  • tests/unit/c8_fc_adapter/test_az391_inbound_subscription.py — 33 unit tests covering all 10 ACs of AZ-391 + 2 NFR smoke tests.

Modified

  • pyproject.toml — added yamspy>=0.3.3,<0.4 + pyserial>=3.5 (transitive of yamspy).

Contract changes

  • _docs/02_document/contracts/c8_fc_adapter/fc_adapter_protocol.mdunchanged. This batch implements the inbound side of the v1.0.0 surface without altering signatures.

Test counts

Metric Before After Delta
Tests passing 410 443 +33
Tests skipped 2 2 0
Tests failing 0 0 0

Architectural notes

  • Decoder lifecycle: the production driver constructs PymavlinkInboundDecoder(source, bus) (or the iNav equivalent), spawns one thread that calls run_decode_loop (AP) or run_poll_loop (iNav), and never touches the source from any other thread. stop() flips an Event; the next loop iteration exits cleanly.
  • Rings are decoder-owned: each decoder exposes imu_ring, attitude_ring, gps_ring, state_ring as read-only attributes; consumers snapshot() or peek_latest() from any thread.
  • Subscription bus is synchronous-fan-out: callbacks fire on the decoder thread (Invariant 8). The bus snapshots its subscriber list under a single lock, then releases the lock BEFORE invoking callbacks, so a callback that calls back into subscribe / cancel cannot deadlock.
  • Subscriber-crash isolation: every callback invocation is wrapped in try/except; on raise the bus emits one kind="c8.inbound.callback_error" DEBUG record and continues with the next subscriber. A bad subscriber CANNOT kill the decode loop.
  • Warm-start hint (AC-5.1 / AC-8): the AP path caches the first GPS_RAW_INT with fix_type >= 3; the cached LatLonAlt and capture-time are embedded into every subsequent FlightStateSignal emitted via HEARTBEAT decode. The iNav path mirrors this through MSP_RAW_GPS / MSP2_INAV_GPS. current_flight_state() (to be wired in AZ-393 / AZ-394) consumes the latest entry from state_ring.
  • Out-of-order detection (AC-9 / Invariant 7) is per-kind, decode-boundary monotonic_ns() based. A frame whose received_at <= last_received_at[kind] is dropped + a single WARN record (kind="c8.inbound.out_of_order_frame_dropped") is emitted. The drop is NOT silenced; an aggregated-counter approach is documented for AZ-392/393 follow-up.
  • GPS spoofing (AP-only, AC-3) uses an indirect signal: STATUSTEXT messages whose text contains "GPS spoofing" or "GPS jamming" set a per-decoder sentinel; subsequent GPS_RAW_INT decodes within 5 s of the sentinel are promoted to GpsStatus.SPOOFED. The 5 s window is wired in code (not config) for batch-9 scope; promotion to a config knob is forward-action.
  • iNav has no spoofing (AC-5 / RESTRICT-COMM-2): the iNav decoder never produces GpsStatus.SPOOFED; verified by test_ac5_inav_spoofed_status_unreachable.

Dependencies introduced

  • yamspy>=0.3.3,<0.4 — iNav MSP2 protocol library (38 KiB pure-Python wheel; built locally).
  • pyserial>=3.5 — transitive of yamspy; production code uses serial transports for the live MSPy connection.
  • pymavlink>=2.4 — already pinned (AP path); installed by this batch since the test suite now imports it.

All three are runtime dependencies ([project.dependencies]).

Known forward-actions

  1. AZ-393 / AZ-394 will compose the decoders into the concrete PymavlinkArdupilotAdapter / Msp2InavAdapter classes (the AZ-391 producer side is component-shaped; the consumer wiring is the next task).
  2. Spoofing-window timeout (5 s) is wired in code; promotion to FcConfig.spoofing_sentinel_window_s is a forward-action contract bump.
  3. Aggregate-counter INFO log every 60 s (mentioned in the spec's risk mitigation) is NOT implemented — we kept the per-drop WARN only, matching the AC-9 contract. The 60 s aggregate is a forward-action enhancement.
  4. C8-PT-01 sustained 200 Hz IMU NFR is not exercised by this batch's unit tests — that's an integration / PT-tier test. The unit-test NFR budget (1 ms avg per IMU callback) provides a sanity ceiling.