Decompose Step 6 snapshot: 140 task specs + contract docs

Closes out greenfield Step 6 (Decompose) for all 14 components
(C1-C13 + cross-cutting helpers/replay). Covers tasks AZ-266..AZ-446
plus the _dependencies_table.md and component contract documents.

State file updated to greenfield Step 7 (Implement), not_started.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 00:39:48 +03:00
parent 8171fcb29e
commit 880eabcb3f
172 changed files with 22897 additions and 35 deletions
@@ -0,0 +1,101 @@
# 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=<label>") 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``Msp2InavAdapter` 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-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_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 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 `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.md``emit_external_position`, `emit_status_text` for iNav; Invariants 2, 3, 4, 6, 8, 9.