Skip to main content
The MAVLink proxy is the agent’s core service, a native Rust router (ados-mavlink-router) that runs as the ados-mavlink systemd service. It opens the flight controller link, parses MAVLink frames and distributes them to the rest of the system: Mission Control over WebSocket, other services over IPC sockets, the cloud relay and the terminal status page.
Anything that reaches the proxy can command the flight controller, including arming and mode changes. Read Safety before you open the proxy to a LAN or fly with it.

Flight controller auto-detection

mavlink.source picks the transport: auto (the default), serial, udp or tcp. With source: auto and mavlink.serial_port empty, the proxy scans serial ports:
  1. Ports listed in the board profile’s uart_paths (for example /dev/ttyS0, /dev/ttyAMA0)
  2. USB serial devices at /dev/ttyACM* and /dev/ttyUSB*
For each candidate port it sweeps a list of baud rates, listening 1.5 seconds at each one for a MAVLink heartbeat. The first port and baud that produce a heartbeat win. If the port is emitting MSP instead of MAVLink, the sweep stops there as well, because no baud will ever yield a heartbeat from an MSP flight controller. If no flight controller is found, the proxy retries every 3 seconds, indefinitely. This covers a flight controller that boots slower than the companion computer or is power-cycled.

Network flight controllers

A flight controller or simulator reachable over the network uses a udp: or tcp: form in mavlink.serial_port. Set mavlink.source to match, or leave it at auto and the prefix decides.

Configuration keys

What baud your drone is actually running

mavlink.baud_rate is only honoured when mavlink.serial_port names a port. With the port left empty, the configured baud is not used at all and the proxy sweeps this list in order:
So a drone left on the default config most likely settles at 115200, not at the 57600 the config file shows. 57600 is third in the sweep, and it is also the last-ditch baud the proxy opens at when no candidate produced a heartbeat, which is a silent port rather than a working link. Two things follow. Setting mavlink.baud_rate on its own changes nothing, so pin mavlink.serial_port alongside it if you want a specific rate. And a fresh install behaves differently depending on what was plugged in at the time: the installer writes the detected port into the config when a flight controller is attached, which pins the link and skips the sweep, while a unit installed with no flight controller attached ships with an empty port and probes on every connect. Read back what the link actually settled on with ados status, rather than reading it off the config file.
A USB-CDC flight controller ignores the requested baud entirely, since bytes flow at the native USB rate. The sweep matters for flight controllers on a real UART. For those, 921600 carries high-rate attitude data and parameter downloads far better than 57600.

Multi-client distribution

The proxy is a one-to-many bridge. A single flight controller link feeds every consumer:

IPC sockets

Two Unix domain sockets carry data between services: /run/ados/mavlink.sock (binary): raw MAVLink frames with a 4-byte big-endian length prefix before each frame. Any service or script can connect and receive the full MAVLink stream. /run/ados/state.sock (msgpack): the vehicle state as length-prefixed msgpack (contract state.v2), a 4-byte big-endian length followed by a body {v, s}, published at about 10 Hz. s holds attitude, position, speed, battery, GPS and armed state. The health service, cloud relay and TUI read it. The frame format is documented in Agent services.

WebSocket endpoint

The proxy exposes a WebSocket endpoint on port 8765 (set in mavlink.endpoints). This is how Mission Control and other browser ground stations connect to the drone. The credential for that endpoint is a short-lived HMAC ticket. A client asks the agent for one with POST /api/_ws/ticket, which is itself gated by the pairing key, and presents it on connect as a WebSocket subprotocol:
The ticket is signed with a key derived from the agent’s pairing key, carries a scope, and expires after 30 seconds by default (120 seconds maximum). Because it is derived rather than stored, re-pairing or a factory reset invalidates every ticket already issued. A non-browser client can present the pairing key directly in an X-ADOS-Key header instead.
A paired node refuses a WebSocket connection that presents neither the pairing key nor a ticket. An unpaired node admits the box itself and its first-boot lifelines without a credential: loopback, link-local addresses, a device on the node’s own access point (reaching 192.168.4.1) and a host on the USB gadget link (reaching 192.168.7.1). It has no key to check yet, and those are the routes a fresh unit is reached from. Every other caller, including the rest of your LAN, is refused until the node is paired. The flag is mavlink.ws_proxy_enforce_auth, it defaults to true, and setting it to false returns to recording an unauthorized connection instead of rejecting it, a deliberate opt-out for a third-party client that cannot present a credential.The raw MAVLink endpoints on TCP 5760 and UDP 14550 and 14551 have nowhere to carry a credential: MAVLink over a raw socket has no handshake and no headers, so the only thing the agent knows about a caller is its address. Their posture follows the pairing state:
  • Unpaired: the raw ports serve the box itself and the first-boot lifelines (loopback, link-local, the node’s access point, the USB gadget link). Other LAN hosts are refused unless mavlink.raw_proxy_lan_access: true. Setting mavlink.raw_proxy_enforce_auth: false (default true) returns to serving and logging unauthorized peers.
  • Paired: the raw ports serve only the box itself. An off-box desktop ground station, on the LAN, the access point or the USB link, is refused, because a paired node wants the pairing key and the raw ports cannot carry it. To attach one, set mavlink.raw_proxy_lan_access: true (default false); off-box peers are then served.
The current posture is reported as mavlink_proxy_posture in the agent’s telemetry. Opening the raw ports to the LAN means anything that can reach 5760 can command the flight controller: keep the agent on a link you control, do not port-forward 8765, 5760, 14550 or 14551 to the internet, and do not join an untrusted WiFi network with a drone powered up.
The WebSocket carries raw binary MAVLink frames, with no encoding changes and no JSON wrapping. Mission Control decodes MAVLink in the browser using the same CRC and payload tables as pymavlink.

Companion heartbeat

The proxy sends a 1 Hz companion heartbeat to the flight controller with MAV_TYPE_ONBOARD_CONTROLLER. This keeps ArduPilot’s GCS failsafe from triggering when the companion computer is the only MAVLink endpoint. Without this heartbeat, ArduPilot assumes the GCS has disconnected after 5 seconds and can trigger RTL or land depending on failsafe settings.

Stream requests

On connection, the proxy asks the flight controller for these messages with SET_MESSAGE_INTERVAL: The first request fires immediately on connection. The proxy then re-sends the requests on an adaptive cadence between 10 and 60 seconds (30 seconds by default), shortening it when the stream stalls, to cover flight controller reboots and buffer stalls.
iNav, Betaflight’s MAVLink telemetry and ArduPilot before 4.1 ignore SET_MESSAGE_INTERVAL and honour only the legacy REQUEST_DATA_STREAM. For those, set mavlink.legacy_stream_request: true. It is off by default because firmware that honours both mechanisms streams roughly twice the requested telemetry.

Reconnection

If the flight controller link drops (power cycle, USB disconnect, cable fault), the proxy:
  1. Closes the old connection cleanly
  2. Retries every 3 seconds, indefinitely
  3. Runs auto-detection again when the source is auto
  4. Re-requests all message streams once reconnected
The IPC sockets and WebSocket stay open during reconnection. Clients see a gap in telemetry but do not need to reconnect themselves. The agent is a transparent pipe for signed frames. The 32-byte key lives in the Mission Control browser as raw bytes in IndexedDB; the agent never persists it. The agent exposes /api/mavlink/signing/capability, /api/mavlink/signing/enroll-fc and related routes so Mission Control can push the key to the flight controller once via SETUP_SIGNING. See MAVLink signing for the full guide.
The agent config file does not carry a signing key. Any legacy mavlink.signing block in an old config is ignored with a deprecation warning on agent start.
From the terminal:
From the REST API:

Next steps

MAVLink signing

Sign every command so only your browser can control the flight controller.

Video pipeline

How the camera feed is encoded and served.

Configuration

Where mavlink.* lives in the agent config.

Drone Agent troubleshooting

Fix a flight controller that does not connect.
Last modified on October 11, 2026