This challenge focuses on reconstructing a UAV pre-arm and mission-control timeline from a single MAVLink telemetry log. We are given only mav.tlog, and our objective is to determine how the autopilot transitioned from boot to mission preparation, why early arming attempts failed, when the motors finally armed, and which autopilot modes appeared in the observable telemetry stream.
This case is a benign flight reconstruction rather than an intrusion. The single mav.tlog capture shows an ArduPilot copter booting, receiving a flight plan, and failing its first arming attempts because it had no position estimate. Once the EKF took GPS as its source and set its origin, the next arm command succeeded in GUIDED mode. Roughly six minutes later the autopilot moved through LOITER and then RTL. Every answer comes from decoding MAVLink messages (HEARTBEAT, STATUSTEXT, COMMAND_ACK, GPS_RAW_INT) and reading their timestamps.
mav.tlog → parse with Python and pymavlink (mavutil.mavlink_connection). A tlog is a stream of MAVLink messages, each stored with a receive timestamp in epoch secondsTimestamp note: Convert the tlog epoch timestamps as UTC (
datetime.utcfromtimestamp). Every timestamp question wantsYYYY-MM-DD hh:mm:ss, so truncate fractional seconds.
Walk the log for COMMAND_LONG command 400 (MAV_CMD_COMPONENT_ARM_DISARM) and its matching COMMAND_ACK. Most acknowledgements carry a failure result; the one with result 0 is the accepted arm, and it lines up with the autopilot’s Arming motors STATUSTEXT. Convert that message’s timestamp to UTC.
for m in mlog:
if m.get_type()=='COMMAND_ACK' and m.command==400: print(utc(m._timestamp), m.result)
The current mode is not a message of its own; it lives in the custom_mode field of the HEARTBEAT. Take the last heartbeat before the Q1 timestamp and map custom_mode through the ArduCopter mode table (4 is GUIDED).
for m in mlog:
if m.get_type()=='HEARTBEAT' and m._timestamp<=arm_ts: mode=m.custom_mode
print(mode)
Tip: The format hint is six characters. Use the ArduCopter name, not the number.
Failures show up in two places: COMMAND_ACK for command 400 with a non-zero result, and PreArm text messages from the autopilot. Two arm commands were rejected with result 4 (Arm: Need Position Estimate), and one further PreArm: Need Position Estimate message was logged. All three count as failed attempts.
for m in mlog:
t=m.get_type()
if t=='COMMAND_ACK' and m.command==400 and m.result!=0: print(utc(m._timestamp), m.result)
if t=='STATUSTEXT' and 'Arm' in m.text: print(utc(m._timestamp), m.text)
Gotcha: Counting only the rejected arm commands gives 2. The autopilot’s own
PreArmmessage is a failed attempt too.
The first message in the file is at 11:21:04. From Q1 the arm is at 11:22:01, so subtract.
print(arm_ts - first_ts)
Take the last GPS_RAW_INT message received before the Q1 timestamp and read its fix_type. The earlier failures were position-estimate problems, so this is the state that finally let the arm through.
for m in mlog:
if m.get_type()=='GPS_RAW_INT' and m._timestamp<=arm_ts: fix=m.fix_type
print(fix)
Filter STATUSTEXT for the Flight plan received message and convert its timestamp.
for m in mlog:
if m.get_type()=='STATUSTEXT' and 'Flight plan' in m.text: print(utc(m._timestamp), m.text)
From Q6 the plan arrived at 11:21:24; from Q1 the arm is at 11:22:01. The difference is the answer.
Search STATUSTEXT for the EKF3 message and take the first hit. The EKF lines repeat per IMU, so match on the exact text.
for m in mlog:
if m.get_type()=='STATUSTEXT' and 'EKF3 IMU0 is using GPS' in m.text: print(utc(m._timestamp)); break
Same search with the EKF3 IMU0 origin set text, which is logged at 11:21:30. Subtract from the Q1 arm time at 11:22:01. Note the origin is set before the “using GPS” message from Q8, not after it.
for m in mlog:
if m.get_type()=='STATUSTEXT' and 'EKF3 IMU0 origin set' in m.text: print(utc(m._timestamp)); break
Collect the unique custom_mode values across every HEARTBEAT from the autopilot. Five values appear (0, 3, 4, 5 and 6), which are STABILIZE, AUTO, GUIDED, LOITER and RTL in the ArduCopter table.
print(sorted({m.custom_mode for m in mlog if m.get_type()=='HEARTBEAT'}))
LOITER is ArduCopter mode 5. Take the first HEARTBEAT whose custom_mode is 5 and convert its timestamp.
for m in mlog:
if m.get_type()=='HEARTBEAT' and m.custom_mode==5: print(utc(m._timestamp)); break
Same approach with mode 6, which is RTL.
for m in mlog:
if m.get_type()=='HEARTBEAT' and m.custom_mode==6: print(utc(m._timestamp)); break
From Q10 and Q11, the first LOITER heartbeat is at 11:27:35 and the first RTL heartbeat at 11:27:47. The gap is the difference of the two.
This is not an intrusion, so the table covers the flight sequence instead.
| Phase | Action |
|---|---|
| Boot | First telemetry message recorded |
| Mission preparation | Flight plan received |
| Pre-arm | Arm commands rejected; autopilot reports it needs a position estimate |
| Pre-arm | EKF3 origin set, then IMU0 switches to GPS |
| Arming | Arm command accepted in GUIDED mode with GPS fix_type 6 |
| Flight | Autopilot enters LOITER, then RTL twelve seconds later |
| Timestamp (UTC) | Event |
|---|---|
| 2026-03-06 11:21:04 | First telemetry message in mav.tlog |
| 2026-03-06 11:21:24 | Flight plan received |
| 2026-03-06 11:21:30 | EKF3 IMU0 origin set |
| 2026-03-06 11:21:55 | EKF3 IMU0 is using GPS |
| 2026-03-06 11:22:01 | Motors armed in GUIDED mode |
| 2026-03-06 11:27:35 | First LOITER heartbeat |
| 2026-03-06 11:27:47 | First RTL heartbeat |
| Type | Value |
|---|---|
| File | mav.tlog |
| Protocol | MAVLink (ArduPilot copter) |
No malicious indicators were present in this case.
| Technique | ID | Description |
|---|---|---|
| None | N/A | Benign telemetry reconstruction; no adversary behaviour to map |
Arming failures are visible in three message types. COMMAND_ACK, STATUSTEXT PreArm lines and HEARTBEAT state each tell part of the story, and counting from only one undercounts attempts. Reconstruct from all of them.
Take timestamps from the log, not the ground station. The tlog stamps every message on receipt, so one parser gives a consistent UTC clock across message types. Convert once, as UTC, and reuse it.
Mode changes need a mapping table. HEARTBEAT.custom_mode is a bare integer whose meaning depends on the vehicle type, so a copter log decoded with the wrong table names the wrong modes. Confirm the vehicle type first.
An unexpected LOITER to RTL sequence is a review trigger. Mode transitions after arming are the signal that matters for investigating a flight, whether they came from the operator, a failsafe or an external command.
The drone armed 57 seconds after the log began, only once the EKF had a GPS-backed position, and finished in LOITER then RTL. A single tlog was enough to rebuild the whole sequence, which is the point: MAVLink telemetry holds the evidence if you decode all of its message types.