Files
Oleksandr Bezdieniezhnykh 740bf37d76 [AZ-641] [AZ-642] [AZ-644] mavlink transport + codec + mission pull
Lands the second batch under epic AZ-626's implementation plan.

mavlink_layer (AZ-641 + AZ-642):
- Hand-rolled MAVLink v2 codec covering the §7.7 surface: HEARTBEAT,
  SYS_STATUS, SET_MODE, ATTITUDE, GLOBAL_POSITION_INT, MISSION_* (7),
  COMMAND_LONG, COMMAND_ACK, EXTENDED_SYS_STATE, STATUSTEXT (17 total).
- Streaming decoder demuxes arbitrary-sized byte arrivals, drops malformed
  frames with typed parse-error counters (crc/truncated/unknown_id/seq_gap),
  and surfaces sequence gaps without hard-failing the link.
- Encoder tracks the per-link tx_seq counter and applies the MAVLink v2
  trailing-zero payload truncation rule.
- UDP and POSIX-serial transports behind a single async Transport trait;
  the run loop owns transport open with bounded exponential backoff
  (2 s serial / 5 s UDP cap) and a tokio::select! per-link read+write
  loop.
- 1 Hz outbound HEARTBEAT scheduler + inbound-heartbeat watchdog that
  fires LinkUp / LinkLost on a broadcast channel and feeds health detail
  (connected, last_heartbeat_age_ms, signing_enabled, parse_errors).

mission_client (AZ-644):
- HTTPS GET /missions/{id} over rustls (no OpenSSL on the airframe).
- Bundled JSON Schema (crates/shared/contracts/mission-schema.json,
  draft-07, additionalProperties:false) validates every response;
  schema-invalid bodies surface as FetchError::SchemaInvalid with a
  1 KiB sample of the raw body for offline analysis.
- Transient failures (timeout, 5xx, 429) retry with bounded exponential
  backoff up to MissionClientOptions.max_attempts (default 5); permanent
  failures (4xx, malformed URL) abort immediately.
- Health surface mirrors AC-1's contract: last_fetch_ts,
  fetch_errors_total, schema_version, connection_state.

Caught and fixed before commit (NOT a code-review finding — caught by
the unit test that hand-computed CRC("123456789")): the hand-rolled
X.25 CRC accumulator was operating in u16 throughout. The MAVLink C
reference declares `tmp` as uint8_t, which silently truncates the
shifted-in bits. Round-trip tests passed (encoder and decoder shared
the bug); a real MAVLink peer would have rejected every frame. Fixed
by mirroring the C reference: `let mut tmp: u8 = …; tmp ^= tmp.wrapping_shl(4);`.
Added a regression test asserting CRC("123456789") == 0x6F91 against
pymavlink's reference value (NOT the textbook 0x29B1 — MAVLink uses a
byte-wise variant, not the bit-reflected CCITT).

AC verification (full detail in
_docs/03_implementation/batch_02_cycle1_report.md):

AZ-641: AC-1 + AC-3 + AC-4 verified via UDP loopback integration tests;
        AC-2 (serial) requires a socat pty pair and runs in the SITL/CI
        tier (test exists as #[ignore]-marked stub).
AZ-642: AC-1 + AC-2 + AC-3 verified via exhaustive codec round-trip and
        decoder negative-path tests; AC-4 (SITL round-trip) requires
        ArduPilot SITL — the CRC fix above means the codec is now
        wire-correct, ready for the sitl-conformance Woodpecker stage.
AZ-644: all four ACs verified via wiremock-driven integration tests.

Workspace gates green:
- cargo check --workspace                                clean
- cargo check --workspace --no-default-features          clean
- cargo fmt --all -- --check                             clean
- cargo clippy --workspace --all-targets -- -D warnings  clean
- cargo test --workspace                                 pass (1 expected ignore)

Layering invariants from module-layout.md hold: mavlink_layer and
mission_client are Layer 2 actors importing only `shared`; no sibling
Layer-2 imports; MavlinkHandle implements shared::contracts::MavlinkSink.

Jira: AZ-641, AZ-642, AZ-644 transitioned To Do → In Progress at batch
start; the matching In Testing transitions follow this commit.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 12:29:49 +03:00

4.3 KiB
Raw Permalink Blame History

MAVLink Message Codec (§7.7 Surface)

Task: AZ-642_mavlink_codec Name: MAVLink v2 encode/decode for the §7.7 surface Description: Encode and decode the ~1015 MAVLink v2 messages this codebase needs (the §7.7 surface only) with strict validation. Complexity: 5 points Dependencies: AZ-640_initial_structure Component: mavlink_layer Tracker: AZ-642 Epic: AZ-637

Problem

Autopilot speaks a deliberately narrow MAVLink command surface (per architecture.md §7.7 — ~1015 messages). Adding messages outside that list requires explicit design review. A hand-rolled MAVLink v2 codec must encode outbound messages with correct sequence numbers, system / component IDs, and (when enabled) signing, and decode inbound messages with strict validation — rejecting malformed frames, unknown IDs, and signing failures.

Outcome

  • Outbound encoder produces wire-correct MAVLink v2 frames for the message surface in §7.7 with monotonically incrementing per-link sequence numbers.
  • Inbound decoder parses the same surface, rejecting malformed frames, unknown message IDs, and frames with sequence-number gaps (logged, not hard-failed).
  • Decoded messages are exposed as a typed MavlinkMessage enum (one variant per supported message kind) on the inbound channel.
  • Per-message-kind parse error counters are exposed via health().

Scope

Included

  • Encode + decode for HEARTBEAT (bidir), COMMAND_LONG outbound subset (arm/disarm, takeoff, set-mode, change-speed, change-alt, land, RTL), COMMAND_ACK inbound, MISSION_COUNT, MISSION_REQUEST_INT, MISSION_ITEM_INT, MISSION_ACK, MISSION_SET_CURRENT, MISSION_CURRENT, MISSION_ITEM_REACHED, MISSION_CLEAR_ALL, GLOBAL_POSITION_INT, ATTITUDE, SYS_STATUS, EXTENDED_SYS_STATE, STATUSTEXT, SET_MODE.
  • Per-link outbound tx_seq counter with wrap-around handling.
  • Strict size + CRC validation; reject malformed frames.
  • Unknown message IDs counted and dropped (not hard-failed).
  • Sequence-number gap detection (logged, not fatal).

Excluded

  • Transport and reconnect (task 02).
  • Heartbeat scheduling (task 02).
  • Ack demultiplexing to callers (task 04).
  • MAVLink-2 signing (task 04).
  • Any message not in the §7.7 surface — adding new messages requires design review.

Acceptance Criteria

AC-1: Round-trip every supported message Given the encoder produces a frame for each message kind in the §7.7 surface with deterministic field values When the same frame is fed back through the decoder Then the typed MavlinkMessage matches the original fields and parse_errors_total does not increment.

AC-2: Malformed frame is rejected Given a byte buffer with a truncated payload or a wrong CRC When the decoder consumes it Then the frame is dropped, parse_errors_total{kind="crc" | "truncated"} increments by 1, and the codec continues processing subsequent bytes.

AC-3: Unknown message ID is counted, not fatal Given an inbound frame with a message ID outside the §7.7 surface When the decoder consumes it Then the frame is dropped, parse_errors_total{kind="unknown_id"} increments by 1, and decoding continues.

AC-4: SITL round-trip Given an ArduPilot SITL instance configured for udp://127.0.0.1:14550 When mavlink_layer emits a COMMAND_LONG for MAV_CMD_NAV_RETURN_TO_LAUNCH Then SITL receives the command and replies with a matching COMMAND_ACK; the decoder emits a MavlinkMessage::CommandAck with result = MAV_RESULT_ACCEPTED.

Non-Functional Requirements

Performance

  • Per-message encode + decode round-trip: ≤50 ms p99 on a healthy link (per description.md §8).

Reliability

  • No silent acceptance of malformed or signed-mismatch frames.

Constraints

  • Hand-rolled — no third-party MAVLink SDK.
  • Adding any message outside the §7.7 surface requires an explicit design review noted in the PR description.

Runtime Completeness

  • Named capability: MAVLink v2 wire-correct encode/decode for the §7.7 command surface.
  • Production code that must exist: real byte-level encoder + decoder; CRC computation; sequence number handling.
  • Allowed external stubs: ArduPilot SITL is the conformance reference for the SITL round-trip AC.
  • Unacceptable substitutes: a JSON or human-readable "MAVLink-like" envelope is not acceptable — the wire format must be MAVLink v2.