# MAVLink ground receiver

URL: /collect/receiver

Record what range control hears from each vehicle, stamped with the receiver's own clock. The receive log is what the telemetry_gap rule reads, and it shows when the ground heard a message, not when the vehicle sent it.



The flight log shows what PX4 believed about its link. The receive log shows what actually reached the ground, and when. The **MAVLink receiver** source records it: it &#x2A;*Listens to telemetry on the ground and records every receipt with its own clock.**

## Before you start [#before-you-start]

* You need the **Admin** role.
* The receiver must hear your vehicles' MAVLink telemetry at the address you give it.
* Keep the receiver's clock on NTP. In the example dataset it reads **NTP, synchronised**, good to ±15 ms.
* Know the MAVLink system IDs of the vehicles to record. Each vehicle page shows its system ID.

## Add the receiver [#add-the-receiver]

<Steps>
  <Step title="Choose MAVLink receiver">
    In **Sources**, select **Add source**. Under **Vehicles**, pick **MAVLink receiver** and select **Continue**.
  </Step>

  <Step title="Fill in the fields">
    | Field          | Example in the console                     | What it sets                                                                               |
    | -------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------ |
    | **Name**       | For example: Fleet log archive, west range | A name the team will recognize, such as the range it serves.                               |
    | **Listen on**  | `udp://0.0.0.0:14560`                      | Where the receiver listens. The example listens for UDP on port 14560, on every interface. |
    | **System IDs** | 1 to 16                                    | The MAVLink system IDs to record.                                                          |
    | **Record**     | Every HEARTBEAT and SYSTEM\_TIME receipt   | Which messages to keep.                                                                    |
  </Step>

  <Step title="Test and add">
    Select **Test connection** and wait for the fourth check. Then select **Add source**. The toast names the source and says &#x2A;*The first sync is running.**
  </Step>
</Steps>

If range control already writes receive logs to a server, connect that directory instead. The example dataset's **Range control receive logs** source reads `*.jsonl` over SFTP from `/var/log/mav-receive/` and polls every 5 minutes. See [SFTP, SMB and NFS](/collect/sources/file-shares).

## What a receive record holds [#what-a-receive-record-holds]

A receive log is receiver JSONL: one JSON object per receipt, one receipt per line. Foxborne recognizes the file by the key `"recv_utc"`. This is the last heartbeat from UAS-04 before the telemetry gap in INC-0142:

```json title="rc1-receive.jsonl" caption="Example dataset, line 1,118, spread over several lines to read"
{
  "recv_utc": "2026-09-24T14:32:03.912Z",
  "mono_ns": 12805145901122,
  "link": "mesh0",
  "sysid": 4,
  "compid": 1,
  "msgid": 0,
  "msg": "HEARTBEAT",
  "seq": 204,
  "len": 9
}
```

| Field             | What it holds                                                      |
| ----------------- | ------------------------------------------------------------------ |
| `recv_utc`        | The arrival time in UTC, on the receiver's clock                   |
| `mono_ns`         | The arrival time on the receiver's monotonic clock, in nanoseconds |
| `link`            | The link the message arrived on                                    |
| `sysid`, `compid` | The sender's MAVLink system and component IDs                      |
| `msgid`, `msg`    | The message ID and name, here 0 and `HEARTBEAT`                    |
| `seq`             | The MAVLink packet sequence number, 0 to 255                       |
| `len`             | The payload length in bytes, 9 for a `HEARTBEAT`                   |

[Receiver JSONL records](/reference/receiver-jsonl) gives the full schema.

## Receive time is not send time [#receive-time-is-not-send-time]

A receipt always comes after the transmission. The record gives the arrival, and the departure was earlier by the link latency. Latency widens the bound on one side only, toward earlier times, because it can never move a transmission after its receipt.

For an event at time `t` with clock error `err` and link latency `lat`, the interval is `[t − err − lat, t + err]`. The heartbeat above arrived at 14:32:03.912, with ±15 ms on the receiver clock and up to 412 ms of latency. It left UAS-04 between 14:32:03.485 and 14:32:03.927.

<Figure caption="The same moment on three clocks, with INC-0142's bounds. The ground receipt lands 96 ms after the moment, the run's median latency, and may have been sent up to 412 ms before it arrived.">
  <ClockBounds />
</Figure>

## Latency per run [#latency-per-run]

Foxborne measures link latency for each run and reports its median and 95th percentile. Bounds carry the 95th percentile.

| Run    | Incident | Median | 95th percentile |
| ------ | -------- | ------ | --------------- |
| R-0931 | INC-0142 | 96 ms  | 412 ms          |
| R-0934 | INC-0143 | 88 ms  | 380 ms          |

The latency follows the receive log wherever it appears. **Source files*&#x2A; gives its bound as **±15 ms + latency**, and **Ordering bounds between sources** adds a line such as **plus 412 ms latency**. On the event page, the **Clock** card's **Receive latency** row reads **Up to 412 ms, 95th percentile**.

Foxborne 1.4.1 added receive log latency estimates from `SYSTEM_TIME`. That MAVLink message, `msgid` 2, carries the sender's own clock: `time_unix_usec`, in microseconds since the UNIX epoch, and `time_boot_ms`, in milliseconds since boot.

## How telemetry\_gap reads the log [#how-telemetry_gap-reads-the-log]

The [telemetry\_gap](/rules/telemetry-gap) rule reads `HEARTBEAT` receipts (`msgid` 0) from each vehicle's system ID. It measures the time between consecutive heartbeats and fires when two are more than 3 s apart.

The finding sits where the next heartbeat was due: one expected interval, 1 s at 1 Hz, after the last receipt. In INC-0142 the receiver heard UAS-04 at 14:32:03.912 and then not until 14:32:10.874.

The finding reads **Heartbeat due and not received. Nothing for 6.96 s**, with the wording **Observed: telemetry receive gap, not necessarily radio failure**.

On the timeline, the **Ground receive** lane draws each receipt as a tick and hatches the gap, labeled **No telemetry received for 6.96 s**.

## Check the result [#check-the-result]

* The source appears in **Connections** with **Healthy** under **Status**.
* Runs gain a **Ground receive log** under **Source files**. Its clock reads **Range control receiver clock**, its anchor **NTP, synchronised** and its parser `receive 0.4.1`.
* The audit log records **Added source** with a detail such as **MAVLink receiver, read-only test passed**.

## Troubleshoot [#troubleshoot]

* **Runs show Partial with No ground receive log for this run.** The receiver did not record that run, or its log never reached Foxborne. Check the source's **Last sync**, and import the log by hand if you have it.
* **A gap appears, and the radio seems fine.** A receive gap is not a radio failure. Compare the vehicle's own view in `telemetry_status` before you conclude anything, as [telemetry\_gap](/rules/telemetry-gap#what-the-finding-does-not-mean) explains.

## Next [#next]

* [telemetry\_gap](/rules/telemetry-gap) describes the rule that reads this log.
* [Receiver JSONL records](/reference/receiver-jsonl) lists every field.
* [Clocks, anchors and error bounds](/concepts/clocks) explains how the three clocks combine.
