mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-23 02:21:12 +00:00
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:
@@ -0,0 +1,104 @@
|
||||
# C8 PymavlinkArdupilotAdapter — outbound GPS_INPUT + STATUSTEXT + NAMED_VALUE_FLOAT
|
||||
|
||||
**Task**: AZ-393_c8_ardupilot_outbound
|
||||
**Name**: C8 `PymavlinkArdupilotAdapter` outbound — `GPS_INPUT` 5 Hz + provenance side-channel
|
||||
**Description**: Implement the `PymavlinkArdupilotAdapter.emit_external_position(EstimatorOutput)` body: encode `EstimatorOutput` into a MAVLink 2.0 `GPS_INPUT` frame (lat/lon/alt from WGS84 conversion via injected `WgsConverter`; `horiz_accuracy` from the injected `CovarianceProjector.to_ardupilot_horiz_accuracy_m`; `vel_n/vel_e/vel_d` from the velocity sub-vector if present in the C5 estimate); write to the pymavlink connection via `mav.gps_input_send(...)`. Side-channel: emit `NAMED_VALUE_FLOAT` with `name="src_lbl"` carrying the `EstimatorOutput.source_label` enum value (encoded as float per the documented enum-to-float mapping); also emit `STATUSTEXT(severity=INFO, "src=<label>")` once per source-label transition (rate-limited — not on every frame). Body of `emit_status_text(msg, severity)` writes pymavlink `STATUSTEXT`. `request_source_set_switch` raises `NotImplementedError("source-set switch owned by AZ-396 task")` — replaced when that task lands. Smoothed-output guard (Invariant 6): `output.smoothed == True` → `FcEmitError`. SPD-violation propagates from `CovarianceProjector` as `FcEmitError`; logged + dropped + continue per § 5 error-handling. Single-writer-thread invariant enforced (Invariant 8). NO signing logic — that's a separate task; `open(...)` here accepts `signing_key=None` for now (signing task replaces this).
|
||||
**Complexity**: 5 points
|
||||
**Dependencies**: AZ-390 (Protocol + DTOs), 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-393
|
||||
**Epic**: AZ-261 (E-C8)
|
||||
|
||||
### Document Dependencies
|
||||
|
||||
- `_docs/02_document/contracts/c8_fc_adapter/fc_adapter_protocol.md` — Invariants 3, 4, 5, 6, 8.
|
||||
- `_docs/02_document/components/10_c8_fc_adapter/description.md` — § 3 External API (MAVLink 2.0), § 5 implementation details.
|
||||
- `_docs/02_document/architecture.md` — § 5 External Integrations.
|
||||
|
||||
## Problem
|
||||
|
||||
Without this task, AP-targeted flights cannot run: there is no GPS_INPUT being emitted to ArduPilot's EKF, and the provenance side-channel (`source_label` via NAMED_VALUE_FLOAT + STATUSTEXT) is not feeding GCS observability. The C5 emit driver has no sink.
|
||||
|
||||
## Outcome
|
||||
|
||||
- `src/gps_denied_onboard/components/c8_fc_adapter/pymavlink_ardupilot_adapter.py` — `PymavlinkArdupilotAdapter` class implementing `FcAdapter`.
|
||||
- Constructor: `__init__(self, config, wgs_converter, covariance_projector, fdr_client, clock)`.
|
||||
- Body of `emit_external_position` — encode + emit `GPS_INPUT`; emit `NAMED_VALUE_FLOAT(name="src_lbl")` every frame; emit `STATUSTEXT` on `source_label` transition (rate-limited).
|
||||
- Body of `emit_status_text` — pymavlink `statustext_send`.
|
||||
- Body of `open(...)` — open pymavlink connection on the configured port with `mavlink_version=2`; reject `signing_key=None` only when the signing task has landed (this task accepts None and emits unsigned frames; a follow-up integration step in the signing task converts this to require non-None).
|
||||
- Body of `close()` — flush pending writes, close connection.
|
||||
- `request_source_set_switch` raises `NotImplementedError("Owned by source-set task; install AZ-396 to enable")`.
|
||||
- `current_flight_state` already implemented by AZ-391 (inbound task) — this task just doesn't override it.
|
||||
- INFO log on first successful `GPS_INPUT` emit: `kind="c8.ap.first_emit"`.
|
||||
- DEBUG log per emit: `kind="c8.ap.emit"` with `{frame_seq, horiz_accuracy_m, source_label}`.
|
||||
- ERROR log on `FcEmitError`: `kind="c8.ap.emit_failed"` with reason.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- `PymavlinkArdupilotAdapter` class implementing `FcAdapter`.
|
||||
- `GPS_INPUT` encoding from `EstimatorOutput`.
|
||||
- `NAMED_VALUE_FLOAT` source-label side-channel (every frame).
|
||||
- `STATUSTEXT` source-label transition (rate-limited).
|
||||
- `emit_status_text` body.
|
||||
- `open` / `close` body (without signing — placeholder).
|
||||
- Unit tests: encoding fidelity (decode the bytes back via pymavlink + assert fields); rate-limited STATUSTEXT (no flood); smoothed-output rejected; non-SPD covariance rejected; single-writer-thread enforcement.
|
||||
|
||||
### Excluded
|
||||
- MAVLink 2.0 signing handshake — owned by signing task.
|
||||
- D-C8-2 source-set switch — owned by source-set task.
|
||||
- iNav adapter — owned by iNav outbound task.
|
||||
- GCS adapter — owned by GCS task.
|
||||
- Inbound subscribe (already implemented in `_inbound_mavlink.py` via AZ-391 — this class composes it).
|
||||
- `current_flight_state` body — already implemented in inbound task.
|
||||
- C8-IT/PT/ST tests — deferred to E-BBT.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
**AC-1: GPS_INPUT field fidelity** — emit a known `EstimatorOutput`; decode the wire bytes via pymavlink; assert lat/lon/alt match WgsConverter output; `horiz_accuracy` matches `CovarianceProjector.to_ardupilot_horiz_accuracy_m(cov_6x6)` to within 1e-3 m.
|
||||
|
||||
**AC-2: GPS_INPUT every frame** — drive 100 frames; assert exactly 100 `GPS_INPUT` messages on the wire.
|
||||
|
||||
**AC-3: NAMED_VALUE_FLOAT every frame** — drive 100 frames; assert 100 `NAMED_VALUE_FLOAT` messages with `name="src_lbl"`; values match the source-label-to-float mapping.
|
||||
|
||||
**AC-4: STATUSTEXT rate-limited on transition** — drive 100 frames with `source_label` toggling every 10 frames; assert exactly 10 `STATUSTEXT` messages (one per transition); within-state frames emit zero STATUSTEXT.
|
||||
|
||||
**AC-5: Smoothed output rejected** — emit an `EstimatorOutput` with `smoothed=True`; assert `FcEmitError` raised + `kind="c8.ap.emit_failed"` logged + no GPS_INPUT on the wire.
|
||||
|
||||
**AC-6: Non-SPD covariance rejected** — emit an `EstimatorOutput` with non-SPD `covariance_6x6`; assert `FcEmitError` raised (propagated from `CovarianceProjector`) + log + no GPS_INPUT.
|
||||
|
||||
**AC-7: Single-writer thread** — call `emit_external_position` from a second thread; assert `RuntimeError`.
|
||||
|
||||
**AC-8: Open without signing key (placeholder)** — `open(port, signing_key=None)` succeeds in this task's context; signing task tightens this to reject None.
|
||||
|
||||
**AC-9: source-set switch raises NotImplementedError** — `request_source_set_switch()` raises with the message `"Owned by source-set task; install AZ-396 to enable"`.
|
||||
|
||||
**AC-10: First emit logged once** — the `kind="c8.ap.first_emit"` INFO log fires exactly once per `open(...)` lifetime.
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
- `emit_external_position` p95 ≤ 5 ms (C8-PT-01 budget).
|
||||
- p99 ≤ 8 ms (defensive headroom).
|
||||
|
||||
## Constraints
|
||||
|
||||
- pymavlink bundled unmodified per D-C8-3.
|
||||
- Single-writer thread enforced.
|
||||
- The source-label-to-float encoding mapping MUST be documented inline + match the operator-side decoder (E-C12).
|
||||
- STATUSTEXT rate-limit: at most 1 per source-label transition AND at most 1 per second per severity (defensive against pathological transition spam).
|
||||
|
||||
## Risks & Mitigation
|
||||
|
||||
- **Risk: pymavlink GPS_INPUT field semantics differ across ArduPilot versions** — *Mitigation*: pinned pymavlink version + integration test against the targeted ArduPilot Plane firmware (verified in IT-3 SITL).
|
||||
- **Risk: STATUSTEXT flood under rapid source-label transitions** — *Mitigation*: AC-4 verifies one-per-transition; secondary 1 Hz cap as a safety net.
|
||||
- **Risk: NAMED_VALUE_FLOAT name truncation** — *Mitigation*: `"src_lbl"` is 7 chars (within MAVLink 10-char limit).
|
||||
|
||||
## Runtime Completeness
|
||||
|
||||
- **Named capability**: AP outbound external-position emission with provenance side-channel.
|
||||
- **Production code**: real pymavlink encode + send; real WgsConverter + CovarianceProjector usage; real STATUSTEXT rate-limit.
|
||||
- **Unacceptable substitutes**: a fake `GPS_INPUT` builder that doesn't go through pymavlink (defeats AC-1 wire-level fidelity).
|
||||
|
||||
## Contract
|
||||
|
||||
Implements `_docs/02_document/contracts/c8_fc_adapter/fc_adapter_protocol.md` — `emit_external_position`, `emit_status_text` for AP; Invariants 3, 4, 5, 6, 8.
|
||||
Reference in New Issue
Block a user