# MAVLink Transport and Heartbeat

**Task**: AZ-641_mavlink_transport_and_heartbeat
**Name**: MAVLink transport + heartbeat
**Description**: Single connection abstraction (UDP or serial, picked at startup), 1 Hz outgoing HEARTBEAT, bounded reconnect on transport loss, autopilot-heartbeat timeout detection.
**Complexity**: 3 points
**Dependencies**: AZ-640_initial_structure
**Component**: mavlink_layer
**Tracker**: AZ-641
**Epic**: AZ-637

## Problem

`mavlink_layer` needs a single, stable connection abstraction to the airframe autopilot (ArduPilot / PX4). The connection is either UDP or serial — picked once at startup from the connection URI (`udp://...` or `serial:///dev/...`); no runtime URI swap. The link must self-heal on transport loss with bounded backoff and surface link health to the rest of the system without silent failure.

## Outcome

- A `MavlinkConnection` opens once at startup and reconnects automatically on transport loss with bounded exponential backoff (≤2 s on serial / ≤5 s on UDP).
- A 1 Hz outgoing `HEARTBEAT` keeps the autopilot's GCS-link path alive.
- Autopilot heartbeats received on the inbound stream are timestamped; a configurable wall-clock timeout flips link state to `lost` and surfaces it via `health()` and a typed signal consumed by `mission_executor`.
- Health surface includes `connected`, `last_heartbeat_age_ms`, `signing_enabled`.

## Scope

### Included
- Connection-URI parser (`udp://host:port` and `serial:///dev/...`).
- UDP socket and serial port concrete transports behind a single `Transport` trait.
- Bounded exponential backoff on transport-open failure and on read failure.
- 1 Hz outgoing `HEARTBEAT` timer.
- Inbound heartbeat timestamping + wall-clock timeout → `link_lost` signal.
- `ComponentHealth` surface fields above.

### Excluded
- Message encoding / decoding (separate task 03).
- Command-ack demux and retry (separate task 04).
- MAVLink-2 signing (separate task 04; only the `signing_enabled` flag is plumbed here).

## Acceptance Criteria

**AC-1: UDP connection opens and survives drop**
Given a configured `udp://127.0.0.1:14550` endpoint
When the autopilot is not listening at process start
Then `MavlinkLayer::run()` retries with exponential backoff up to its cap and reports `connected = false` via `health()`; when the autopilot becomes reachable, the link reconnects within ≤5 s.

**AC-2: Serial connection opens and survives drop**
Given a configured `serial:///dev/pts/N` endpoint backed by a `socat` pair (or equivalent)
When the peer end is closed and reopened
Then `mavlink_layer` reconnects within ≤2 s and resumes heartbeat emission.

**AC-3: Heartbeat emitted at 1 Hz**
Given a healthy link
When the connection is open for 10 s
Then exactly 10 ± 1 outbound `HEARTBEAT` frames are observed by the peer.

**AC-4: Autopilot heartbeat loss flips link state**
Given a healthy link that has been emitting peer heartbeats
When the peer stops sending heartbeats
Then within the configured timeout (default 3 s) `health()` reports `link_lost = true` and a typed `LinkLost` signal is emitted on the public output channel.

## Non-Functional Requirements

**Performance**
- Reconnect latency: ≤2 s serial, ≤5 s UDP.
- Heartbeat cadence: 1 Hz ± 50 ms.

**Reliability**
- No infinite retry — bounded backoff cap is configurable (default 30 s).
- Transport-open failure surfaces to health → red; never silently absorbed.

## Constraints

- Hand-rolled — no third-party MAVLink SDK (per `architecture.md §5`).
- Single connection per process; no runtime URI swap.

## Runtime Completeness

- **Named capability**: MAVLink emission (HEARTBEAT) and link liveness.
- **Production code that must exist**: real UDP socket and real serial port transports.
- **Allowed external stubs**: in CI / integration tests, the peer end may be `socat` for serial or a loopback UDP listener.
- **Unacceptable substitutes**: a "fake transport" that swallows writes and synthesises heartbeats is not allowed in production code — only as a test double under `#[cfg(test)]`.
