Stop Fighting Your Hardware: Why Letting the Flight Controller Do Its Job Changed How I Think About System Design
Admin User
Author
I spent three hours last month debugging a Python script that was supposed to control a robotic arm. The arm would jerk sporadically, miss targets by centimeters, and generally behave like it was actively resisting my commands. Turns out, I was sending conflicting instructions—my high-level logic was fighting the arm's built-in stabilization routine instead of working with it. Reading through a deep dive on autonomous drone control reminded me why this happens so often in embedded systems: we treat specialized hardware like dumb actuators when they're actually sophisticated decision-makers.
The article I came across was about something very specific—keeping a GPS-denied drone hovering indoors using only optical flow and LiDAR. But the real lesson buried in there applies to any system where you've got a capable microcontroller and a companion computer trying to cooperate. The author stumbled onto a problem that probably deserves more attention in our industry: the architecture question of who decides what, and when.
The Problem Nobody Warns You About: Mode Conflicts
Here's the scenario that got my attention. A developer manages to get their drone airborne in GUIDED mode by directly sending attitude commands to bypass ArduPilot's GPS requirement. Smart workaround. But then they switch to LOITER mode expecting stable hovering, and immediately the drone drops like someone cut the strings.
The reason? LOITER is designed as a hybrid mode. It's supposed to blend autonomous stabilization with pilot input. So when the flight controller entered LOITER, it checked the RC transmitter. The throttle stick was at zero—a legacy behavior that meant "descend" to the firmware. The drone obeyed.
This is the kind of bug that makes you want to throw equipment out a window, because the hardware is working perfectly. The bug is purely architectural. Two subsystems with different assumptions about what the current state means. I've seen this exact pattern in production code—different parts of the system building conflicting mental models of reality.
The Elegant Solution: Velocity Commands and Bitmasks
Instead of fighting the firmware's assumptions, the developer stayed in GUIDED mode and used a bitmask-based velocity command. By setting VELOCITY_MASK = 3527, they told the Pixhawk: "Ignore everything except velocity. I'm commanding zero velocity on all axes."
This is genuinely clever. Rather than wrestle with mode semantics, they found a communication protocol that's unambiguous. The flight controller now has a single job: maintain zero velocity. If optical flow detects drift, the EKF3 sensor fusion automatically adjusts motor outputs. No conflicting commands. No guessing about pilot intent.
The velocity-over-position approach also reveals something important about sensor constraints. Position estimates drift indoors without GPS. But velocity is something the flight controller can measure right now through optical flow. That's a much stronger signal to work with.
What Worked, What Didn't, and What I'd Question
I appreciate the architecture here—separating decision-making (companion computer) from execution (flight controller). But I noticed the failsafe implementation, while good instinct, is still incomplete.
Their emergency handler catches exceptions and keyboard interrupts, which covers maybe 70% of real failure modes. What if the serial connection drops? What if the MAVLink message queue backs up? A truly reliable system would also include heartbeat monitoring. If the flight controller stops receiving velocity commands after, say, 500 milliseconds, it should auto-trigger a failsafe regardless of what the companion computer thinks it's doing.
I would also be cautious about how long this approach stays reliable. The continuous velocity hold loop runs every 0.5 seconds. For a proof-of-concept, fine. But in production indoor drone operations with multiple units, I'd want better telemetry on IMU saturation and sensor fusion confidence metrics. The optical flow sensor can be fooled by featureless walls or rapid motion. You need visibility into why the drone is where it is.
The Bigger Lesson
What stuck with me most is this: the moment a system has enough autonomy to make decisions, your job as an engineer shifts. You're no longer trying to micromanage every output. You're negotiating with intelligent hardware.
Stop sending granular commands that fight the controller's internal logic. Instead, define clear boundaries—"maintain this velocity"—and let the hardware do what it's built for: sensor fusion, stabilization, and recovery. Your companion computer focuses on strategy. The flight controller handles tactics.
This principle applies whether you're working with drones, robots, embedded systems, or even distributed backend services. Architecture matters more than you think, and "talking to your hardware in its language" beats "forcing it to listen to yours" every single time.
What Would You Do Differently?
I'm curious how others handle the autonomy handoff in their embedded systems. Would you trust the failsafe approach described here, or do you layer additional safety mechanisms? Drop a note—I'm genuinely interested.
Source: This post was inspired by "Building a Custom Autonomous Drone Stack - Part 3: The Zero-Velocity Hover" by Dev.to. Read the original article