mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 10:51:14 +00:00
8a9cf88a46
AZ-396: PymavlinkArdupilotAdapter.request_source_set_switch body sends MAV_CMD_SET_EKF_SOURCE_SET, awaits COMMAND_ACK with timeout, enforces Invariant 11 idempotence (1s rate-limit + skip-after-success). Adds runtime_root.SpoofRecoverySink to bridge C5 spoof-promotion-recovered signal to the C8 outbound thread via a bounded dispatch queue. FcConfig gains spoof_recovery_source_set + source_set_switch_timeout_ms. AZ-397: QgcTelemetryAdapter implements GcsAdapter strategy: MAVLink 2.0 to QGC, emit_summary downsamples 5Hz to configurable summary_rate_hz [0.5, 5.0] via integer modulo, emit_status_text mirrors to GCS link, subscribe_operator_commands translates COMMAND_LONG / PARAM_REQUEST_* / REQUEST_DATA_STREAM / MISSION_* / SET_MODE into OperatorCommand DTOs and audits each receipt to FDR. FcKind.GCS_QGC added for PortConfig. Tests: 25 new (12 AZ-396 + 13 AZ-397); full suite 501 passing, 2 skipped. Contracts unchanged (additive FcConfig fields, range relaxation on GcsConfig.summary_rate_hz, additive FcKind enum value). Co-authored-by: Cursor <cursoragent@cursor.com>
6.6 KiB
6.6 KiB
Batch 11 — Cycle 1 Implementation Report
Batch: 11 of N Tasks landed: AZ-396 (D-C8-2 source-set switch + spoof-recovery sink) + AZ-397 (QgcTelemetryAdapter) Cycle: 1 Date: 2026-05-11
Scope
| Task | Component | Purpose |
|---|---|---|
| AZ-396 | C8 FC adapter (AP) + runtime root | PymavlinkArdupilotAdapter.request_source_set_switch body — sends MAV_CMD_SET_EKF_SOURCE_SET (ardupilotmega cmd 42007) with param1 = config.fc.spoof_recovery_source_set, waits for COMMAND_ACK up to config.fc.source_set_switch_timeout_ms, idempotence per Invariant 11 (rate-limit within 1 s + skip after success). runtime_root.SpoofRecoverySink provides a bounded-queue thread sink that consumes C5's spoof-promotion-recovered signal (AZ-385 publisher side; future task) and dispatches the source-set switch from a single dedicated thread (Invariant 8 preserved). |
| AZ-397 | C8 GCS adapter | QgcTelemetryAdapter — concrete GcsAdapter strategy. Open/close a MAVLink 2.0 channel to QGC on the configured UART; emit_summary downsamples 5 Hz → summary_rate_hz via modulo arithmetic and emits GLOBAL_POSITION_INT + NAMED_VALUE_FLOAT("horiz_m"); emit_status_text mirrors a STATUSTEXT to the GCS link; subscribe_operator_commands registers a pymavlink message-hook that translates COMMAND_LONG / PARAM_REQUEST_* / REQUEST_DATA_STREAM / MISSION_* / SET_MODE into OperatorCommand DTOs and writes every receipt to FDR (kind="c8.gcs.operator_command") as the audit trail. |
Files added / modified
Added (prod)
src/gps_denied_onboard/runtime_root/spoof_recovery_sink.py—SpoofRecoveryPublisherProtocol +SpoofRecoverySink(dispatch thread, bounded queue, error isolation).src/gps_denied_onboard/components/c8_fc_adapter/mavlink_gcs_adapter.py—QgcTelemetryAdapter+_compute_downsample_modulohelper.
Added (tests)
tests/unit/c8_fc_adapter/test_az396_source_set_switch.py— 12 AC tests (10 ACs + 2 sink-isolation cases).tests/unit/c8_fc_adapter/test_az397_qgc_telemetry.py— 13 AC tests (10 ACs + parametrised rate cases).
Modified (prod)
src/gps_denied_onboard/_types/fc.py— addedFcKind.GCS_QGCenum value (additive; letsPortConfig.fc_kinddiscriminate the GCS link variant alongside AP/iNav).src/gps_denied_onboard/components/c8_fc_adapter/pymavlink_ardupilot_adapter.py—request_source_set_switchbody (AZ-396) replaces the NotImplementedError placeholder; new helpers_wait_for_command_ackand_handle_source_set_switch_failure; ctor fields_last_switch_attempt_ns+_last_switch_succeeded.src/gps_denied_onboard/config/schema.py—FcConfigextended withspoof_recovery_source_set: int = 1+source_set_switch_timeout_ms: int = 1500(defaults align with AZ-396 spec);GcsConfig.summary_rate_hzvalid range widened from[1.0, 2.0]to[0.5, 5.0]per AZ-397 AC-10.src/gps_denied_onboard/config/loader.py—ENV_KEY_MAPextended withFC_SPOOF_RECOVERY_SOURCE_SET+FC_SOURCE_SET_SWITCH_TIMEOUT_MS;_FIELD_COERCIONSextended with the two new int fields.src/gps_denied_onboard/runtime_root/__init__.py— re-exportsSpoofRecoveryPublisher+SpoofRecoverySink.
Modified (tests)
tests/unit/c8_fc_adapter/test_az390_adapter_protocol.py— updatedtest_ac3_fc_kind_has_two_membersto reflect the additiveGCS_QGCenum value; updatedtest_gcs_summary_rate_out_of_range_rejectedboundaries (5.1/0.4) to match the new valid range.tests/unit/c8_fc_adapter/test_az393_ardupilot_outbound.py— AC-9 placeholder check (NotImplementedError) replaced by a callability check; the real source-set-switch behaviour now lives under AZ-396 tests.
Contract changes
_docs/02_document/contracts/c8_fc_adapter/fc_adapter_protocol.md— unchanged at v1.0.0._docs/02_document/contracts/shared_config/composition_root_protocol.md— unchanged at v1.2.0. AdditiveFcConfigfields + valid-range relaxation forGcsConfig.summary_rate_hz(existing configs remain valid).
Test counts
| Metric | Before | After | Delta |
|---|---|---|---|
| Tests passing | 476 | 501 | +25 |
| Tests skipped | 2 | 2 | 0 |
| Tests failing | 0 | 0 | 0 |
Architectural notes
- Source-set switch threading: the AP adapter's
request_source_set_switchenforces single-writer via the same_enforce_single_writerused byemit_external_position. TheSpoofRecoverySinkdispatch thread is the documented single writer; the C5 publisher thread callssink.publish()which only enqueues. The bounded queue guarantees the publisher never blocks on UART. _wait_for_command_ackfilters bytype="COMMAND_ACK"— necessary because the inbound MAVLink decoder thread (AZ-391) also callsrecv_matchon the same connection. Real pymavlink routes by type internally; the unit-test stub explicitly mirrors that behaviour so the test-vs-production gap stays small.- GCS adapter downsampling:
_compute_downsample_modulo = round(_COMPOSE_ROOT_INVOKE_HZ / summary_rate_hz). Tests pin the exact integer modulo so any future change to_COMPOSE_ROOT_INVOKE_HZshows up as a deliberate AC update. The modulo is computed once at construction; updates tosummary_rate_hzrequire a reopen. - Operator command dispatch: pymavlink's message-hook list is the canonical inbound-routing seam.
_ensure_operator_handler_attachedis idempotent — multiplesubscribe_operator_commandscalls share a single hook; subscriber fan-out happens via theSubscriptionBus(C8 inbound pattern, AZ-391). Subscriber crashes are isolated by the bus. FcKind.GCS_QGC: pragmatic compromise to keepPortConfigsingle-typed across FC and GCS variants. A futureLinkKindsuperclass refactor would split the two; documented in the AZ-390 follow-up.
Dependencies introduced
- None.
Known forward-actions
- AZ-385 (C5 spoof-promotion gate) publishes the recovery signal the
SpoofRecoverySinkconsumes. The composition root wires the two with one call:publisher.subscribe_spoof_promotion_recovered(sink.publish). Until AZ-385 lands, the sink is constructed but inactive. _SWITCH_RATE_LIMIT_Sand the GCS NAMED_VALUE_FLOAT name ("horiz_m") — both module-level constants; promotion to config + contract bump deferred.global_position_intrelative-altitude placeholder — currently mirrors absolute altitude; the runtime root must surface the launch reference frame before the relative field is meaningful.- C8-IT-07 SITL end-to-end (3 s spoofing-promotion latency) — deferred to E-BBT; this batch delivers the wire surface for that gate.