10  PX4 and the Autopilot Stack: MAVLink, MAVSDK, and ROS 2

The previous chapter ended inside the flight controller, with the cascade’s gains living as named parameters — MC_ROLLRATE_P and its kin — in a piece of software it kept calling “the PX4 firmware” without ever justifying the choice. And the Companion Computers chapter of Part I ended at the other side of the wire, with a MAVLink heartbeat arriving over three verified conductors and a promise that Part II would show how a Python program turns that link into an autonomous takeoff. This chapter keeps both promises at once, because they are the same promise: it maps the software stack that stands between a mission script and the four motor commands, layer by layer, and makes the three decisions every autonomy project has to make before writing its first line of flight code. Which autopilot firmware runs the loops — PX4 or ArduPilot, a choice that turns out to hinge on law as much as engineering? Which programming layer does the mission code use to command it — MAVSDK, a library that makes a drone feel like an object with methods, or ROS 2, a middleware that makes it one participant in a graph of cooperating processes? And where does the code fly first — because the correct answer is nowhere, until the identical firmware has flown the identical mission in simulation.

The stakes of the chapter are inherited directly from the one before. The cascade of PID loops is superb at what it does and completely indifferent to why: it will track a setpoint into the ground as faithfully as into a perfect hover. Everything that makes the running example a system rather than a stabilizer — the search, the detection, the deterrence — enters the flight controller from outside, as messages on the wire Part I installed. This chapter is about the grammar of those messages and the software that speaks it.

10.1 Learning Objectives

  • Compare PX4 and ArduPilot as autopilot firmware, and explain concretely what the BSD 3-clause and GPL v3 licenses each oblige a team to do when it modifies the firmware and distributes vehicles containing it — and why neither license reaches mission code on the companion computer.
  • Describe MAVLink’s role as the common protocol among flight controller, ground station, and companion computer, and distinguish its two idioms: streamed telemetry and acknowledged commands.
  • Write and explain a minimal MAVSDK mission — connect, verify, arm, take off, land — and identify the class of project for which MAVSDK is the right programming layer.
  • Explain how ROS 2 talks to PX4 without MAVLink, through uORB topics exposed across the Micro XRCE-DDS bridge, and why the mission-side architecture becomes a timer-driven node rather than a linear script.
  • State Offboard mode’s two hard rules — stream setpoints before entering, keep streaming above about 2 Hz to stay — and justify them as a dead-man’s switch protecting the vehicle from its own companion computer.
  • Explain why “simulate, then fly” is standard practice: PX4 SITL runs the identical firmware against a modeled plant, so the mission code that survives Gazebo is byte-for-byte the code that flies.

10.2 Key Vocabulary

  • PX4 — an open-source autopilot firmware, licensed BSD 3-clause, that implements the estimator, the cascaded control loops, the flight modes, and the failsafe logic on Pixhawk-class flight controllers; the firmware this book’s platform runs.
  • ArduPilot — the other major open-source autopilot firmware, licensed GPL v3, older and more extensively documented than PX4, with a faster-moving but historically rockier release cadence.
  • MAVLink (Micro Air Vehicle Link) — the lightweight binary message protocol spoken among flight controllers, ground stations, and companion computers: framed packets carrying typed payloads, addressed by system and component ID, over any serial or network transport.
  • QGroundControl — the cross-platform ground-control station developed alongside PX4: mission upload, live telemetry, parameter editing, sensor calibration, and firmware flashing through one MAVLink-speaking interface.
  • Mission Planner — ArduPilot’s companion ground-control station, Windows-first and denser, exposing more of the firmware’s features at the cost of a steeper interface.
  • MAVSDK — a library (with Python, C++, and other language bindings) that wraps MAVLink in high-level calls — arm(), takeoff(), land() — so a single linear program can command a vehicle without composing protocol messages by hand.
  • ROS 2 (Robot Operating System 2) — the middleware, introduced with the running example’s architecture at the close of Part I, in which independent nodes exchange typed messages over named topics and services; here, the scalable alternative to MAVSDK for mission software.
  • uORB — PX4’s internal publish–subscribe message bus, over which the firmware’s own modules (estimator, controllers, commander) exchange data; the DDS bridge exposes these same topics to ROS 2 rather than translating them.
  • Micro XRCE-DDS — the bridge protocol between PX4 and ROS 2: a lightweight client inside the firmware serializes uORB topics across the UART to an agent process on the companion computer, which republishes them as ordinary ROS 2 topics.
  • Offboard mode — the PX4 flight mode in which the autopilot follows position, velocity, or attitude setpoints streamed by an external computer, entered only while a setpoint stream is already flowing and abandoned, in favor of a failsafe behavior, the moment the stream stalls.
  • SITL (Software-in-the-Loop) — running the unmodified autopilot firmware as a desktop process against a physics simulator such as Gazebo, so that missions, modes, and failsafes can be exercised — and crashed — with no aircraft at risk.

10.3 Choosing the Firmware: PX4 versus ArduPilot

The flight controller Part I selected is a piece of hardware; what the previous chapter analyzed — the estimator, the cascade, the parameters — is firmware running on it, and that firmware is a choice. Two open-source projects dominate the space, and the striking thing about the comparison is what it is not about. Both PX4 and ArduPilot are mature, both fly multirotors, fixed-wings, and stranger vehicles, both implement an EKF-based estimator and a cascaded control architecture of exactly the shape the previous chapter derived, and both are trusted by commercial operators. A team choosing between them on control performance is choosing on noise. The real differences are a software license, a ground station, and a development culture — and the first of these deserves more attention than engineering teams usually give it, because it is the one difference that can bind a company years later. Figure 10.1 lays the comparison out.

Comparison chart with two columns. The left column header reads PX4, BSD 3-Clause, permissive; the right reads ArduPilot, GPL v3, copyleft. Four rows compare them. License terms: PX4 modifications may stay private, the obligation is attribution not disclosure; ArduPilot distributing modified firmware obliges you to offer your source to every recipient. Ground station: QGroundControl, cross-platform, cleaner interface, fewer knobs, versus Mission Planner, Windows-first, denser interface, more features exposed. Documentation: PX4 has a thinner record of prior projects to crib from; ArduPilot has extensive deep documentation from a longer hobbyist and survey lineage. Update history: PX4 has a reputation for cleaner, more conservative release patches; ArduPilot has faster-moving releases with a rockier patch history. A bottom panel titled the licensing consequence drawn out shows a box labeled proprietary firmware change, for example a custom control module, with two arrows: one labeled in PX4 leading to a teal box reading BSD, sell the vehicle, keep the source, competitors never see the modification; the other labeled in ArduPilot leading to an amber box reading GPL, sell the vehicle, publish the change, every recipient is entitled to the modified source and may redistribute it, including to competitors. A closing line notes that neither obligation touches mission code on the companion computer, since a separate program talking over a serial link is not a derivative work of the firmware under either license.
Figure 10.1: The PX4 versus ArduPilot decision: identical control theory, different licenses, ground stations, and release cultures — with the licensing consequence drawn out for a team that modifies the firmware and ships vehicles containing it.

10.3.1 What the licenses actually oblige

The source-of-record difference is one line — PX4 is licensed under the BSD 3-clause license, ArduPilot under the GPL v3 — and the line is easy to read past, because both licenses grant the same generous core: anyone may use, study, modify, and redistribute the code, free of charge, forever. The divergence is entirely about what happens to modifications, and it only activates at a specific, well-defined moment: distribution. A team that modifies either firmware and flies it on its own aircraft owes nobody anything, under either license. The obligations attach when copies of the firmware leave the team’s hands — most commonly, when it sells or delivers vehicles with the modified firmware flashed aboard.

At that moment the two licenses part company. The BSD 3-clause license is permissive: it asks that the copyright notice and license text be preserved, and that the project’s name not be used to endorse the derivative — and that is the entire bill. The modification itself may remain a trade secret. A drone manufacturer can develop a proprietary control module inside PX4, ship a thousand vehicles running it, and never show a line of it to anyone; this is precisely why a long roster of commercial autopilot vendors builds on PX4, and why the Dronecode ecosystem around it is structured as an industry consortium. The GPL v3 is copyleft: distributing a binary built from modified ArduPilot obliges the distributor to offer the corresponding source — the modification included — to every recipient, under the same GPL terms, which means every recipient is in turn free to redistribute it to the world. A proprietary firmware advantage built on ArduPilot is therefore, structurally, a contradiction: the act of selling the product is the act of publishing the advantage.

Neither position is “better”; they encode different theories of how a commons should grow. Copyleft ensures improvements flow back — one reason ArduPilot’s feature set and vehicle support are so broad is that two decades of forks were legally obliged to remain open. Permissiveness ensures companies can invest without forfeiting their differentiation — one reason PX4 attracts commercial teams whose autonomy stack is their product. What a team must not do is discover the difference after the fact. The scenario that matters is concrete: a startup builds custom autonomy behavior into the firmware itself — a tighter rate controller, a proprietary failure-detection module — and later takes on customers. On PX4, that intellectual property is theirs to keep. On ArduPilot, their first shipment is a publication event.

One boundary of the obligation matters just as much and is drawn at the bottom of Figure 10.1: the license governs the firmware, not the system around it. Mission code running on the companion computer — a MAVSDK script, the ROS 2 nodes of the running example — is a separate program communicating with the firmware over a serial protocol, not a derivative work of it, and neither license reaches across that link. A team can keep its entire autonomy stack proprietary above a GPL autopilot. The license question bites only those who modify the autopilot itself — which is exactly why the architecture this book builds does all of its innovating on the companion-computer side of the wire.

10.3.2 Tooling and culture

The second difference a team lives with daily is the ground station. QGroundControl grew up alongside PX4: cross-platform, comparatively clean, organized around the workflow of flying — mission upload, live telemetry, parameter editing, sensor calibration, firmware flashing. Mission Planner grew up alongside ArduPilot: Windows-first, denser, and famously feature-rich, exposing more of the firmware’s considerable surface area at the cost of a steeper interface. Both speak MAVLink, and either can talk to either firmware in a pinch, but each is at home with its own. The remaining differences are cultural and worth stating as tendencies rather than laws: ArduPilot, the older project, carries deeper documentation and a longer public record of prior builds to learn from, while PX4 has a reputation for cleaner, more conservative release engineering — patches that arrive less often and break less.

This book’s platform runs PX4, and the reasons can now be stated honestly rather than by fiat. The permissive license matches the book’s premise — a team whose value lives in custom autonomy software. QGroundControl is the tool the coming chapters use for parameter work and mission upload. And one reason outranks the others for what follows: PX4’s first-class ROS 2 integration, which gives the running example’s node graph a direct line into the autopilot that ArduPilot’s MAVLink-translation route does not match. Everything the previous chapter said about the cascade still holds verbatim — PX4’s parameters (MC_ROLLRATE_P, MPC_Z_VEL_P_ACC, and hundreds more) are those loops’ gains, stored on the flight controller, inspected and changed live from QGroundControl without recompiling anything. The firmware is chosen; the question becomes how to talk to it.

10.5 MAVSDK: Flying in a Dozen Lines

Raw MAVLink is a wire format; nobody should hand-assemble packet bytes to fly an aircraft. MAVSDK is the standard library answer: language bindings (Python here; C++, Swift, and others exist) that wrap the protocol in the vocabulary a mission author actually thinks in. The vehicle becomes an object; flying becomes method calls. Here is the entire program that takes the running example’s aircraft off the ground and brings it back — connecting over precisely the serial device and baud rate the Companion Computers chapter wired and verified:

import asyncio
from mavsdk import System

async def run():
    drone = System()
    # The same UART link the heartbeat test proved, wrapped by MAVSDK
    await drone.connect(system_address="serial:///dev/ttyTHS1:921600")

    # The same liveness check as the heartbeat test, far less code
    async for state in drone.core.connection_state():
        if state.is_connected:
            break

    await drone.action.arm()
    await drone.action.takeoff()
    await asyncio.sleep(10)      # hover
    await drone.action.land()

asyncio.run(run())

The program is linear — do this, then this, then this — and Python’s asyncio machinery is what lets a linear story coexist with a fundamentally asynchronous world: each await yields until the underlying MAVLink exchange completes, so await drone.action.arm() does not return until the autopilot has actually acknowledged the arm command, the acknowledged-command idiom of the previous section surfacing as ordinary control flow. The connection_state loop is worth a second look, because it is the heartbeat test of Part I reborn as three lines of code: the program refuses to proceed until MAVLink heartbeats prove the far end is alive. The discipline is identical; only the notation changed. (One implementation note for the curious: the Python binding delegates protocol work to a small native server process it launches automatically — invisible in normal use, but the explanation for the extra process a ps will show.)

What MAVSDK is for follows from what the example shows. For single-vehicle missions — waypoint surveys, scripted photography passes, the take-off-fly-land shape of most first projects — it is close to ideal: minutes from idea to flying code, no infrastructure, no middleware to configure. Its plugins (action, telemetry, mission, offboard) cover the autopilot’s whole public surface. What it is not built for is the thing the running example needs: a system in which perception, navigation, deterrence, and safety monitoring are separate concerns producing and consuming live data streams. A MAVSDK program is one process; as the mission grows, everything — the camera driver, the detector, the state machine, the failsafe checks — must grow inside that one process, and the modularity argument the closing chapter of Part I made (a crash in one function must not take down the safety monitor) has no purchase. The limitation is architectural, not a missing feature, and it is the doorway to ROS 2.

10.6 ROS 2: The Middleware Path

Part I’s closing chapter already introduced ROS 2’s vocabulary on the way to sketching the running example’s architecture: independent nodes, each one process with one job; topics, named publish–subscribe channels for streaming data; services, request–response channels for acknowledged actions; typed messages on all of them. That sketch stands; this section adds the one piece it deferred — how a graph of ROS 2 nodes actually reaches the autopilot.

The answer is unexpectedly elegant: it doesn’t translate. PX4 is itself, internally, a publish–subscribe system — its modules exchange data over an in-firmware message bus called uORB, with topics for vehicle status, local position, trajectory setpoints, and everything else the firmware knows. Rather than converting uORB traffic to MAVLink and back, PX4 embeds a Micro XRCE-DDS client: a lightweight process in the firmware that serializes selected uORB topics across the UART to an agent running on the companion computer, which republishes them as native ROS 2 topics. From a mission node’s point of view, the autopilot’s internals simply appear in the ROS 2 graph, under a reserved namespace: subscribe to /fmu/out/vehicle_local_position and the EKF2 estimate of the previous chapter streams into your callback; publish to /fmu/in/trajectory_setpoint and the cascade’s outermost loop consumes your setpoint directly. There is no protocol seam for a concept to be lost in — the position estimate a ROS 2 node receives is the same message, in the same type, that PX4’s own position controller reads. Figure 10.3 sets the two programming layers side by side.

Two-panel comparison. Left panel, MAVSDK, the direct path: one linear script with steps in order, each awaited — connect to the serial link, then wait for a heartbeat, then arm, takeoff, hover, land — with an arrow labeled MAVLink messages over the UART into a PX4 flight controller box that translates each command internally. Best for: single-vehicle missions and waypoint work, quick prototyping, projects where the drone is the whole system. Limitation: not built for multi-sensor fusion, robot-to-robot communication, or large modular architectures; the program grows monolithic as the mission grows. Right panel, ROS 2, the middleware path: independent nodes coupled only by named topics. A camera node feeds an image topic to a detection node, which publishes a target position topic to a mission node, which publishes fmu in trajectory setpoint to a Micro XRCE-DDS agent box that bridges ROS 2 topics to PX4 with no MAVLink, then uORB topics over the UART into a PX4 flight controller whose DDS client exposes its internal topics directly. A separate failsafe node subscribes to the same topics and can override any node. Best for: multi-sensor multi-subsystem pipelines, perception feeding flight decisions directly, anything needing modularity and reuse. Limitation: steeper learning curve and real infrastructure — bridge, message packages, QoS profiles — to stand up before anything flies. Caption: the running example is built on the right-hand path, because its detection, navigation, and failsafe functions are separate processes that must exchange live data, exactly the architecture MAVSDK is not built to carry.
Figure 10.3: MAVSDK and ROS 2 compared as programming layers: a linear awaited script speaking MAVLink, against a graph of independent nodes whose setpoints cross the Micro XRCE-DDS bridge into PX4’s own uORB topics — with the strengths and limits of each, and the running example committed to the graph.

The comparison in the figure is a genuine trade, not a ranking. ROS 2 costs more before the first flight: a bridge agent to run, message packages to install, quality-of-service profiles to match, and a programming model — covered below — that is less natural than a script. It pays that cost back the moment the mission stops being one program’s story. Computer-vision output feeding flight decisions, multiple sensors fused on the companion computer, behaviors developed and tested as separate processes, a safety monitor that must not share a failure mode with the logic it supervises — each of these is a topic-and-node shape, and the running example needs all four. That is why the Goose Management System is built on ROS 2 rather than on MAVSDK, exactly as Part I’s closing sketch assumed: its mission sequencing, vehicle interfacing, navigation and deterrence behaviors, and Failsafe node are separate processes wired by the Services-for-actions, Topics-for-streams rule that chapter established. The full graph — every node, channel, and authority-transfer rule — is the business of the Systems Architecture chapter in Part V; this chapter’s job is the layer underneath it: how any one of those nodes commands flight at all.

10.7 Offboard Mode: A Dead-Man’s Switch

PX4 has many flight modes — manual stabilization, altitude hold, position hold, autonomous mission, return-to-launch — and one of them is the entire reason this book’s architecture works. In Offboard mode, the autopilot follows setpoints streamed by an external computer: position, velocity, or attitude targets, consumed by the cascade of the previous chapter at whatever loop the setpoint type addresses. Offboard is the handoff Part I’s closing chapter described — from this mode onward, a process is flying the aircraft — and PX4 makes it deliberately the hardest mode to stay in, because an external computer is the least trustworthy source of commands the autopilot ever accepts. A pilot’s sticks fail rarely and visibly; a Python process on a companion computer can hang, crash, or deadlock silently at any moment, and the autopilot has no way to inspect its health. So PX4 imposes two hard rules, both enforced by the firmware rather than by convention:

  1. Stream before you enter. PX4 refuses to switch into Offboard mode until setpoints are already arriving. A computer that cannot yet produce a steady stream has no business being handed the aircraft.
  2. Keep streaming to stay in. If the setpoint stream stalls — drops below roughly 2 Hz — PX4 concludes the companion computer has died, exits Offboard on its own authority, and executes a failsafe behavior instead of coasting on the last command it heard.

Both rules are one idea: a dead-man’s switch. The stream itself is the proof of life, and the proof must be continuous. This design converts the scariest failure in the system — autonomy software dying mid-flight — from a catastrophe into a mode change, and it does so without trusting a single line of companion-computer code. It also dictates, completely, the shape of the mission software: any program that flies in Offboard mode must be built around something that publishes a setpoint on a reliable clock, no matter what else the program is doing. A linear script structure fights that requirement; a timer-driven one embodies it.

10.8 A Complete Mission Node: Flying a Square

The shape becomes concrete in a full ROS 2 mission: take off, fly a square, land — the same flight a MAVSDK script would express in fifteen sequential lines, rebuilt around the streaming obligation. The rebuild is worth every line, because this node’s skeleton (setup once, then a 10 Hz callback that consults a few state flags and does exactly one thing per tick) is the skeleton of every Offboard node in the rest of this book, the running example’s included.

The flight plan itself is plain data — a Python list the loop will walk:

# Each waypoint: (x, y, z, hold_seconds, label)
# NED frame, metres, origin where the EKF started.
# z is negative-up: -5.0 means 5 m altitude.
TRAJECTORY = [
    ( 0.0,  0.0, -5.0, 10.0, 'takeoff_climb'),
    ( 0.0,  0.0, -5.0,  5.0, 'takeoff_settle'),
    (10.0,  0.0, -5.0, 12.0, 'corner_1'),
    (10.0, 10.0, -5.0, 12.0, 'corner_2'),
    ( 0.0, 10.0, -5.0, 12.0, 'corner_3'),
    ( 0.0,  0.0, -5.0, 12.0, 'corner_4_return'),
    ( 0.0,  0.0, -5.0,  5.0, 'pre_land_settle'),
]
CMD_NAV_LAND = 21   # raw MAVLink command id for auto-land

Three conventions in ten lines deserve flagging. Coordinates are NED — x north, y east, z down, so altitude is negative, and -5.0 means five metres up; the frame conventions behind that choice get a chapter of their own in Part IV, but the sign discipline starts now, because the single most common simulator “crash” in a first Offboard mission is a positive z commanding five metres underground. The origin is wherever the EKF initialized — the launch point, echoing the barometer-reference discussion of Part I. And each waypoint carries a hold time rather than an arrival test: progression will be purely time-based, the simplest scheme that can work, with its obvious weakness (the vehicle advances on schedule whether or not it arrived) left for the state-machine treatment in the next chapter to repair.

The node’s constructor wires it to PX4’s topics and starts the clock:

class OffboardSquare(Node):
    def __init__(self):
        super().__init__('offboard_square')
        qos = QoSProfile(reliability=BEST_EFFORT,
                         durability=TRANSIENT_LOCAL,
                         history=KEEP_LAST, depth=1)
        self.mode_pub = self.create_publisher(
            OffboardControlMode, '/fmu/in/offboard_control_mode', 10)
        self.sp_pub = self.create_publisher(
            TrajectorySetpoint, '/fmu/in/trajectory_setpoint', 10)
        self.cmd_pub = self.create_publisher(
            VehicleCommand, '/fmu/in/vehicle_command', 10)
        self.create_subscription(
            VehicleLocalPosition, '/fmu/out/vehicle_local_position',
            self.on_pos, qos)
        self.timer = self.create_timer(0.1, self.loop)   # 10 Hz

The quality-of-service profile is the one genuinely ROS 2-specific trap here. PX4 publishes its topics best-effort — the streaming contract, where a lost message is cheaper than a retransmission — and ROS 2 only connects a publisher and subscriber whose QoS policies are compatible. A subscriber left at the default reliable policy is not degraded; it is silent, receiving nothing, with no error raised. Every integration checklist for this stack includes “match PX4’s QoS” for the same reason every serial checklist in Part I included “match the baud rate”: both are timing-and-contract agreements between parties who cannot see each other’s configuration, and both fail quietly. The 10 Hz timer is the node’s heart, chosen to clear PX4’s roughly-2 Hz Offboard floor with five-fold margin.

The dead-man’s obligation gets its own method, called on every tick that the node wants PX4 to keep trusting it:

def publish_setpoint(self, x, y, z):
    # Heartbeat AND setpoint stream together at >= 2 Hz,
    # or PX4 refuses to enter (or stay in) Offboard mode.
    m = OffboardControlMode()
    m.timestamp = self.now()
    m.position = True                  # commanding by position
    self.mode_pub.publish(m)

    sp = TrajectorySetpoint()
    sp.timestamp = self.now()
    sp.position = [float(x), float(y), float(z)]
    sp.yaw = 0.0
    self.sp_pub.publish(sp)

Two messages ship together: the OffboardControlMode heartbeat, which tells PX4 what kind of setpoint to expect (position = True — the outermost loop of the cascade, exactly where the previous chapter said the mission layer plugs in), and the TrajectorySetpoint itself. The remaining logic is the 10 Hz callback, a handful of flags deciding which one thing this tick does:

def loop(self):
    self.counter += 1

    if self.land_sent:                     # landing: stay silent
        return

    if not self.armed_sent:                # warm-up, then arm
        self.publish_setpoint(0.0, 0.0, -5.0)
        if self.first_pos_tick is None:    # PX4 not talking yet
            return
        if self.counter - self.first_pos_tick < 10:
            return                         # stream for >= 1 s first
        self.send_cmd(VEHICLE_CMD_DO_SET_MODE, 1.0, 6.0)  # Offboard
        self.send_cmd(VEHICLE_CMD_COMPONENT_ARM_DISARM, 1.0)
        self.armed_sent = True
        self.wp_start_tick = self.counter
        return

    if self.wp_index < len(TRAJECTORY):    # fly the square
        x, y, z, hold, label = TRAJECTORY[self.wp_index]
        self.publish_setpoint(x, y, z)     # re-stream active target
        if (self.counter - self.wp_start_tick) * 0.1 >= hold:
            self.wp_index += 1
            self.wp_start_tick = self.counter
        return

    self.send_cmd(CMD_NAV_LAND)            # hand off to PX4's landing
    self.land_sent = True

Read the branches in mission order and each one is a rule from earlier in the chapter made executable. The warm-up branch streams setpoints before attempting anything — rule one — and goes further: it waits for the first VehicleLocalPosition message (proof PX4 is talking at all), then keeps streaming a full second more so the autopilot sees an established stream, and only then sends the mode change and the arm command. Arming earlier simply fails, and the failure is PX4 doing its job. The mode-change parameters (1.0, 6.0) say “custom mode, main mode six” — Offboard, in PX4’s numbering. The flying branch re-publishes the active waypoint every tick — rule two, the stream never pauses just because the target hasn’t changed — and advances on elapsed hold time. And the landing handoff inverts everything: the node sends NAV_LAND once, latches land_sent, and then deliberately falls silent. The silence is not sloppiness; it is the safest line in the program. PX4’s own landing logic — tuned descent rate, ground detection, auto-disarm — is more trustworthy than anything this node could stream, and a node that kept heartbeating could allow the autopilot to be flipped back into Offboard, fighting the landing it just requested. The dead-man’s switch is symmetric: the way software hands back control is to stop proving it wants control.

That is the complete pattern — data, wiring, heartbeat, flags — and it scales. Replace the trajectory list with a search-grid generator and the time-based advance with position-error tests and a detection subscriber, and this node grows up into the running example’s navigation behavior; keep the skeleton and swap the branch logic, and it becomes any Offboard behavior at all.

10.9 Simulate, Then Fly

Everything above can be typed, and none of it should be trusted. The mission node has a dozen ways to be subtly wrong — a positive z, a QoS mismatch, arming before the stream is warm, heartbeats that continue through the landing — and several of them end with an aircraft on its back. The remedy is the last of this chapter’s three decisions, and the only one with a unanimous answer: PX4 SITL, software-in-the-loop simulation, shown in Figure 10.4.

Two-row diagram. Top row, on the bench, SITL, everything is a process on one Linux machine: a mission code box, the same ROS 2 node or MAVSDK script unchanged, connects over a local socket to a PX4 firmware box, the flight code compiled for the laptop instead of the Pixhawk with the same EKF2, loops, modes, and failsafes, which exchanges motor commands and simulated sensor data with a Gazebo simulator box providing modeled physics, wind, and a synthetic IMU, GPS, and barometer, where a crash costs a restart. A dashed arrow between rows reads: when the mission survives simulation, swap only the plant. Bottom row, in the field, the two computers and three wires of Part I: the same mission code byte-for-byte, now on the Jetson Nano, connects over the UART at 921,600 baud to the same PX4 firmware now on the Pixhawk 6C's microcontroller, which exchanges motor commands and real sensor data with the 2.2 kilogram airframe — real physics, real wind, real sensors, where a crash costs the aircraft. Caption: a bug caught in the top row costs nothing; the identical bug in the bottom row costs a drone.
Figure 10.4: Software-in-the-loop: the identical mission code and identical PX4 firmware run on the bench against Gazebo’s modeled physics and synthetic sensors, and in the field against the real airframe — only the plant is swapped, so a bug caught in simulation is the same bug that would have cost the aircraft.

The load-bearing word in the figure is identical. SITL is not a model of the autopilot; it is the autopilot — the same PX4 source, with the same EKF2, the same cascaded loops, the same mode logic, the same Offboard dead-man’s switch and failsafe behaviors, compiled for a desktop processor instead of the Pixhawk’s microcontroller. What is simulated is only the plant: Gazebo supplies the physics — rigid-body dynamics, propeller thrust, wind if asked — and synthesizes the sensor readings (IMU, GPS, barometer) that the real airframe would produce, closing the same sense–think–act loop Part I drew, entirely in software. The mission node connects to this simulated vehicle exactly as it would to the real one and cannot tell the difference; when the mission finally flies, nothing about the software changes — the plant is swapped under it, and that is all.

That identity is what gives simulation its teaching and its safety value at once. The mistakes listed above are not hypothetical; every one of them is a rite of passage, and in SITL each produces its instructive failure — the underground setpoint, the silent QoS mismatch, PX4 curtly rejecting a premature arm — at a cost of one restart. The economics are stark and settle the practice: a bug caught in Gazebo costs nothing, and the same bug on hardware costs a drone — and Part I priced the drone. Standing the environment up (building PX4 SITL, installing the ROS 2 bridge packages, driving Gazebo’s worlds) and flying real waypoint missions in it is the opening business of the next chapter, and this chapter’s related lab walks the same ground; the habit, though, begins here, because it is a design principle rather than a tool: no Offboard code touches the aircraft until the identical code has flown the identical mission in simulation. The running example honors it throughout — every behavior in the chapters ahead debuts in Gazebo, and the field flights of Part V fly code that simulation has already tried to break.

The stack is now complete from mission to motor. PX4 was chosen with reasons, MAVLink read as a protocol rather than folklore, the two programming layers weighed and the middleware path taken, Offboard mode’s contract turned into a working node, and a place established where all of it can fail safely. What the mission node still lacks is judgment: its square is flown on a timer, blind to whether the vehicle actually reached anything, and its plan cannot respond to the world at all. Giving mission logic that judgment — waypoints with arrival criteria, state machines that branch on events, and the SITL environment to prove them in — is the next chapter’s work.

10.10 Check Your Understanding

  1. A startup modifies autopilot firmware with a proprietary fault-detection module and sells 200 vehicles with the firmware flashed aboard. State its source-code obligations if the base firmware was PX4, and if it was ArduPilot. Then explain why the startup’s ROS 2 mission stack, running on the companion computer, is unaffected in both cases.

On PX4 (BSD 3-clause), the obligations are to preserve the copyright notice and license text and not use the project’s name for endorsement; the module itself may remain secret. On ArduPilot (GPL v3), selling the vehicles is distribution, which obliges the startup to offer the corresponding source — the fault-detection module included — to every recipient under the GPL, and recipients may redistribute it further. The ROS 2 stack is unaffected because it is a separate program communicating with the firmware over a serial protocol, not a derivative work of the firmware; neither license’s terms reach across that process boundary. Note also that a team that modifies either firmware but never distributes it — flying only its own aircraft — owes nothing under either license, because the obligations attach at distribution.

  1. PX4 refuses to enter Offboard mode until setpoints are already streaming, and drops out of Offboard if the stream falls below roughly 2 Hz. Explain the single design idea behind both rules, and why the square-mission node goes silent — stops publishing entirely — after sending NAV_LAND.

Both rules implement a dead-man’s switch: the setpoint stream is the companion computer’s continuous proof of life, and PX4 will neither accept nor retain authority from a computer that cannot maintain it, because a hung or crashed process cannot be detected any other way. Requiring the stream before entry proves the computer is healthy before it is trusted; requiring it continuously converts a mid-flight software death into a mode change and failsafe rather than a coast on stale commands. The silence after NAV_LAND is the same mechanism used in reverse: the safest descent is PX4’s own landing logic, and by stopping its stream the node guarantees it cannot be flipped back into Offboard mid-landing — software relinquishes control by ceasing to prove it wants control.

  1. In the mission’s trajectory list, one waypoint reads (10.0, 10.0, -5.0, 12.0, 'corner_2'). Decode all five fields, state the side length and altitude of the square being flown, and predict what happens on the first flight of a student who writes 5.0 instead of -5.0 for the altitude.

The fields are x = 10.0 m north of the EKF origin, y = 10.0 m east, z = −5.0 m in the NED (north-east-down) frame — 5 m above the origin, since z points down — held for 12.0 seconds, with a human-readable label. The square is 10 m on a side, flown at 5 m altitude, with the origin at the launch point where the EKF initialized. Writing +5.0 commands a position 5 m below the origin: the vehicle attempts to descend through the ground. In SITL this produces the classic first-mission crash into the floor of the Gazebo world at a cost of one restart — which is precisely the class of sign error the simulate-then-fly rule exists to catch before hardware is involved.

  1. Two projects need mission software: (a) a bridge-inspection drone that flies a pre-planned path and records video, operated by one script on the companion computer; (b) the running example, where a detector’s output must alter flight behavior in real time and a failsafe monitor must survive a crash of the mission logic. Choose the programming layer for each and give the deciding criterion.

Project (a) fits MAVSDK: a single vehicle flying a linear mission is exactly the one-process, one-story shape a MAVSDK script expresses in a few dozen lines, with no middleware to stand up. Project (b) needs ROS 2: perception feeding flight decisions requires live typed data streams between separate concerns, and a failsafe monitor that must not share a failure mode with the mission logic must be a separate process — both are node-and-topic shapes that a single monolithic script cannot provide. The deciding criterion is architectural: whether the drone is the whole system (MAVSDK) or one participant in a multi-process system exchanging live data (ROS 2). Convenience favors MAVSDK; modularity, and safety by process isolation, force ROS 2.

  1. A student’s mission node runs without errors, but its position callback never fires and PX4 rejects every arm attempt. The bridge agent is running and the heartbeat test from Part I passes. Diagnose the most likely fault, and explain why this failure — like a baud-rate mismatch — produces silence rather than an error message.

The signature fault is a quality-of-service mismatch: PX4 publishes its /fmu/out/... topics best-effort, and a subscription created with ROS 2’s default reliable policy is incompatible, so the middleware never connects the pair — no messages arrive, and no error is raised, because unmatched QoS is a legal configuration, not a fault. With no VehicleLocalPosition received, the node’s warm-up gate (first_pos_tick is None) never opens properly and its arm attempt comes before PX4 sees a warm stream, so PX4 correctly rejects it. The failure is silent for the same structural reason baud mismatch is: it is a contract disagreement between two parties who cannot inspect each other’s configuration — the wire (or middleware) faithfully does what both sides asked, and what they asked for happens to be nothing. The fix is to subscribe with a PX4-compatible profile (best-effort reliability).

  1. SITL is sometimes described as “testing against a model of the autopilot.” Explain precisely what is and is not modeled in PX4 SITL, and why the distinction determines which classes of bug simulation can catch.

The autopilot is not modeled — SITL compiles the actual PX4 source, so the estimator, cascaded loops, mode logic, Offboard entry rules, and failsafe behaviors are the real code, bit-for-bit the logic that flies. What is modeled is the plant: Gazebo supplies rigid-body physics, propeller forces, and synthetic IMU, GPS, and barometer readings in place of the real airframe and sensors. Consequently SITL reliably catches every bug that lives in software and its contracts — sign errors in NED coordinates, QoS mismatches, arming before the stream is warm, heartbeats that continue through landing, state-machine logic errors — because the code paths involved are identical to flight. What it cannot fully catch are plant-fidelity issues: real vibration corrupting the IMU, real battery sag, wind beyond the model, sensor noise the synthesis underestimates. Simulation removes the software unknowns so that the first hardware flight risks only the physics.

10.11 Try It

Part 1 — dry-run the mission loop with no ROS 2 installed. The loop() method’s branch logic is plain Python, and its correctness is testable on any machine: write a harness that instantiates the flags (counter, first_pos_tick, armed_sent, wp_index, wp_start_tick, land_sent), stubs publish_setpoint() and send_cmd() to append to a log list instead of publishing, and calls loop() 1,500 times (150 simulated seconds), injecting first_pos_tick = 25 to mimic PX4 first talking at tick 25. From the log, verify every contract in the chapter: no arm or mode command before ten ticks of established stream; a setpoint published on every tick between warm-up and landing with no gaps (count them); waypoints advancing exactly on their hold times; NAV_LAND sent exactly once; and zero publications of any kind afterward. Then break it the instructive way: reorder the branches so the trajectory check comes before the land_sent check, rerun, and find the contract violation in your log. This is the cheapest simulation level of all — below even SITL — and catching a flag bug here costs milliseconds.

Part 2 — rebudget the square as a mission. The trajectory holds each of its seven entries for the listed dwell times. Compute the mission’s minimum duration from the table alone, then re-plan it as a search pattern for the running example: replace the square with a four-leg lawnmower pass over a 40 m × 30 m area at the same 5 m altitude, choosing hold times from leg lengths at a conservative 2 m/s cruise. Check the total against the 3.5-minute search allocation in Part I’s mission time budget — and if it does not fit, decide what to trade (area, speed, or altitude) and defend the choice in two sentences. Then note what the time-based scheme cannot do: if wind slows the vehicle so a leg is unfinished when its hold expires, the plan advances anyway. Write down what signal the node would need to subscribe to, and what test it would apply, to advance on arrival instead — you have just specified the next chapter.

Related Lab: See Appendix B for the lab assignment that applies this chapter’s concepts.