Skip to main content

MAVLink Proxy

The MAVLink proxy is the agent’s core service, a native Rust router (ados-mavlink-router). It connects to the flight controller over serial UART, parses MAVLink frames, and distributes them to every other part of the system: the GCS over WebSocket, other services via the IPC socket, the cloud relay, and the terminal status page. The proxy runs as the ados-mavlink systemd service.

Flight controller auto-detection

When mavlink.serial_port is empty in config (the default), the proxy scans serial ports in this order:
  1. Ports listed in the board profile’s uart_paths (e.g., /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 produces a heartbeat wins. 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 FC is found, the proxy backs off and retries, starting at 1 second and doubling to a 30 second ceiling. This handles cases where the FC boots slower than the companion computer or gets power-cycled mid-flight.

Serial connection

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 an FC is attached, which pins the link and skips the sweep, while a unit installed with no FC 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 FCs 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 serial connection feeds multiple consumers:

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 (JSON): a parsed telemetry summary published at 10 Hz. Contains attitude, position, speed, battery, GPS, and armed state as JSON. Used by the health service, cloud relay, and TUI.

WebSocket endpoint

The proxy exposes a WebSocket endpoint at port 8765 (configurable via mavlink.endpoints). This is the primary way ADOS Mission Control and other browser-based GCS tools 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 still admits any caller: it has no credential to check against, and refusing would lock you out of a node you have not claimed yet. 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 accept a connection with no credential by default, so that a desktop GCS can attach. Unlike the WebSocket they have nowhere to carry one: MAVLink over a raw socket has no handshake and no headers, so the only thing the agent knows about a caller is its address. Setting mavlink.raw_proxy_enforce_auth to true makes a paired node admit those three ports from the box itself and refuse every off-box peer, the AP hotspot and the USB gadget network included, because a paired node checks for a pairing key and the raw ports have no way to carry one. That closes the ports to a desktop GCS on your LAN, with nothing the GCS can present to get back in, which is why it is off unless you ask for it. On an unpaired node the same flag keeps the lifelines a fresh unit actually has, loopback, link-local, the first-boot AP subnet and the USB gadget subnet, and treats the rest of the LAN as unauthorized. Either way, treat any network the agent is on as a network that can fly the aircraft: 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, wire-compatible with the SITL bridge tool. No encoding changes, no JSON wrapping. The GCS 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 prevents 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 requests data streams from the FC:
  • SYS_STATUS (battery, health)
  • ATTITUDE (roll, pitch, yaw)
  • GLOBAL_POSITION_INT (GPS lat/lon/alt)
  • GPS_RAW_INT (fix type, satellites, HDOP)
  • VFR_HUD (airspeed, groundspeed, heading)
  • RC_CHANNELS (RC input values)
  • HEARTBEAT (armed state, flight mode)
Stream requests are re-sent every 30 seconds to handle FC reboots and MAVLink buffer stalls. The initial request fires immediately on connection.

Reconnection

If the serial connection drops (FC power cycle, USB disconnect, cable fault), the proxy:
  1. Closes the old connection cleanly (prevents file descriptor leaks)
  2. Waits 2 seconds
  3. Restarts auto-detection from the beginning
  4. Re-requests all data 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 signing key lives in the GCS browser as a non-extractable Web Crypto key. The agent exposes /api/mavlink/signing/capability, /api/mavlink/signing/enroll-fc, and a few related endpoints so the GCS can push the key to the flight controller once via SETUP_SIGNING. The agent never persists the key. See MAVLink Signing for the full guide, enable steps, and troubleshooting.
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.

State IPC format

The JSON telemetry on /run/ados/state.sock looks like this (published at 10 Hz):
From the terminal:
From the REST API:
For setup, open the local setup webapp and use the MAVLink page.