mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 12:31:12 +00:00
1e0be08e8a
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>
7.9 KiB
7.9 KiB
Batch 10 — Cycle 1 Implementation Report
Batch: 10 of N Tasks landed: AZ-393 (C8 AP outbound) + AZ-394 (C8 iNav outbound) + AZ-395 (C8 AP MAVLink 2.0 per-flight signing) Cycle: 1 Date: 2026-05-11
Scope
| Task | Component | Purpose |
|---|---|---|
| AZ-393 | C8 FC adapter (AP outbound) | PymavlinkArdupilotAdapter.emit_external_position body: GPS_INPUT encoder via pymavlink.mav.gps_input_send; per-frame NAMED_VALUE_FLOAT(name="src_lbl") provenance side-channel; transition-only STATUSTEXT with 1 Hz per-severity hard cap. Single-writer thread invariant, smoothed-output guard, SPD-violation propagation. |
| AZ-394 | C8 FC adapter (iNav outbound) | Msp2InavAdapter — hand-encoded MSP2_SENSOR_GPS (code 0x1F03, 52-byte LE payload) over YAMSPy's send_RAW_msg; transition-only STATUSTEXT on the unsigned secondary MAVLink channel. Signing-key rejection (Invariant 2), source-set-switch unsupported, signing-asymmetry assertion (Invariant 9). |
| AZ-395 | C8 FC adapter (AP signing) | Per-flight ephemeral signing key via secrets.token_bytes(32) + setup_signing handshake; BUILD_DEV_STATIC_KEY=ON enables repeatable dev path from FcConfig.dev_static_signing_key; mid-flight signing-failure detection (no-raise + ERROR log + WARNING STATUSTEXT); in-place key zeroisation in bytearray on close(); FDR rotation event with NO key bytes. |
| Shared | _types/pose.py |
Added EstimatorOutput.smoothed: bool = False to enable Invariant 6 gate at the C8 boundary; C5 sets to True on smoothed-history output (forward action). |
Files added / modified
Added (prod)
src/gps_denied_onboard/components/c8_fc_adapter/_outbound_provenance.py— canonicalSOURCE_LABEL_TO_FLOATtable +source_label_to_float(...)+StatusTextTransitionRateLimiter.src/gps_denied_onboard/components/c8_fc_adapter/pymavlink_ardupilot_adapter.py—PymavlinkArdupilotAdapter(full outbound + signing path).src/gps_denied_onboard/components/c8_fc_adapter/_msp2_sensor_gps_encoder.py—MSP2_SENSOR_GPSwire encoder/decoder (52-byte LE payload).src/gps_denied_onboard/components/c8_fc_adapter/msp2_inav_adapter.py—Msp2InavAdapter(MSP2 primary + unsigned MAVLink secondary).
Added (tests)
tests/unit/c8_fc_adapter/test_az393_ardupilot_outbound.py— 11 AC tests (10 ACs + adapter-fixture).tests/unit/c8_fc_adapter/test_az394_inav_outbound.py— 11 AC tests (10 ACs + iNav config rejection cross-check).tests/unit/c8_fc_adapter/test_az395_mavlink_signing.py— 11 AC tests covering ephemeral keys, FDR no-leak, log no-leak, mid-flight no-raise, zeroisation, BUILD_DEV_STATIC_KEY path, severity mapping, AZ-393 placeholder tightening.
Modified
src/gps_denied_onboard/_types/pose.py— addedEstimatorOutput.smoothed: bool = False(additive default).src/gps_denied_onboard/config/schema.py— extendedFcConfigwithdev_static_signing_key: str = ""+signing_failure_threshold: int = 3; allow"dev_static"as a third validsigning_key_source; cross-field validation(adapter='inav', signing_key_source!='none') → reject;(signing_key_source='dev_static', dev_static_signing_key=='') → reject;signing_failure_threshold < 1 → reject.src/gps_denied_onboard/config/loader.py— wired the two new fields intoENV_KEY_MAP(FC_DEV_STATIC_SIGNING_KEY,FC_SIGNING_FAILURE_THRESHOLD) and_FIELD_COERCIONS.
Contract changes
_docs/02_document/contracts/c8_fc_adapter/fc_adapter_protocol.md— unchanged at v1.0.0. Surface implemented in this batch was declared in v1.0.0 / AZ-390 (batch 8)._docs/02_document/contracts/shared_config/composition_root_protocol.md— unchanged at v1.2.0. The two newFcConfigfields are additive defaults; no behavioural change for existing callers.
Test counts
| Metric | Before | After | Delta |
|---|---|---|---|
| Tests passing | 443 | 476 | +33 |
| Tests skipped | 2 | 2 | 0 |
| Tests failing | 0 | 0 | 0 |
Architectural notes
- Lazy import of pymavlink / yamspy: both adapter classes import their heavy wire dependencies only at
_connect/_connect_msptime. Tests inject aconnect_factory/msp_connect_factory, so the AC suite runs without the C-extension transports. Production builds load pymavlink (AP) or yamspy (iNav) only when the matchingBUILD_FC_*flag links the respective adapter into the binary — consistent with ADR-002. - WGS84 source-of-truth: both adapters require the composition root to pre-attach the WGS84 fix to
EstimatorOutput.extras["wgs84"](aLatLonAlt). If the enricher is missing the key,_extract_wgs84raisesFcEmitErrorrather than guessing — a missing enricher is a composition bug, not an emit-time degraded mode. TheWgsConverterdependency is still injected so a future refactor can move the enrichment inside the adapter without a public-API change. - Single-writer thread: enforced inside
emit_external_positionandemit_status_textfor both adapters via_enforce_single_writer. The first-emit thread becomes the binding for the lifetime ofopen(...); a second thread raisesRuntimeError. Combined with the composition-root-levelbind_outbound_emit_threadfrom batch 8, this gives two-tier defence-in-depth against the multi-writer race. - Signing-failure poll:
_poll_signing_failure_counterruns once per emit. It readsconnection.mav.signing.sig_count(provided by pymavlink), compares against_signing_failure_threshold(default 3, configurable viaFcConfig.signing_failure_threshold), and logs + sends WARNINGSTATUSTEXTonly on the transition past the threshold (tracked by_signing_failure_logged_at_count). It NEVER raises — the FC ignores unsigned messages and AC-5.2 fallback (AZ-388) takes over downstream. - Key zeroisation: the signing key is stored as a
bytearrayso we can overwrite it in place insideclose()viafor i in range(len(buf)): buf[i] = 0.pymavlink.setup_signingreceivesbytes(key)(a copy), so our buffer is the authoritative one we control. AC-7 verifies the buffer is zero post-close by capturing a direct reference to the bytearray before close. - MSP2 wire format:
MSP2_SENSOR_GPS(code0x1F03) is iNav-specific and absent from YAMSPy'sMSPCodestable. We hand-roll the 52-byte LE payload (struct.pack/unpack) and ship through YAMSPy's lower-levelsend_RAW_msg(code, data). The unit test exercises the round-trip viadecode_msp2_sensor_gps. An IT-tier test against real iNav firmware will exercise the on-target decode.
Dependencies introduced
- None.
pymavlink>=2.4andyamspy>=0.3.3,<0.4were already pinned inpyproject.toml(batch 9). This batch consumes those dependencies via the adapter shells.
Known forward-actions
- AZ-389 (
smoothedflag at C5 source) — C5's smoothed-history output (AZ-387) must setEstimatorOutput.smoothed=Trueso the Invariant 6 gate at C8 fires correctly. The default-False field landed in this batch; C5 wiring lands in the C5 batch. Msp2InavAdapter.current_flight_state()currently returns aFlightState.INITdefault because the iNav inbound decoder (AZ-391) is not yet composed INTO the adapter; the composition root readsdecoder.state_ringdirectly. Wiring the inbound decoder INTO the adapter shell (parallel to the AP adapter's_inboundfield) is the next composition refinement — does not affect the AC surface but tightens the producer/consumer model.StatusTextTransitionRateLimiter._min_interval_s(1 s default) is a constructor default. Promotion toFcConfig.statustext_transition_min_interval_sis a forward-action contract bump.- Source-set switch (AZ-396) replaces the
NotImplementedError("Owned by source-set task; install AZ-396 to enable")raise; AP adapter'srequest_source_set_switchis the integration point. - C8-ST-01 SITL signing handshake test — gated by IT-3 / ADR-008. The wire surface delivered here is the input to that gate.