[AZ-558] Route C8 outbound encoder bytes through MavlinkTransport seam

All FC adapter outbound MAVLink bytes now go through the AZ-401
MavlinkTransport seam (NoopMavlinkTransport in replay,
SerialMavlinkTransport in live). New helpers in
_outbound_mavlink_payloads.py extract encode/pack/seq-bump so the four
AP _send sites and the iNav statustext _send site become
encode -> pack -> transport.write. TlogReplayFcAdapter emits real
AP-shape MAVLink bytes through the injected NoopMavlinkTransport,
satisfying replay protocol Invariant 5 and unblocking AZ-401 AC-9.

Closes AZ-558. Also unskips AZ-401 AC-9 and AZ-404 AC-4b. Live wire
output remains byte-identical (proven via two-instance MAVLink
byte-equivalence tests). AST scan asserts no .mav.<name>_send( calls
remain in the retrofit set (AP / iNav / tlog adapters).

Out of scope (logged in review): GCS adapter retrofit; airborne live
strategy registration that would activate the SerialMavlinkTransport
factory injection path.

Tests: 2110 passed, 92 environmental skips, 1 unrelated pre-existing
macOS cold-start flake deselected.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-16 05:33:45 +03:00
parent d7e6b0959e
commit 2b19b8b90b
18 changed files with 1106 additions and 90 deletions
+52 -14
View File
@@ -520,23 +520,61 @@ def test_ac8_replay_branch_imports_only_public_apis() -> None:
# ----------------------------------------------------------------------
# AC-9: NoopMavlinkTransport.bytes_written() > 0 — BLOCKED
# AC-9: NoopMavlinkTransport.bytes_written() > 0 (closed by AZ-558)
@pytest.mark.skip(
reason=(
"BLOCKED on AZ-399 design choice: TlogReplayFcAdapter raises "
"FcEmitError on emit_external_position rather than routing the "
"encoder bytes through the MavlinkTransport seam. Closing this "
"gap requires retrofitting AP/iNav/QGC encoder code paths to "
"consume MavlinkTransport — see batch 61 report. NoopMavlinkTransport "
"+ MavlinkTransport Protocol classes are present (covered by "
"test_az400_mavlink_transport.py) but the wiring that makes "
"bytes_written > 0 in replay mode is deferred."
def test_ac9_noop_transport_bytes_written_after_runtime_drive(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""AZ-401 AC-9 / AZ-558 AC-4: replay encoders write through the seam.
Drives 10 ``EstimatorOutput`` ticks through a replay-wired
:class:`TlogReplayFcAdapter` with a :class:`NoopMavlinkTransport`
injected as its outbound seam. After the AZ-558 retrofit the
adapter encodes ``GPS_INPUT`` + ``NAMED_VALUE_FLOAT`` per tick
and writes the packed bytes through the transport — replay
protocol Invariant 5 (encoders run in both modes; only the
transport differs).
"""
# Arrange
from pymavlink.dialects.v20 import ardupilotmega as _mavlink
monkeypatch.setenv("BUILD_REPLAY_SINK_JSONL", "ON")
transport = NoopMavlinkTransport()
outbound_mav = _mavlink.MAVLink(file=None, srcSystem=1, srcComponent=1)
fc = TlogReplayFcAdapter.__new__(TlogReplayFcAdapter)
# Initialise only the slots the encoder code path consults so the
# test stays focused on the wire-routing contract (no tlog file,
# no BUILD_TLOG_REPLAY_ADAPTER gate, no decode thread).
fc._mavlink_transport = transport
fc._outbound_mav = outbound_mav
fc._sequence_number = 0
fc._clock = WallClock()
fc._clock_us_provider = lambda: int(fc._clock.monotonic_ns() // 1000)
fc._clock_ms_boot_provider = (
lambda: int(fc._clock.monotonic_ns() // 1_000_000) % 0xFFFFFFFF
)
output = EstimatorOutput(
frame_id=uuid4(),
position_wgs84=LatLonAlt(lat_deg=50.0, lon_deg=30.0, alt_m=100.0),
orientation_world_T_body=Quat(w=1.0, x=0.0, y=0.0, z=0.0),
velocity_world_mps=(0.0, 0.0, 0.0),
covariance_6x6=np.eye(6, dtype=np.float64) * 0.25,
source_label=PoseSourceLabel.VISUAL_PROPAGATED,
last_satellite_anchor_age_ms=0,
smoothed=False,
emitted_at=0,
)
# Act
for _ in range(10):
fc.emit_external_position(output)
# Assert
assert transport.bytes_written() > 0, (
f"NoopMavlinkTransport.bytes_written() = {transport.bytes_written()}; "
"expected > 0 after 10 emit_external_position calls"
)
)
def test_ac9_noop_transport_bytes_written_after_runtime_drive() -> None:
raise NotImplementedError("see skip reason")
# ----------------------------------------------------------------------