[AZ-396] [AZ-397] Batch 11: C8 source-set switch + QGC telemetry adapter

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>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 05:06:56 +03:00
parent 1e0be08e8a
commit 8a9cf88a46
16 changed files with 1608 additions and 12 deletions
@@ -229,7 +229,7 @@ def test_ac2_fc_telemetry_frame_dto_frozen() -> None:
def test_ac3_fc_kind_has_two_members() -> None:
# Assert
assert {m.name for m in FcKind} == {"ARDUPILOT_PLANE", "INAV"}
assert {m.name for m in FcKind} == {"ARDUPILOT_PLANE", "INAV", "GCS_QGC"}
def test_ac3_flight_state_has_five_members() -> None:
@@ -505,9 +505,11 @@ def test_signing_key_source_unknown_value_rejected() -> None:
def test_gcs_summary_rate_out_of_range_rejected() -> None:
# AZ-397 widened the valid range to [0.5, 5.0] (AC-10); the boundary
# cases below now fall OUTSIDE the new range.
# Act + Assert — too high
with pytest.raises(ConfigError, match=r"summary_rate_hz"):
GcsConfig(summary_rate_hz=5.0)
GcsConfig(summary_rate_hz=5.1)
# Too low
with pytest.raises(ConfigError, match=r"summary_rate_hz"):
GcsConfig(summary_rate_hz=0.5)
GcsConfig(summary_rate_hz=0.4)