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)
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}")
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
)
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()
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
Enable the Service
sudo systemctl daemon-reload
sudo systemctl enable drone_blackbox.service
sudo systemctl start drone_blackbox.service
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)
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
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.













