# MAVLink telemetry logs and receive records

URL: /collect/formats/mavlink

Two ground-side records of MAVLink traffic: the ground station's .tlog and the Foxborne receiver's JSONL. Both stamp each message when it reached the ground, so both give receive time, never send time.



MAVLink traffic from a vehicle can land in two records on the ground. Ground station software writes a telemetry log, the `.tlog`, and the Foxborne MAVLink receiver at range control writes receiver JSONL. Both are written on the ground as each message arrives, and both say when the ground heard it.

## The two records [#the-two-records]

| Property                | Ground station telemetry log                                  | Receiver JSONL                                                                        |
| ----------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| File                    | `.tlog`                                                       | `.jsonl`                                                                              |
| Written by              | Ground station software                                       | The Foxborne MAVLink receiver                                                         |
| What it holds           | Every MAVLink packet the ground station logged, whole         | One JSON object per message received: its name, its sender and its arrival            |
| Time stamp              | 8 bytes before each packet: microseconds since the Unix epoch | `recv_utc` in UTC to the millisecond, and `mono_ns` on the receiver's monotonic clock |
| Clock                   | The ground station computer's clock                           | The receiver's clock, kept on UTC by NTP                                              |
| How it reaches Foxborne | Listed in the **File import** catalog                         | The **MAVLink receiver** source, a storage source or **Import files**                 |

## Ground station telemetry logs [#ground-station-telemetry-logs]

Ground stations record `.tlog` files of the MAVLink traffic they handle. Mission Planner starts one when it connects, and QGroundControl writes one after each flight when that setting is on.

### Structure [#structure]

A `.tlog` has no header of its own. It is a run of records to the end of the file, each an 8-byte timestamp followed by one MAVLink packet as it arrived.

| Bytes in the record | Field     | Content                                                                                  |
| ------------------- | --------- | ---------------------------------------------------------------------------------------- |
| 0 to 7              | Timestamp | A `uint64`, big endian: microseconds since the Unix epoch, on the ground station's clock |
| 8 to end            | Packet    | The MAVLink packet as received, from its start marker to its checksum                    |

The packet's start marker says which MAVLink version follows, and with it where each field sits:

| Packet field              | MAVLink 1         | MAVLink 2                                                           |
| ------------------------- | ----------------- | ------------------------------------------------------------------- |
| Start marker, byte 0      | `0xFE`            | `0xFD`                                                              |
| Payload length            | Byte 1            | Byte 1                                                              |
| Message ID                | Byte 5, one byte  | Bytes 7 to 9, three bytes                                           |
| Header before the payload | 6 bytes           | 10 bytes                                                            |
| After the payload         | A 2-byte checksum | A 2-byte checksum, then a signature block when the packet is signed |

### The timestamp is receive time [#the-timestamp-is-receive-time]

pymavlink, the MAVLink library for Python, shows where the timestamp comes from. It writes each record right after it parses a packet from the link, with the computer's clock at that moment:

```python title="pymavlink/mavutil.py" caption="pymavlink, mavfile.recv_msg(), abridged"
msg = self.mav.parse_char(s)
if msg:
    if self.logfile and  msg.get_type() != 'BAD_DATA' :
        usec = int(time.time() * 1.0e6) & ~3
        self.logfile.write(struct.pack('>Q', usec) + msg.get_msgbuf())
```

The stamp is therefore the time the ground station logged the packet, on the ground station's clock. It is a receive time, as good as that computer's time synchronization.

pymavlink clears the timestamp's two lowest bits when it writes, and its reader takes those two bits back as a link number. Its stamps land on multiples of 4 µs.

```python title="pymavlink/mavutil.py" caption="pymavlink, mavlogfile.pre_message(), abridged"
tbuf = self.f.read(8)
(tusec,) = struct.unpack('>Q', tbuf)
t = tusec * 1.0e-6
...
self._link = tusec & 0x3
```

### Importing .tlog files [#importing-tlog-files]

The console's **File import** entry, under **Integrations** and **Ingest**, lists `.tlog` among the formats it takes by upload, drop folder or removable media. Each file arrives under a user login with the sender's SHA-256 manifest, and it is checked against that manifest before it is parsed.

**Settings**, under **Import**, lists the formats your deployment reads. In the example deployment, **File import** reads **On, 4 formats**, and `.tlog` is not among them. Its **Formats on** row names PX4 ULog, journalctl JSON, collector JSONL and receiver JSONL.

The **Telemetry gap** alert, R2 under **Alerts**, names its signals as vehicle `HEARTBEAT` arrivals in the ground-station `.tlog` and the ground receiver log. In the example deployment it reads the range control receive logs. **Sortie closeout triage**, automation A1, counts a ground-station `.tlog` among the sources a test card can list.

## Receiver JSONL [#receiver-jsonl]

The Foxborne MAVLink receiver listens for telemetry on the ground and writes one JSON object per receipt, one receipt per line. Foxborne recognizes the file by the key `"recv_utc"` in its first 4,096 bytes, and the example dataset's parser is `receive 0.4.1`.

| 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, such as 0 and `HEARTBEAT`                 |
| `seq`             | The MAVLink packet sequence number, 0 to 255                       |
| `len`             | The payload length in bytes, 9 for a `HEARTBEAT`                   |

In the example dataset, the receiver clock is good to ±15 ms, and the run page gives the bound as **±15 ms + latency**. [Receiver JSONL records](/reference/receiver-jsonl) has the full schema and an example record. [MAVLink ground receiver](/collect/receiver) sets the receiver up.

## Why both are receive time [#why-both-are-receive-time]

A receipt always comes after the transmission, later by the link latency. Both records stamp the arrival, and neither carries the moment the vehicle sent the message.

A `HEARTBEAT` has no time field. The vehicle's own clock reaches the ground only in messages that carry it, such as `SYSTEM_TIME`, `msgid` 2. Its `time_unix_usec` counts microseconds since the Unix epoch, and `time_boot_ms` milliseconds since boot.

Foxborne 1.4.1 added receive log latency estimates from `SYSTEM_TIME`. Foxborne gives each receipt an interval that reaches back by the clock error plus the latency, and forward by the clock error alone:

```text title="Send window of a receipt" caption="Example dataset, INC-0142: ±15 ms clock error, 412 ms latency at the 95th percentile"
interval      = [ t - error - latency , t + error ]
earliest send = 14:32:03.912 - 15 ms - 412 ms = 14:32:03.485
latest send   = 14:32:03.912 + 15 ms          = 14:32:03.927
```

The same holds for a `.tlog` stamp: it marks when the ground station logged the packet, on the ground station's clock.

## HEARTBEAT at 1 Hz [#heartbeat-at-1-hz]

Vehicles send `HEARTBEAT`, `msgid` 0, on a steady schedule. MAVLink's heartbeat service describes the rate as typically 1 Hz on radio links, and treats a system as lost after four or five missed heartbeats.

In the example dataset, UAS-04's heartbeats reach range control about once a second as system ID 4, and UGV-02's as system ID 22. A gap in those receipts is the first sign on the ground that something stopped.

## How telemetry\_gap reads receipts [#how-telemetry_gap-reads-receipts]

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

The finding sits where the next heartbeat was due, one expected interval after the last receipt. It reads **Observed: telemetry receive gap, not necessarily radio failure**.

| Incident | Last received | Next received | Finding title                                          |
| -------- | ------------- | ------------- | ------------------------------------------------------ |
| INC-0142 | 14:32:03.912  | 14:32:10.874  | **Heartbeat due and not received. Nothing for 6.96 s** |
| INC-0143 | 15:41:02.713  | 15:41:33.689  | **Heartbeat due and not received. Nothing for 31.0 s** |

A receive gap proves only that nothing arrived. In INC-0142 the MAVLink router on the companion computer had stopped, and in INC-0143 the radio had dropped off the companion's USB bus.

## Related [#related]

* [Receiver JSONL records](/reference/receiver-jsonl) gives every field of a receive record.
* [MAVLink ground receiver](/collect/receiver) records receipts at range control.
* [telemetry\_gap](/rules/telemetry-gap) explains what a receive gap does and does not mean.
* [Clocks, anchors and error bounds](/concepts/clocks) explains why receive time is not send time.
