DEV Community

Cover image for Building a Custom Autonomous Drone Stack - Part 1: The Black Box Flight Recorder
Harsh Pandhe
Harsh Pandhe

Posted on

Building a Custom Autonomous Drone Stack - Part 1: The Black Box Flight Recorder

Why We Ditched MAVROS for Raw PyMAVLink and systemd

If you're building an autonomous UAV, MAVROS is the undisputed industry standard for bridging a companion computer (like a Raspberry Pi or Jetson Nano) to your flight controller (like a Pixhawk). It wraps the MAVLink serial protocol into the ROS ecosystem. For most projects, it's the perfect tool.

But what if you're building a micro-UAV for a strictly GPS-denied environment?

What if your Raspberry Pi needs every ounce of CPU and RAM to run heavy, tightly-coupled LiDAR SLAM algorithms like FAST-LIO?

During the development of our autonomous drone stack, we hit a massive computational bottleneck. We simply couldn't afford to burn precious CPU cycles and deal with middleware overhead just to log telemetry.

We needed a zero-latency "Black Box" flight recorder. It needed to capture Optical Flow drift, Barometer altitude, CPU load, and frame vibrations at a strict 10 Hz frequency so we could mathematically verify stability before autonomous flight.

Our solution?

We bypassed ROS and MAVROS entirely for the telemetry layer and went straight to the metal using raw pymavlink over a blazing 921600 baud serial connection.


The PyMAVLink Interceptor

Instead of wrestling with ROS nodes, topic publishers, and XML configurations, we wrote a lightweight procedural Python script. It opens a direct UART connection to the Pixhawk, intercepts the binary MAVLink stream, and logs only the data we care about directly into a CSV file.

One important detail: flight controllers won't continuously spam data unless you explicitly ask them to.

1. Connect to the Pixhawk 6C

import time
import csv
from pymavlink import mavutil

PORT = "/dev/ttyAMA0"
BAUD = 921600

print(f"Connecting to Pixhawk on {PORT} at {BAUD} baud...")
master = mavutil.mavlink_connection(PORT, baud=BAUD)
Enter fullscreen mode Exit fullscreen mode

We use a 921600 baud rate to avoid bottlenecking the telemetry stream.

2. Wait for the First Heartbeat

master.wait_heartbeat()
print(f"Connected! Target System: {master.target_system}")
Enter fullscreen mode Exit fullscreen mode

This guarantees the Pixhawk is fully booted and speaking MAVLink before we continue.

3. Request All Data Streams at 10 Hz

master.mav.request_data_stream_send(
    master.target_system,
    master.target_component,
    mavutil.mavlink.MAV_DATA_STREAM_ALL,
    10,
    1
)
Enter fullscreen mode Exit fullscreen mode

MAV_DATA_STREAM_ALL tells ArduPilot to actively send telemetry rather than waiting for individual requests.


The Async Logging Loop

By sending request_data_stream_send(), we tell the Pixhawk to start continuously flooding the serial port with MAVLink packets.

Different messages arrive at different frequencies:

  • Attitude → 50 Hz
  • Battery Status → 1 Hz
  • Optical Flow → varies

Writing directly to CSV every time a packet arrives would create inconsistent rows.

Instead, we maintain a state dictionary, continuously update it, and snapshot it every 0.1 seconds.

Simplified Logging Loop

state = {
    "Alt": 0.0,
    "Flow_X": 0.0,
    "Flow_Y": 0.0,
    "Quality": 0
}

last_write = time.time()

while True:
    msg = master.recv_match(blocking=True, timeout=0.1)

    if not msg:
        continue

    msg_type = msg.get_type()

    if msg_type == "VFR_HUD":
        state["Alt"] = msg.alt

    elif msg_type == "OPTICAL_FLOW":
        state["Flow_X"] = msg.flow_comp_m_x
        state["Flow_Y"] = msg.flow_comp_m_y
        state["Quality"] = msg.quality

    if time.time() - last_write >= 0.1:
        write_to_csv(state)
        last_write = time.time()
Enter fullscreen mode Exit fullscreen mode

This keeps all asynchronous packets synchronized into a single telemetry snapshot.


The systemd Autopilot

A flight recorder is useless if:

  • You forget to start it before takeoff.
  • You need to SSH into the drone after every battery swap.
  • The script crashes mid-flight.

To make the logger production-ready, we wrapped it inside a Linux systemd service.

Unlike cron or rc.local, systemd automatically:

  • Starts the logger when the Raspberry Pi boots.
  • Restarts it if it crashes.
  • Runs silently in the background.

Create /etc/systemd/system/drone_blackbox.service

[Unit]
Description=Autonomous Drone Black Box Flight Recorder
After=multi-user.target network.target

[Service]
User=pi
WorkingDirectory=/home/pi/drone_project
ExecStart=/usr/bin/python3 /home/pi/drone_project/drone_blackbox.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
Enter fullscreen mode Exit fullscreen mode

Enable the Service

sudo systemctl daemon-reload
sudo systemctl enable drone_blackbox.service
sudo systemctl start drone_blackbox.service
Enter fullscreen mode Exit fullscreen mode

Your Raspberry Pi is now a professional-grade auto-starting telemetry recorder.


🚨 The Serial Port "Gotcha"

Suppose your logger is already running in the background and you launch another Python script for autonomous takeoff.

Linux will throw:

serial.serialutil.SerialException:
device reports readiness to read but returned no data
(device disconnected or multiple access on port)
Enter fullscreen mode Exit fullscreen mode

A hardware serial port can only be owned by one process at a time.

Option 1

Stop the logger temporarily:

sudo systemctl stop drone_blackbox.service
Enter fullscreen mode Exit fullscreen mode

Option 2

Use a MAVLink router like MAVProxy to split one physical serial connection into multiple virtual UDP ports.


Final Thoughts

By ditching ROS and MAVROS for telemetry logging, we achieved:

  • Lower CPU overhead
  • Near-zero latency
  • Deterministic 10 Hz logging
  • Automatic startup with systemd
  • Improved reliability during indoor autonomous flights

Sometimes the best architecture isn't adding another layer of abstraction.

Sometimes it's removing one.


Up Next

Logging data is easy.

Making the drone fly itself is hard.

In Part 2 of this series, we'll explore:

  • Autonomous takeoff without GPS
  • ArduPilot's EKF3 safety engine
  • State polling and pre-arm checks
  • Safe operation in GPS-denied environments
  • The realities of indoor autonomous flight

Stay tuned.

Top comments (0)