Run Atlas directly over a serial radio shipped
A pair of SiK-family radios (RFD900-class) with Atlas on both ends needs no adapter, no pppd and no IP plan: one [[link]] type = "serial" on each node puts Atlas frames straight onto the port, paces them on the radio's own buffer and feeds its signal figures into the link score. This guide takes a radio pair that runs through pppd today to a native serial link, adds a third node on a shared channel, and shows how to go back. The mechanism is described in Native serial link.
Prepare the radios
Atlas does not configure the radio. Set each radio once with its AT commands (enter command mode with +++, list parameters with ATI5, write with AT&W, reboot with ATZ):
| Parameter | Set to | Why |
|---|---|---|
SERIAL_SPEED (ATS1) | the link's baud, e.g. 57 for 57 600 | Both sides of the serial line must agree; a mismatch reads as noise. |
AIR_SPEED (ATS2) | the same on both radios, e.g. 64 | Copy it into the link's air_rate, which sets the capacity and the pacer's ceiling. |
MAVLINK (ATS6) | 1 | MAVLink framing makes the radio answer each heartbeat with a RADIO_STATUS frame: RSSI, noise and its transmit-buffer fill. |
RTSCTS | 1 only if the cable carries RTS and CTS | Optional. With it, a full radio stops the line instead of dropping bytes; pacing works either way. |
For several nodes on one channel, load the radios' multipoint firmware, give each radio its own node id and set every radio's destination to broadcast (NODEDESTINATION 65535), so every radio hears every other.
Take the radio off pppd
On both nodes, stop and disable the SiK adapter that owns the radio and the pppd that attaches to its pseudo-terminal, so neither starts again at boot:
sudo systemctl disable --now <the adapter's unit> <the pppd unit for this radio>
Remove the old link that bound to the PPP interface (the [[link]] with bind_device = "ppp0" or similar) and any endpoint_per_link entry that names it. If something still holds the port when the daemon starts, the serial link's open fails and retries; once the link holds the port, it holds it exclusively, so a late starter gets EBUSY rather than a share of the byte stream.
Configure the link
Use the radio's /dev/serial/by-id/ path: it names the same radio after every replug. Point to point, on each node:
[[link]]
name = "rfd"
type = "serial"
device = "/dev/serial/by-id/usb-FTDI_FT231X_USB_UART_D30A1B2C-if00-port0"
baud = 57600 # = SERIAL_SPEED on the radio
radio = "rfd900" # rfd900 | sik write heartbeats; generic does not
air_rate = "64 kbps" # = AIR_SPEED on the radio
flow_control = "none" # "rtscts" if radio and cable carry it
probe_interval_ms = 1000 # radios are slow and half duplex
peers = ["10.0.100.2"] # the node at the other end
[[peer]]
public_key = "…"
allowed_ips = ["10.0.100.2/32"]
# no endpoint: a serial link needs none
With no capacity, the link's capacity is the lower of the line rate (baud × 0.8) and air_rate — 46 kbit/s for this example, where the 57 600-baud line is narrower than the air. At 115 200 baud the 64 kbit/s air sets it. Set the link's probe_interval_ms to 500–1000: a round trip on a half-duplex radio includes waiting for the other side's transmit window. Leave unusable_rtt_ms at its default.
Validate before restarting. atlasd config check runs the daemon's own load and refuses, by name, a serial key on a non-serial link, an unsupported baud, a reserved or duplicate address, peer_addresses without an own address, and two serial links on one device:
$ atlasd config check -c /etc/atlas/config.toml
✓ /etc/atlas/config.toml is loadable
Restart the daemon on both nodes. The log names the port as it opens:
serial link rfd: /dev/serial/by-id/usb-FTDI_FT231X_USB_UART_D30A1B2C-if00-port0 open at 57600 baud (none, radio rfd900)
A third node on the same channel
With the radios on multipoint firmware and broadcasting, give each node an address on the channel and tell it where the others sit. Node 1:
[[link]]
name = "rfd"
type = "serial"
device = "/dev/serial/by-id/usb-FTDI_FT231X_USB_UART_D30A1B2C-if00-port0"
baud = 57600
radio = "rfd900"
air_rate = "64 kbps"
probe_interval_ms = 1000
address = 1
peer_addresses = { "10.0.100.2" = 2, "10.0.100.3" = 3 }
Nodes 2 and 3 take address = 2 and 3 with their own maps. Addresses run 1–254 and must differ on one channel; peers is for point-to-point links and is refused here. A node missing from peer_addresses is learned from its first authenticated frame, so a map is needed only on the side that speaks first. Every addressed frame carries three more bytes.
Check it
$ curl -s localhost:9800/api/stats | jq '.links[] | select(.name == "rfd")
| {alive, radio_rssi_dbm, radio_telemetry_source, queue_delay_ms,
serial: .serial | {open, frames_rx, crc_errors, framing_errors,
radio_status_frames, radio_buffer_free_pct, air_rate_kbps}}'
| What to look for | Where | Healthy |
|---|---|---|
| The device is held | serial.open; the link card's serial 57600 chip | true; the chip is not amber |
| The link is up | alive | true after the first probe round trip |
| Frames arrive clean | frames_rx against crc_errors and framing_errors | frames_rx climbing, errors a small share |
| The radio reports | radio_status_frames, radio_status_age_secs, radio_rssi_dbm | Reports climbing, age under a second, radio_telemetry_source is atlasd-serial |
| Pacing holds the radio's buffer | radio_buffer_free_pct, radio_overflow_bytes_est | Free space well above zero under load, overflow flat |
| The queue below is short | queue_delay_ms, egress_queue.serial_bytes | Tens of milliseconds with voice in use |
| Recovery works | pull and replug the radio's USB cable; atlasd journal -c link | link.serial_reopened, reopens +1, traffic back within a fraction of a second |
| Multipoint | not_for_us | Climbing on every node: frames for the others are seen and dropped |
Voice and telemetry on the radio
Voice, Control and Position ride the high band and go to the radio first; video and bulk wait in Atlas's queue while the radio's buffer is full. Fifty 20 ms voice frames a second cost about 43 kbit/s on the wire natively, so a call fits a 64 kbit/s radio with room for telemetry, and on a 57 600-baud line leaves very little else. Carrying the previous packet for loss recovery raises the call to about 69 kbit/s, more than 64 kbit/s of air holds: on such a radio, turn redundancy off for the class or use 40 ms frames (about 28 kbit/s). Keep Video and Bulk off a serial radio that should carry only Control, Voice and Position; the transport classes page shows how.
Going back to pppd
Remove or disable the serial [[link]] on both nodes (enabled = false keeps it in the file), restore the PPP link, start the adapter and pppd again, and restart the daemon. Nothing on the radio needs to change: the adapter reads the same MAVLink framing.
Boundaries
- Both ends run Atlas. Radios that must also carry non-Atlas IP traffic, or devices that offer only PPP or IP, stay on the adapter and pppd.
- On a shared channel, link-state floods and gossip go out as one unicast copy per peer, and frames from two senders that interleave at a receiver lose the split frame (counted in
framing_errors). - A radio with MAVLink framing off, or
radio = "generic", reports nothing: the link still paces on the serial line and drains its model of the radio at the declaredair_rate, and the link score has no RSSI. radio_buffer_bytes(default 2048) is the size the pacer assumes for the radio's buffer; set it to your radio's figure if it differs.
When something does not look like the table above, see the serial entries in troubleshooting.