# mavlink2

[MAVLink v2](https://mavlink.io/en/guide/serialization.html) for Elixir: a
frame codec, message signing, dialects generated from MAVLink XML, a link
process over a serial port or any other byte transport, a router, and the
command, parameter and mission protocols.

- Frames: payload truncation and zero-extension, resynchronisation after a
  bad frame, partial frames buffered until complete, 32-bit system ids and
  header targets (`SYSID32`, `TARGET32`).
- Dialects: `use MAVLink2.Dialect.XML` turns a MAVLink XML file and its
  includes into a struct per message, `encode/1`, `decode/2` and enum lookups.
- Signing: sign and verify, per-stream replay protection, timestamps that
  never repeat when the clock steps back.
- Link: a GenServer that owns a transport, numbers outgoing frames, delivers
  decoded frames to subscribers and counts errors, sequence gaps per sender
  and rejected signatures.
- Router: forwards frames between links by MAVLink's routing rules,
  unchanged, signatures intact.
- Services: commands with acks and retries, parameters read, set and
  listed, missions uploaded, downloaded and cleared.
- Checked against pymavlink: `crc_extra`, payload length, wire order and field
  types for every message in `common.xml`, packed payloads, and signed frames
  byte for byte; and against the C library: frames with extended headers,
  signed and unsigned.

MAVLink v1 frames are refused.

## What MAVLink is

[MAVLink](https://mavlink.io/) (Micro Air Vehicle Link) is a lightweight
message protocol for drones, robots and the systems around them: autopilots,
companion computers, ground stations, gimbals, cameras and microcontrollers.
It is the protocol the PX4 and ArduPilot autopilots, and the QGroundControl
and Mission Planner ground stations, speak.

- **Messages** are defined in XML: each has an id and a fixed list of typed
  fields, sent in small binary frames with a checksum. A set of definitions
  is a *dialect*; `common.xml` holds the messages most systems share, and a
  project can add its own.
- **Systems and components** each have an id, carried in every frame, so
  many of them can share links and messages can be routed between them.
- **Microservices** are protocols built from messages: commands with acks,
  parameters, missions, and more.
- **MAVLink 2**, the version this library implements, added extension
  fields, 24-bit message ids, payload truncation and message signing.

The [MAVLink developer guide](https://mavlink.io/en/) documents the
[frame format](https://mavlink.io/en/guide/serialization.html), the
[message definitions](https://mavlink.io/en/messages/common.html) and the
[microservices](https://mavlink.io/en/services/).

## When to use MAVLink

### Use cases

| Use case | What MAVLink gives it |
| --- | --- |
| A companion computer (a Nerves board, a Raspberry Pi) talking to a PX4 or ArduPilot flight controller over UART | The autopilot already speaks it: telemetry, commands, parameters and missions with no firmware changes |
| A ground station or fleet service talking to vehicles | The same messages QGroundControl and Mission Planner use, so both can be on the link alongside it |
| A host processor talking to a microcontroller on the same board (motor control, power, sensors, access control) over UART or USB | A custom dialect in XML, with C headers for the firmware generated by MAVLink's tools and an Elixir module for the host generated by this library, both from one file; no extra hardware beyond the port |
| Several components sharing links (a telemetry radio, a companion, a gimbal, a camera) | System and component ids on every frame, and routing rules, so `MAVLink2.Router` can forward between them |
| A link that must refuse forged or replayed commands | Message signing with a shared key and per-stream timestamps |

### Compared with building your own

A protocol over a byte stream needs all of these, and MAVLink has them:

| Need | MAVLink |
| --- | --- |
| Finding frame boundaries in a stream, and recovering after noise | A start byte, a length, and a checksum to resynchronise on |
| Catching corruption | CRC-16 over every frame |
| Catching two ends built from different message definitions | `crc_extra`, a per-message byte from the definition folded into the checksum, so a mismatched peer's frames fail the check |
| Detecting lost frames | A sequence number per sender |
| Adding fields without breaking old peers | Extension fields, and zero-extension of short payloads |
| Authenticating frames | Signing, with replay protection |
| Tools to inspect and test it | pymavlink, MAVProxy, a generated Wireshark dissector, and autopilot simulators |

### Compared with other protocols

| Alternative | Difference |
| --- | --- |
| Protobuf, CBOR or JSON over serial | These encode a message but do not frame it on a byte stream, check it, number it or sign it. MAVLink's overhead is 12 bytes a frame (25 signed), and trailing zeros are dropped from the payload |
| MQTT, ROS 2 or DDS | These need an IP network, and MQTT also needs a broker. MAVLink runs point to point on a 57600-baud radio |
| CAN, DroneCAN or Cyphal | A multi-drop bus with priority arbitration, and framing, CRC, acknowledgement and retransmission in the controller. Each classic CAN frame costs about 67 bits for at most 8 data bytes, and DroneCAN and Cyphal use one of those for a tail byte: a 28-byte message is 5 frames, about 655 bits, against 400 bits for MAVLink on a UART at 8N1. MAVLink needs only a UART or USB port, and leaves checking to software and retries to the services (`MAVLink2.Command`, `Param`, `Mission`) |

### When something else fits better

- **Payloads over 255 bytes:** a large payload has to be split, or sent through MAVLink's FTP service.
- **Video and bulk data:** these usually go on their own stream (RTP, for example).
- **Rich request/response APIs** with nested or variable-length types: gRPC or HTTP suit these better on an IP network.

## Using it, or having an agent use it

[`usage-rules.md`](usage-rules.md) is a short set of rules for writing code
against this library, for people and coding agents alike; tools such as
[`usage_rules`](https://hex.pm/packages/usage_rules) gather it from your
dependencies. The library checks how it is called:

- A mistake in a call raises `ArgumentError` naming what is wrong and what
  would be right, with the closest names for a misspelt option or enum
  entry (`:baud` suggests `:speed`; `:mav_cmd_arm` suggests
  `:mav_cmd_component_arm_disarm`).
- Errors from the other end of a link, timeouts and refusals, are returned
  as `{:error, reason}`.

## Installation

```elixir
def deps do
  [
    {:mavlink2, "~> 0.1"},
    # only for MAVLink2.Transport.UART
    {:circuits_uart, "~> 1.5"}
  ]
end
```

Requires Elixir 1.18 or later.

## Usage

### A dialect

Put a MAVLink XML file and the files it includes in your project, for example
`common.xml`, `standard.xml` and `minimal.xml` from
[mavlink/mavlink](https://github.com/mavlink/mavlink/tree/master/message_definitions/v1.0):

```elixir
defmodule MyApp.MAVLink do
  use MAVLink2.Dialect.XML, file: "priv/mavlink/common.xml"
end
```

The path is relative to the project root. The module recompiles when any of
the XML files changes. Each message becomes a struct (`HEARTBEAT` is
`MyApp.MAVLink.Heartbeat`) with the XML's field names; enums and entries are
lowercase atoms.

### A link

```elixir
alias MyApp.MAVLink, as: D

{:ok, link} =
  MAVLink2.Link.start_link(
    dialect: D,
    device: "/dev/ttyUSB0",
    speed: 57_600,
    sysid: 255,
    compid: 190
  )

:ok = MAVLink2.Link.subscribe(link, self(), decode: true, only: [D.Heartbeat])

:ok =
  MAVLink2.Link.send(link, %D.Heartbeat{
    type: D.enum_value(:mav_type, :mav_type_gcs),
    autopilot: D.enum_value(:mav_autopilot, :mav_autopilot_invalid)
  })

receive do
  {MAVLink2.Link, ^link, frame, %D.Heartbeat{} = heartbeat} ->
    {frame.sysid, frame.compid, heartbeat}
end

MAVLink2.Link.stats(link)
#=> %{rx: 1, tx: 1, errors: 0, dropped: 0, unknown: 0, unsigned: 0, bad_signature: 0, replay: 0}
```

Link options:

| Option | |
| --- | --- |
| `:dialect` | the dialect module (required) |
| `:transport` | a `MAVLink2.Transport`; default `MAVLink2.Transport.UART`. Remaining options go to its `open/1` |
| `:device`, `:speed`, `:flow_control` | `MAVLink2.Transport.UART` options; `:speed` defaults to 115200. A USB CDC ACM port (`/dev/ttyACM0`) is a serial device too |
| `:sysid`, `:compid` | this end's ids; default 1, 1. `:sysid` may be up to 32 bits; 0 is refused |
| `:unknown` | `:keep` delivers frames whose id the dialect lacks, unchecked, with `known: false`; default `:skip` |
| `:sendable` | message ids the peer accepts; others return `{:error, {:not_sendable, id}}` |
| `:signing` | a `MAVLink2.Signing`; see below |
| `:name` | registered name |

If the transport does not open, the link logs a warning and `start_link/1`
returns `:ignore`.

### Signing

```elixir
key = :crypto.strong_rand_bytes(32)  # shared with the peer
signing = MAVLink2.Signing.new(key, link_id: 1)

MAVLink2.Link.start_link(dialect: D, device: "/dev/ttyUSB0", signing: signing)
```

With `:signing`, every frame sent is signed and a frame received is delivered
only if it verifies. Rejected frames are counted as `:unsigned`,
`:bad_signature` or `:replay`. `accept_unsigned: [msgid]` lets the listed
message ids through unsigned, and `accept_unsigned: :all` every one, for a
link that is secure by itself (USB, a board-level UART). A subscriber that
calls `Link.subscribe(link, self(), rejected: true)` also gets each refused
frame, as `{MAVLink2.Link, link, {:rejected, reason, frame}}`, to show as
untrusted.

A frame from a stream not seen before is refused when its timestamp is more
than a minute behind the clock, and this end's own frames reflected back are
refused as replays. A board that boots without real time can save
`signing.timestamp` and start from it with `Signing.new(key, timestamp: saved)`.

### A target over 255

A system id over 255 goes in the header: `Frame.encode/2` sets `SYSID32` for
a `sysid` over 255 and `TARGET32` for a `target_sysid`. `Link.send/2` does
this for a message whose `target_system` is over 255, putting 255 in the
payload as MAVLink requires:

```elixir
MAVLink2.Link.send(link, %D.CommandLong{target_system: 100_000, target_component: 1, command: 400})
```

The services below do the same for any target.

### Routing

```elixir
MAVLink2.Router.start_link(links: [:telemetry, :companion, :gimbal], sysid: 1)
```

The router learns which link each `{sysid, compid}` is on from what it
receives, sends broadcasts to every other link, sends a frame for another
system to the links where that system has been seen, and never back where it
came from. Within its own system (`:sysid`), a frame for a component goes
only where that component has been seen. It forwards with
`MAVLink2.Link.forward/2`, which writes a frame's original bytes, so
signatures survive.

- Links with `unknown: :keep` let it pass on messages outside its dialect.
- Frames a link's signing refuses are forwarded too, but teach it no routes.
- `SETUP_SIGNING` is never forwarded.
- A `SYSTEM_TIME` showing a reboot clears that system's routes.

### Commands, parameters and missions

Each call runs in its own task subscribed to the link, sends again when the
target is quiet, and needs a dialect with `common.xml`'s messages. `target`
is `{sysid, compid}`.

```elixir
{:ok, :mav_result_accepted, _ack} =
  MAVLink2.Command.long(link, {1, 1}, :mav_cmd_component_arm_disarm, [1.0])

{:ok, %{value: 1}} = MAVLink2.Param.read(link, {1, 1}, "SYSID_THISMAV")
{:ok, _} = MAVLink2.Param.set(link, {1, 1}, "RTL_ALT", 3000, :mav_param_type_int32)
{:ok, params} = MAVLink2.Param.list(link, {1, 1})

{:ok, opaque_id} =
  MAVLink2.Mission.upload(link, {1, 1}, [
    [command: :mav_cmd_nav_takeoff, frame: :mav_frame_global_relative_alt, z: 20.0]
  ])

{:ok, items, opaque_id} = MAVLink2.Mission.download(link, {1, 1})
```

| Module | Options |
| --- | --- |
| `MAVLink2.Command` | `:timeout`, `:retries`, `:progress_timeout`, `:on_progress` for `MAV_RESULT_IN_PROGRESS` acks; `cancel/3` sends `COMMAND_CANCEL` |
| `MAVLink2.Param` | `:timeout`, `:retries`, `:encoding` (`:cast` for ArduPilot, `:bytewise` for PX4); `list/3` to compid 0 lists every component |
| `MAVLink2.Mission` | `:timeout`, `:item_timeout`, `:retries`, `:mission_type` (mission, fence or rally) |

### Without a link

`MAVLink2.Frame` encodes and decodes frames directly:

```elixir
{msgid, payload} = D.encode(heartbeat)
{:ok, bytes} = MAVLink2.Frame.encode(%MAVLink2.Frame{msgid: msgid, payload: payload}, D)

{frames, errors, rest} = MAVLink2.Frame.drain(bytes, D)
```

`drain/2` returns every complete frame in a buffer, the errors it skipped
over, and the bytes left for the next read.

## Specification

The library follows the [MAVLink developer guide](https://mavlink.io/en/):

| Part | Guide |
| --- | --- |
| Frames, checksum, `crc_extra`, payload truncation | [Serialization](https://mavlink.io/en/guide/serialization.html) |
| Signing | [Message signing](https://mavlink.io/en/guide/message_signing.html) |
| `MAVLink2.Router` | [Routing](https://mavlink.io/en/guide/routing.html) |
| `MAVLink2.Command`, `Param`, `Mission` | [Command](https://mavlink.io/en/services/command.html), [parameter](https://mavlink.io/en/services/parameter.html) and [mission](https://mavlink.io/en/services/mission.html) protocols |

Where it goes beyond the guide:

- **`SYSID32` and `TARGET32`** (incompat flags `0x02`, `0x04`) follow the C
  library (`c_library_v2`), which has them before the guide does.
- **Signing** is stricter: a timestamp equal to the stream's last is a replay,
  as in the C library; the window for new streams counts from the clock even
  before this end has sent; and this end's own frames reflected back are
  refused.
- **Recovery after noise** resumes after the start byte of a frame that fails
  its checksum or has an unknown id; the guide leaves recovery to the
  implementation.
- **A system broadcast within the router's own system** (target component 0)
  goes to the links where that system has been seen.
- **MAVLink v1** is not implemented.

## Modules

| Module | |
| --- | --- |
| `MAVLink2.Frame` | v2 encode and decode for a dialect; `drain/2` over a byte stream |
| `MAVLink2.Dialect` | the behaviour a dialect implements: `crc_extra/1`, `payload_length/1`, optionally `target_offsets/1` |
| `MAVLink2.Dialect.XML` | generates a dialect from MAVLink XML at compile time |
| `MAVLink2.XML` | reads MAVLink XML with its includes: wire order, lengths, `crc_extra` |
| `MAVLink2.Codec` | payloads from a wire layout; NaN and the infinities as atoms |
| `MAVLink2.Signing` | sign, verify, per-stream replay protection |
| `MAVLink2.CRC` | CRC-16/MCRF4XX with `crc_extra` |
| `MAVLink2.Link` | a link process over one transport |
| `MAVLink2.Router` | forwards frames between links |
| `MAVLink2.Command` | `COMMAND_LONG` and `COMMAND_INT`, with acks |
| `MAVLink2.Param` | the parameter protocol |
| `MAVLink2.Mission` | the mission protocol |
| `MAVLink2.Transport` | the transport behaviour: `open/1`, `write/2`, `close/1` |
| `MAVLink2.Transport.UART` | a serial port through `Circuits.UART` |
| `MAVLink2.Transport.Loopback` | in memory, for tests; `echo: true` sends writes back, `peer:` joins two links |

A custom transport implements `MAVLink2.Transport` and sends received bytes to
the link as `{:mavlink_data, binary}`.

## Tests

```sh
mix test
```

The fixtures are pymavlink 2.4.50's XML definitions and its output on them;
see `test/fixtures/README.md`. The extended-header frames in
`test/mavlink2/extended_header_test.exs` are the C library's.

## License

MIT; see `LICENSE`. The files in `test/fixtures` keep their own licenses, listed
in `test/fixtures/README.md`.
