PX4 Precision Landing with ROS 2 and Gazebo

PX4 Precision Landing with ROS 2 and Gazebo

Set up vision-based precision landing on Windows with WSL2: PX4 SITL, Gazebo, ROS 2 Humble and ARK's tracktor-beam land a simulated drone on an ArUco marker.

Landing a drone somewhere near where it took off is easy. Landing it on the tray that slides out of a docking station is not. Precision landing closes the gap with a camera and a printed marker: the drone finds the marker, locks onto it from altitude, and follows it all the way to touchdown.

This tutorial walks you through the full setup in simulation on a Windows machine, from a bare install to a drone that lands itself on a marker. You’ll install the flight software, the simulator, and the open-source vision code that ties them together, then fly a simulated drone off its landing pad and watch it find the marker and touch down on its own. As usual, you can follow along with the written tutorial below or the video from the livestream.

The Stack

Five pieces and two translators make up the system:

  • WSL2 + Ubuntu 22.04. A Linux computer living inside Windows, where everything in this tutorial runs.

  • PX4 (SITL). The drone’s brain, the same flight code that runs on real autopilots.

  • Gazebo Harmonic. The physics simulator, providing an x500 quad, a downward camera, and a world with an ArUco marker in it.

  • ROS 2 Humble. The robotics middleware where programs publish and subscribe to topics.

  • Micro XRCE-DDS Agent. Translator number one, between PX4 and ROS 2, which turns uORB topics into /fmu/... topics.

  • ros_gz_bridge. Translator number two, which carries the Gazebo camera feed into ROS 2.

  • tracktor-beam (ARK). A ready-made ArUco tracker plus the Precision Land custom flight mode.

All the precision landing logic runs on the ROS 2 side, not inside PX4, and the agent translates the results into PX4 messages. That only works when every layer is the version the others expect. Get one wrong and the messages never reach each other, with no error to tell you why. The steps below are prescriptive about versions for that reason.

The work splits into three parts: a one-time setup per machine, a handful of launches you run every session, and the flight itself.

One-Time Setup: Configure WSL First

All of this is done once per machine. Budget an afternoon: OpenCV takes 20 to 40 minutes to build, and the first PX4 build takes about 15. Use a machine with at least 16 GB of RAM, or 32 GB if you have it.

Nothing here uses Docker, because a container hides what is going on underneath. On native Linux, skip the WSL steps and the rest is more or less the same.

Configure WSL memory before building anything

On the Windows side, create or edit C:\Users\<you>\.wslconfig:

[wsl2]

[wsl2]

[wsl2]

Set memory a bit below your physical RAM. WSL defaults to about half of it, and when a big compile runs past that limit the Linux out-of-memory killer terminates the compiler. The large swap file is the safety net that keeps those compiles alive. These values worked on a 16 GB machine. On a 32 GB machine, giving WSL 24 GB made the builds fly.

Install WSL2 and Ubuntu 22.04

Run this in PowerShell as Administrator:

wsl --install -d Ubuntu-22.04
wsl --install -d Ubuntu-22.04
wsl --install -d Ubuntu-22.04

It must be 22.04. A plain wsl --install gives you 24.04, and ROS 2 Humble does not exist for 24.04. Reboot if prompted, open Ubuntu, and create your Linux username and password. Every step from here on runs inside the Ubuntu terminal.

Verify the environment

Three quick checks before investing hours in builds:

lsb_release -a   # must say 22.04
free -h          # Mem ~12G, Swap 16G
nproc            # should say 8
lsb_release -a   # must say 22.04
free -h          # Mem ~12G, Swap 16G
nproc            # should say 8
lsb_release -a   # must say 22.04
free -h          # Mem ~12G, Swap 16G
nproc            # should say 8

free -h proves the .wslconfig changes applied, and nproc should match the processors value you set. Wrong numbers? Run wsl --shutdown in PowerShell and relaunch. Closing the window is not enough, because WSL stays alive in the background. If that still does not take, restart the machine.

Clone PX4 and Pin It to the Tested Commit

Clone the autopilot source with all of its submodules:

cd ~
git clone <https://github.com/PX4/PX4-Autopilot.git> --recursive
cd ~
git clone <https://github.com/PX4/PX4-Autopilot.git> --recursive
cd ~
git clone <https://github.com/PX4/PX4-Autopilot.git> --recursive

Then pin it to the one PX4 commit tracktor-beam is tested against:

cd ~/PX4-Autopilot
git

cd ~/PX4-Autopilot
git

cd ~/PX4-Autopilot
git

Skip the pin and you get the classic silent failure: topics appear in ros2 topic list, but no data flows. Newer PX4 main uses versioned messages such as vehicle_status_v4 that do not match the px4_msgs bundled with tracktor-beam.

Re-sync the submodules after any checkout. Forgetting this causes odd build errors in the sitl_gazebo and mavlink modules:

git submodule update --init --recursive
git submodule update --init --recursive
git submodule update --init --recursive

Run the PX4 toolchain installer. It installs the compilers, the Python dependencies, and Gazebo Harmonic in one go:

bash
bash
bash

Then close and reopen Ubuntu. Several of the installer’s changes, including the new PATH and group memberships, only apply to new shells.

Install ROS 2 Humble

Add the ROS 2 repository

apt cannot find ros-humble-* yet, because ROS lives in its own repository. Install the tools to add it first. Skip this and you get E: Unable to locate package ros-humble-desktop.

sudo apt update && sudo apt install -y curl

sudo apt update && sudo apt install -y curl

sudo apt update && sudo apt install -y curl

Some ROS dependencies live in Ubuntu’s universe component, so enable it:

sudo
sudo
sudo

Add the ROS signing key so apt can verify the ROS packages:

sudo curl -sSL \
  <https://raw.githubusercontent.com/ros/rosdistro/master/ros.key> \
  -o

sudo curl -sSL \
  <https://raw.githubusercontent.com/ros/rosdistro/master/ros.key> \
  -o

sudo curl -sSL \
  <https://raw.githubusercontent.com/ros/rosdistro/master/ros.key> \
  -o

Then add the repository itself, which tells apt where ROS 2 lives:

echo "deb [arch=$(dpkg --print-architecture)\
  signed-by=/usr/share/keyrings/ros-archive-keyring.gpg]\
  <http://packages.ros.org/ros2/ubuntu\>
$(. /etc/os-release && echo $UBUNTU_CODENAME) main" \
  | sudo tee

echo "deb [arch=$(dpkg --print-architecture)\
  signed-by=/usr/share/keyrings/ros-archive-keyring.gpg]\
  <http://packages.ros.org/ros2/ubuntu\>
$(. /etc/os-release && echo $UBUNTU_CODENAME) main" \
  | sudo tee

echo "deb [arch=$(dpkg --print-architecture)\
  signed-by=/usr/share/keyrings/ros-archive-keyring.gpg]\
  <http://packages.ros.org/ros2/ubuntu\>
$(. /etc/os-release && echo $UBUNTU_CODENAME) main" \
  | sudo tee

Refresh apt to pick up the newly added repository:

sudo
sudo
sudo

Install ROS 2 Humble, the Gazebo bridge, and the dev tools

sudo apt install -y

sudo apt install -y

sudo apt install -y

This one takes a while. It installs the desktop tools, rqt, and the ros_gz bridge package. ros-humble-desktop also includes rqt_image_view, which you use later to watch the annotated camera feed.

Source ROS in every terminal, automatically

Every terminal that touches ROS must source the setup file, and a session uses several terminals at once. Put it in .bashrc once and forget it:

echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc
source

echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc
source

echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc
source

A terminal that skipped sourcing, or carries a stray ROS_DOMAIN_ID, silently sees zero topics. It is a top-three cause of missing topics.

The Micro XRCE-DDS Agent

ROS 2 is built on DDS, the Data Distribution Service, a brokerless publish and subscribe middleware from aerospace and defense. Every node discovers its peers via multicast and streams data directly to them, so there is no central server to crash. Topics are strongly typed and carry quality of service policies. The catch is weight. A full DDS participant, with its discovery state, QoS machinery, and buffers, is far too heavy for a flight controller.

So PX4 runs a featherweight XRCE client instead. It sends compact “publish this for me” requests over UDP to the Micro XRCE-DDS Agent, which stands in the ROS 2 graph on PX4’s behalf.

When you run ros2 topic echo /fmu/out/vehicle_odometry, you are subscribed to a topic the agent created as PX4’s proxy. That is also why px4_msgs must match the firmware revision. Clone the agent, pinned to the known-good 2.4.3:

cd ~
git clone -b

cd ~
git clone -b

cd ~
git clone -b

Run the CMake build command as follows:

cd Micro-XRCE-DDS-Agent
mkdir build && cd build
cmake ..
make -j$(nproc)
cd Micro-XRCE-DDS-Agent
mkdir build && cd build
cmake ..
make -j$(nproc)
cd Micro-XRCE-DDS-Agent
mkdir build && cd build
cmake ..
make -j$(nproc)

Install the agent system-wide, with ldconfig registering the new shared libraries:

sudo make install
sudo

sudo make install
sudo

sudo make install
sudo

Then verify that the MicroXRCEAgent runs:

MicroXRCEAgent --help
MicroXRCEAgent --help
MicroXRCEAgent --help

If usage information prints, the install worked. Press Ctrl+C if it lingers.

Clone tracktor-beam

tracktor-beam is ARK Electronics’ open-source demo workspace for vision-based precision landing with PX4 and ROS 2. ARK builds US-made PX4 flight controllers and companion computers.

This is a build-versus-buy decision, and here you buy. The hard, undifferentiated parts of precision landing (frame transforms, mode plumbing, PX4 integration) are already solved and BSD-3 licensed, so your engineering hours go into what is specific to your problem.

cd ~
git clone \
  <https://github.com/ARK-Electronics/tracktor-beam.git> \
  --recursive
cd ~
git clone \
  <https://github.com/ARK-Electronics/tracktor-beam.git> \
  --recursive
cd ~
git clone \
  <https://github.com/ARK-Electronics/tracktor-beam.git> \
  --recursive

Remember the --recursive flag, since px4_msgs and the px4_ros2 library come in as submodules. The workspace contains four things:

  • An aruco_tracker node. It runs OpenCV’s detectMarkers and solvePnP against the camera intrinsics, then publishes the target pose along with the annotated /image_proc debug stream.

  • A precision_land node. This implements the actual landing behavior.

  • Vendored px4_msgs. These are matched to a tested PX4 commit, the one you pinned earlier.

  • The px4_ros2 interface library. Everything else is built on it.

One launch file wires all of it together, Gazebo camera bridges included. It is also a working example of the px4_ros2 custom-mode pattern: adding behavior to PX4 without forking the firmware or babysitting offboard setpoints. That pattern is useful independent of landing.

Install OpenCV and the Build Dependencies

The repository ships a script that builds OpenCV from source with the ArUco module:

cd

cd

cd

This is the other long one: 20 to 40 minutes in WSL. Start it and get coffee. Permission denied? Run chmod +x install_opencv.sh and try again.

The build needs to know which Gazebo to compile against. The variable must be set in the shell that runs colcon build, and .bashrc makes that automatic:

echo "export GZ_VERSION=harmonic" >> ~/.bashrc
source

echo "export GZ_VERSION=harmonic" >> ~/.bashrc
source

echo "export GZ_VERSION=harmonic" >> ~/.bashrc
source

Install the system dependencies before building:

sudo apt install -y libgflags-dev python3-rosdep
sudo rosdep init 2>/dev/null; rosdep update
cd ~/tracktor-beam
rosdep install --from-paths src --ignore-src -r -y
sudo apt install -y libgflags-dev python3-rosdep
sudo rosdep init 2>/dev/null; rosdep update
cd ~/tracktor-beam
rosdep install --from-paths src --ignore-src -r -y
sudo apt install -y libgflags-dev python3-rosdep
sudo rosdep init 2>/dev/null; rosdep update
cd ~/tracktor-beam
rosdep install --from-paths src --ignore-src -r -y

rosdep reads every package in src/ and installs everything they declare, including the gflags headers that ros_gz_sim needs. Skip it and the build stops partway with fatal error: gflags/gflags.h: No such file or directory, the first round of missing-header whack-a-mole. One minute of rosdep prevents it.

Build the Memory Hog First

ros_gz_bridge compiles enormous auto-generated C++ files. Parallel g++ processes each want gigabytes, and the Linux out-of-memory killer terminates them with Killed signal terminated program cc1plus. Build that one package alone, with limited parallelism:

cd ~/tracktor-beam
MAKEFLAGS="-j2" colcon build --parallel-workers 1 \
  --packages-select

cd ~/tracktor-beam
MAKEFLAGS="-j2" colcon build --parallel-workers 1 \
  --packages-select

cd ~/tracktor-beam
MAKEFLAGS="-j2" colcon build --parallel-workers 1 \
  --packages-select

Keep htop open next to it if you like. Memory should peak and spill into swap, not die. Once built, it is never rebuilt, since later changes only touch precision_land.

Then build the rest. Already-built packages are skipped automatically:

If it hiccups once on a submodule-heavy first build, run it again before debugging. Then source the workspace so ros2 can see the freshly built aruco_tracker and precision_land packages. Any terminal that launches tracktor-beam nodes needs this:

source
source
source

First PX4 SITL Build and Smoke Test

One make target builds SITL and launches the simulator with the downward-camera x500 sitting in the ArUco world:

cd ~/PX4-Autopilot
make

cd ~/PX4-Autopilot
make

cd ~/PX4-Autopilot
make

The first build takes about 15 minutes. Success is a Gazebo window with the drone sitting on the ArUco marker.

Once you see it, press Ctrl+C. If you have flickering or rendering glitches under WSL try running export LIBGL_ALWAYS_SOFTWARE=1 and launch again, which renders in software instead of on the GPU.

Install QGroundControl 4.4 Inside WSL

Not the latest release but QGC 4.4. Current QGroundControl builds need glibc 2.36, which means Ubuntu 24.04, and they die on 22.04 with GLIBC_2.36 not found. The 4.4.x line runs on 22.04 and does everything this setup needs.

sudo apt install -y libfuse2 libxcb-xinerama0 \
  libxkbcommon-x11-0 libpulse-dev libgstreamer1.0-0 \
  gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
  gstreamer1.0-plugins-bad gstreamer1.0-gl libqt5gui5
cd ~
wget <https://github.com/mavlink/qgroundcontrol/releases\>
/download/v4.4.4/QGroundControl.AppImage
chmod

sudo apt install -y libfuse2 libxcb-xinerama0 \
  libxkbcommon-x11-0 libpulse-dev libgstreamer1.0-0 \
  gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
  gstreamer1.0-plugins-bad gstreamer1.0-gl libqt5gui5
cd ~
wget <https://github.com/mavlink/qgroundcontrol/releases\>
/download/v4.4.4/QGroundControl.AppImage
chmod

sudo apt install -y libfuse2 libxcb-xinerama0 \
  libxkbcommon-x11-0 libpulse-dev libgstreamer1.0-0 \
  gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
  gstreamer1.0-plugins-bad gstreamer1.0-gl libqt5gui5
cd ~
wget <https://github.com/mavlink/qgroundcontrol/releases\>
/download/v4.4.4/QGroundControl.AppImage
chmod

Running QGC inside WSL, not as a Windows application, avoids the WSL2 NAT problem entirely. It sees SITL on localhost.

Every Run: One Launch per Terminal

The one-time setup is done. This part runs every session: open four WSL terminals with one launch each, QGroundControl in a fifth, and a sixth kept free for the flight.

Terminal 1: PX4 and Gazebo

cd ~/PX4-Autopilot
make

cd ~/PX4-Autopilot
make

cd ~/PX4-Autopilot
make

Wait for the pxh> prompt and the Gazebo window before moving on.

Terminal 2: The uXRCE-DDS Agent

MicroXRCEAgent udp4 -p 8888
MicroXRCEAgent udp4 -p 8888
MicroXRCEAgent udp4 -p 8888

The agent looks dead before it connects. The PX4 client takes a few seconds to handshake after SITL boots, so wait for session established and a stream of create topic lines. Check ros2 topic list too early and you see only parameter_events and rosout. Patience, not panic.

Terminal 3: The Precision Landing Stack

cd ~/tracktor-beam
source

cd ~/tracktor-beam
source

cd ~/tracktor-beam
source

One launch file starts everything: the camera bridges (no manual parameter_bridge), the ArUco tracker, and the Precision Land custom flight mode. The px4_ros2 interface library registers the mode with the autopilot, and QGC shows it like any other.

Terminal 4: Annotated Camera View

Select /image_proc from the topic dropdown. You get the live camera feed with the marker outlined and its pose axes drawn. This is where you watch the marker lock during the descent.

Launch QGroundControl

cd

cd

cd

QGC auto-discovers SITL on localhost. FUSE error? Run ./QGroundControl.AppImage --appimage-extract-and-run instead. Garbled window? Prefix the command with LIBGL_ALWAYS_SOFTWARE=1.

The Demo Flight

Four moves: take off, fly away, switch to Precision Land, and watch the drone hunt back to the marker and touch down.

Confirm Precision Land appears in QGC

Check the flight mode dropdown. Precision Land should be listed alongside the built-in modes: a ROS 2 node has added a first-class flight mode to the autopilot.

If the option is missing, then Terminal 3 is probably not running or is not connected. Check its output for any errors.

Set up the Gazebo camera

In Gazebo’s Entity Tree, right-click x500_mono_cam_down_0 and choose Follow, and the view tracks the drone automatically. Set this before takeoff so you are not fumbling mid-descent.

Take Off and Fly a Few Meters Away

In the PX4 terminal, take off:

This climbs to 2.5 meters, which is enough for this flight. You can also use QGC’s Takeoff slider and climb to about 10 meters, where the marker fills a healthy chunk of the camera frame and is easy to detect.

Landing from directly above the marker is anticlimactic, so move the drone off to the side first to make the hunt visible. In the QGC map, click a spot 5 to 8 meters from the marker, choose Go to location, and wait for the drone to arrive and hold. For a smaller, repeatable move, a short pymavlink script in another WSL terminal flies the drone 3 meters east and 4 meters north, 5 meters from where it started:

pip install pymavlink
python3 nudge.py 3 4
pip install pymavlink
python3 nudge.py 3 4
pip install pymavlink
python3 nudge.py 3 4

Give the drone a minute to stabilize. Do not drag the model in Gazebo while it is armed: teleporting a flying vehicle makes the EKF very unhappy. Repositioning before arming with the translate tool is fine.

Switch to Precision Land and Watch

Select Precision Land in the QGC mode dropdown. The drone climbs first, then slides over until the marker centers in /image_proc, descends with the marker locked in frame, touches down on the tag, and disarms.

If it searches instead of descending, the tracker lost the marker. Climb slightly and re-enter the mode.

Gotchas Cheat Sheet

Symptom

Fix

Unable to locate package ros-humble-*

ROS repo not added (see Add the ROS 2 repository), or you are on 24.04. Check lsb_release -a.

Only parameter_events + rosout in topic list

Too early. Wait a few seconds, then run uxrce_dds_client status at pxh> and check that ROS_DOMAIN_ID is unset.

fatal error: gflags/gflags.h

sudo apt install libgflags-dev plus rosdep install (see Install OpenCV and the Build Dependencies).

c++: fatal error: Killed ... cc1plus

Out of memory. Build ros_gz_bridge with -j2 and 1 worker (see Build the Memory Hog First), and verify .wslconfig with free -h.

QGC: GLIBC_2.36 not found

The latest QGC needs 24.04. Use the 4.4.x AppImage (see Install QGroundControl 4.4 Inside WSL).

No camera topics after launch

Run gz topic -l and check the sensor path (.../camera_link/sensor/imager/image).

Weird state after crashes

pkill -f "gz sim"; pkill -x px4, then relaunch clean.

Gazebo or QGC rendering glitches

export LIBGL_ALWAYS_SOFTWARE=1

Conclusion

You now have vision-guided precision landing running end to end on a Windows machine: a version-matched flight stack and simulator, a tracker that finds an ArUco marker from a downward camera, and a custom Precision Land mode you can pick in QGroundControl like any built-in. You flew the drone off its pad and watched it find the marker and land on it. This is simulation only, and a marker that moves during the descent is untested. For a first hardware test, print a DICT_4X4 or 6X6 marker at 15 to 30 cm. We can’t wait to see what you build!

Additional Resources