mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 12:11:13 +00:00
[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>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
# 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.py` — `TelemetryRing[T]` bounded drop-oldest ring with first-overflow INFO + monotonic `dropped_count`.
|
||||
- `src/gps_denied_onboard/components/c8_fc_adapter/_subscription.py` — `SubscriptionBus` + `SubscriptionHandle` (concrete `Subscription` Protocol).
|
||||
- `src/gps_denied_onboard/components/c8_fc_adapter/_inbound_mavlink.py` — `PymavlinkInboundDecoder` (ArduPilot path) + `MAVLinkSource` Protocol.
|
||||
- `src/gps_denied_onboard/components/c8_fc_adapter/_inbound_msp2.py` — `Msp2InavInboundDecoder` (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.md` — **unchanged**. 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.
|
||||
Reference in New Issue
Block a user