> Source: https://nexusatlas.dev/guides/serial-radio — Nexus Atlas developer documentation (Guides). Converted from the HTML page; the page is canonical.

# 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](https://nexusatlas.dev/docs/concepts/serial-link).

> **What you need:** both nodes on a build with the serial link, the radios on USB serial adapters, a terminal program for the radios' AT commands, and the radio's port free of any other process. Change both ends of a radio pair in the same maintenance window: a native link talks only to another native 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](https://nexusatlas.dev/docs/concepts/qos) 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 declared `air_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](https://nexusatlas.dev/guides/troubleshooting#serial-closed).
