Welcome, it's recommended that you start HERE, as this main README will allow you to navigate this large and complex project. Each of the subsections have their own respective READMEs and docs for full explanations.
Jacob's Ladder is a modular, system-agnostic UAV command-and-control (C2) framework built on ROS 2 Humble and PX4. It lets you write autonomous drone missions entirely in ROS 2 — the same code runs in Gazebo simulation on your laptop and on real hardware in the field, with no rewrites needed.
The framework uses PX4 external modes instead of the traditional offboard API. External modes register directly with PX4 through the companion computer, appearing as selectable flight modes in QGroundControl alongside the built-in ones (Stabilized, Position, Mission, etc.). If the companion computer ever stops communicating, PX4 automatically failsafes — so the system is safe by design.
Within the project you'll find several pacakges, this README serves as a higher level overview of all the components involved in the project. For a deeper view, there are embedded markdown files which break down specific rationale, processes, and objectives.
Use the repo uv environment for Python dependencies. It is created with system
site packages enabled so ROS 2, cv_bridge, JetPack OpenCV, and other system
Python modules remain visible inside the venv.
uv venv --python 3.10 --system-site-packages .venv
uv sync
source .venv/bin/activate
source /opt/ros/humble/setup.bash
source install/setup.bash # after colcon buildThat's it — uv sync installs everything, including the YOLO/drogue
image-processing stack (torch, torchvision, ultralytics). There is no extra
to enable and no bootstrap script to run.
opencv-python is deliberately not installed — see
OpenCV on the Jetson below.
On JetPack 6.2 / CUDA 12.6 (aarch64), uv sync pulls the pinned CUDA-enabled
torch and torchvision wheels from
pypi.jetson-ai-lab.io/jp6/cu126
plus the jetson-cudss-bootstrap workspace package (see
packages/jetson_cudss_bootstrap), which preloads the cuDSS runtime shared
libraries torch needs at import time. No LD_LIBRARY_PATH exports or extra
install steps are required — uv sync (or uv run ...) is enough.
The only reason to pass --system-site-packages when creating the venv is so
ROS 2, cv_bridge, and JetPack OpenCV stay visible inside it. If you re-create
the venv, keep that flag.
The OpenCV that ships with the ARK Electronics / JetPack image cannot build or
run this workspace. You must build the CUDA-enabled one with
installation_scripts/install_opencv.sh.
A stock ARK JetPack 6.2 image has three OpenCVs on it, and none of them work:
| Install | Version | Location | CUDA | contrib aruco |
|---|---|---|---|---|
| Ubuntu debs (ROS Humble links these) | 4.5.4d | /usr/lib/aarch64-linux-gnu |
no | no |
JetPack/ARK nvidia-opencv |
4.8.0 | /usr/lib, headers in /usr/include/opencv4 |
no | no |
opencv-python pip wheel |
4.9.0.80 | venv site-packages |
no | n/a |
Three concrete problems:
- No CUDA.
/usr/include/opencv4/opencv2/cvconfig.hcontains/* #undef HAVE_CUDA */, and there is nolibopencv_cuda*.soanywhere on the image.cv2.cuda.getCudaEnabledDeviceCount()returns0. - No contrib modules, so
aruco_trackercannot compile. It doesfind_package(OpenCV 4 REQUIRED COMPONENTS core imgproc calib3d aruco)and includes<opencv2/aruco.hpp>. The JetPack build only has the core-repo subset (opencv2/objdetect/aruco_detector.hpp) — noaruco.hpp, nolibopencv_aruco. - A broken pkg-config file.
/usr/lib/pkgconfig/opencv4.pcfromnvidia-opencv-devdeclaresprefix=/usr/local, but nothing OpenCV is installed there. Any pkg-config-driven build gets-L/usr/local/lib -lopencv_coreand fails to link. Installing to/usr/local(the default below) incidentally makes that file correct again.
REMOVE_DEFAULT_OPENCV=no OPENCV_BUILD_JOBS=6 ./installation_scripts/install_opencv.shTakes roughly 60–120 minutes on an 8-core Orin NX and needs ~10 GB of disk. It
needs sudo for the apt step and make install, so run it interactively.
Answer no to the "remove the default OpenCV" prompt (or pass
REMOVE_DEFAULT_OPENCV=no as above). apt purge *libopencv* would take
ros-humble-cv-bridge, ros-humble-image-geometry and the rest of
image_pipeline with it, since they link libopencv_*.so.4.5d.
Useful environment variables:
| Variable | Default | Notes |
|---|---|---|
OPENCV_VERSION |
4.10.0 |
|
OPENCV_CUDA_ARCH |
8.7 |
Orin NX / Orin Nano / AGX Orin are all sm_87. Use 7.2 for Xavier. Listing only your arch roughly halves build time. |
OPENCV_WITH_CUDNN |
ON |
Set OFF if cmake fails to detect cuDNN. JetPack 6.2 ships cuDNN 9, which older OpenCV releases mis-detect. Nothing here needs it — YOLO runs inference through torch, not cv2.dnn. |
OPENCV_BUILD_JOBS |
nproc - 1 |
Lower it if the build gets OOM-killed. |
OPENCV_PYTHON_VENV |
repo .venv |
Where the cv2 bindings are installed. |
OPENCV_INSTALL_PREFIX |
/usr/local |
The script verifies itself at the end: it checks that the cv2 the venv actually
imports is the version just built and that
cv2.cuda.getCudaEnabledDeviceCount() >= 1, and exits non-zero otherwise. If it
reports a different version than it just built, something on sys.path is
shadowing the build — almost always the opencv-python pip wheel (uv pip uninstall opencv-python) or the system python3-opencv deb.
Re-running the script is incremental: it keeps ~/opencv_src and its release/
build directory, so a second run only compiles what changed. Set
OPENCV_CLEAN_BUILD=1 to force a full rebuild.
The script installs the cv2 Python bindings directly into the repo venv's
site-packages, because OpenCV's default
${prefix}/lib/python3.10/site-packages is invisible to a venv. Venv
site-packages precedes /usr/lib/python3/dist-packages on sys.path, so this
build wins import cv2 over the system 4.5.4 deb.
The pip wheel is CPU-only and lives in the same site-packages the script writes
cv2 into, so it shadows the CUDA build and silently disables GPU acceleration
for every cv2 call. It is therefore excluded from pyproject.toml twice over:
it is not a direct dependency, and because ultralytics requires it
unconditionally, [tool.uv] override-dependencies drops the transitive
requirement with a never-true marker.
If you are working on a machine without the CUDA build, install it by hand:
uv pip install opencv-pythonTell colcon which OpenCV to use, so you don't get the JetPack 4.8.0 by accident:
colcon build --cmake-args -DOpenCV_DIR=/usr/local/lib/cmake/opencv4The workspace vendors src/vision_opencv, so cv_bridge and image_geometry
are rebuilt against the same OpenCV as everything else. Make sure
install/setup.bash is sourced after /opt/ros/humble/setup.bash so the
overlay shadows the 4.5-linked debs — loading two OpenCV ABIs into one process
causes crashes that look random.
START HERE:
| Package | Objective |
|---|---|
| Micro-XRCE-DDS-Agent | Should NOT be Altered : Use given branch -> Communication Agent from Drone to Host Computer |
| aruco_tracker | ROS 2 Wrapper for OpenCV detection of an ArUco Marker |
| drogue_flight | TODO: In-Progress Porting for flying to a detected KC-130 drogue |
| example_autonomous_mode | Start here to write a new mode -> A minimal, working External Mode (take off, hold, land) documented line by line and meant to be copied |
| jacob_manual | ROS 2 External Modes that Require Manual Control to get in the air, but fly autonomous missions after |
| precision_land | ROS 2 External Modes that fly autonomous missions for object detection, trajectory planning, and landing |
| oak_d_visual_odometry | ROS 2 nodes for OAK-D visual odometry using NVIDIA cuVSLAM, with optional PX4 VehicleOdometry output for flight. |
| px4-ros2-interface-lib | Should NOT be Altered : Use Given Branch -> ROS 2 <-> PX4 Bridge, allows us to create External Modes |
| px4_msgs | Should NOT be Altered : Use Given Branch -> PX4 Messages for ROS 2 Communication |
| px4_msgs_old | Should NOT be Altered : Use Given Branch -> More PX4 Messages for ROS 2 Communication |
| translation_node | Should NOT be Altered : Use Given Branch -> Introduced in PX4 v1.15.0, translates old px4 messages necessary for past versions to new px4 messages |
| vision_opencv | Should NOT be Altered : Use Given Branch -> OpenCV STD library |
Not a ROS 2 package, but it runs on the vehicle:
| Component | Objective |
|---|---|
| battery_monitor | Watches pack voltage on the INA238 and alerts at configurable land / min thresholds. Runs as a systemd service, independent of ROS 2. |
| Capability | Description |
|---|---|
| Precision Landing | Detect an ArUco marker with a downward camera, approach it, and autonomously land on it — even on a moving platform |
| Front-Camera Approach | Fly toward a target detected by a forward-facing camera using PID control |
| Combined Approach + Land | Use a front camera to approach, then seamlessly hand off to a downward camera for precision landing |
| Drogue Collection | Detect a drogue target via YOLO, plan a smooth S-curve trajectory, and fly to it |
| GPS-Denied Flight (VIO) | Estimate position indoors with an OAK-D stereo camera and NVIDIA cuVSLAM, fed to PX4's EKF2 as external vision — see oak_d_visual_odometry |
| Takeoff & Hold | Simple building-block modes for taking off, holding altitude, and landing |
| Battery Voltage Alerting | Watch the pack on the INA238 and warn — desktop popup, terminal broadcast — at your configured land and minimum voltages, before a pack gets over-drained. See battery_monitor |
A few concepts that will help you navigate this project:
- Node — A single process that does one job (e.g. track ArUco markers, send flight commands). Nodes communicate by publishing and subscribing to topics.
- Topic — A named data channel. One node publishes messages to a topic; other nodes subscribe to receive them. For example, the ArUco tracker publishes marker positions to a topic that the precision landing mode subscribes to.
- Package — A folder containing related code, a
CMakeLists.txt(orsetup.pyfor Python), and apackage.xmlmanifest. Each folder undersrc/is a package. - Launch file — A Python script that starts multiple nodes at once with the right parameters. You'll find these in each package's
launch/folder. - Workspace — The overall project directory. You build everything with
colcon buildand sourceinstall/setup.bashto make your nodes available.
┌───────────────────────────────────────────────────────────┐
│ QGroundControl │
│ (select External Mode here) │
└───────────────────┬───────────────────────────────────────┘
│ MAVLink
┌───────────────────▼───────────────────────────────────────┐
│ PX4 Autopilot │
│ (flight controller firmware) │
└───────────────────┬───────────────────────────────────────┘
│ uXRCE-DDS (serial or UDP)
┌───────────────────▼───────────────────────────────────────┐
│ Micro XRCE-DDS Agent │
│ (bridges PX4 topics ↔ ROS 2 topics) │
└───────────────────┬───────────────────────────────────────┘
│ ROS 2 topics
┌────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌──────────┐ ┌────────────────────┐
│ Translation │ │ ArUco │ │ External Mode │
│ Node │ │ Tracker │ │ (e.g. PrecisionLand│
│ │ │ │ │ TakeoffHold, etc.) │
│ Converts │ │ Detects │ │ │
│ PX4 msg │ │ markers │ │ Reads sensor data, │
│ versions │ │ from │ │ runs state machine,│
│ │ │ camera │ │ sends setpoints │
└─────────────┘ └──────────┘ └────────────────────┘
- Ubuntu Linux (22.04 recommended)
- Docker Engine
- PX4 Autopilot (v1.16.0 recommended)
- QGroundControl Daily Build
Create your workspace on your system and add in PX4-Autopilot
mkdir -p ~/jacob_ladder_ws/src && cd ~/jacob_ladder_ws/src
git clone https://github.com/PX4/PX4-Autopilot.git --recursiveThe v1.16.0 tag is highly recommended. The included translation node provides compatibility with most PX4 versions from 1.16 onward, but some versions may lack the Gazebo worlds and models needed for simulation — refer to PX4's official documentation if that's the case.
cd PX4-Autopilot
git status
git checkout v1.16.0Download the daily build: https://docs.qgroundcontrol.com/master/en/qgc-user-guide/releases/daily_builds.html
git clone https://github.com/CursedRock17/Jacob_Ladder.git
cd Jacob_Ladder
git checkout drogue_collector
git submodule update --init --recursiveThat is the whole clone procedure — there is nothing to patch by hand.
It points at cdenihan/px4-ros2-interface-lib, a fork of
Auterion/px4-ros2-interface-lib, on branch
jacob-ladder/executor-skip-msg-check. The fork is the upstream 1.6.0 tag plus
a single commit adding setSkipMessageCompatibilityCheck() and its backing flag
to ModeExecutorBase.
Upstream only provides that method on ModeBase, where it is protected — and
ModeExecutorBase's friend relationship with ModeBase is not inherited by
derived classes, so no executor subclass can reach it. Several of our mode
executors call it, so without the fork precision_land, drogue_flight and
jacob_manual fail to compile with:
error: 'setSkipMessageCompatibilityCheck' was not declared in this scope
This used to be distributed as a patch.diff in the repo root that you applied
by hand. That is gone: it was undocumented, any git submodule update silently
reverted it, and it left the submodule permanently dirty. The change now lives in
a real commit, so a plain git submodule update --init --recursive gives you a
tree that builds.
To rebase onto a newer upstream release:
cd src/px4-ros2-interface-lib
git remote add upstream https://github.com/Auterion/px4-ros2-interface-lib.git
git fetch upstream --tags
git rebase <new-tag> # one commit to replay
git push fork HEAD --force-with-lease
cd ../.. && git add src/px4-ros2-interface-lib && git commitNote the guard short-circuits waitForFMU() as well as
messageCompatibilityCheck(), so an executor that skips the check also does not
wait for the FMU before registering.
The docker/ directory contains a layered Dockerfile stack built on px4io/px4-dev-base-jammy.
Pre-built images are available on Docker Hub, so you can skip manual dependency installation entirely.
Install Docker Engine: https://docs.docker.com/engine/install/ubuntu/
Start the container:
If on Linux: First make sure your user has docker permissions, a quick check:
sudo groupadd docker
sudo usermod -aG docker $USER
newgrp docker
# Make sure to refresh computer for groups to kick into effect
docker run hello-worldThen open up the container
xhost +
export CONTAINER_NAME=your_container_name
docker run -it --privileged \
-u $(id -u):$(id -g) \
-v ~/src/PX4-Autopilot:/src/PX4-Autopilot/:rw \
-v ~/path/to/Jacob_Ladder:/path/to/Jacob_Ladder/:rw \
-v /tmp/.X11-unix:/tmp/.X11-unix:ro \
-e DISPLAY=:0 \
--network host \
--name=$CONTAINER_NAME \
jacobsafeer/px4-dev-harmonic-jammy-humble-opencv-rqt:latest bashIf on Windows:
export CONTAINER_NAME=your_container_name
docker run -it --privileged \
--user root \
-v "C:\path\PX4":/src/PX4/:rw \
-v "C:\path\Jacob_Ladder":/src/Jacob_Ladder/:rw \
-e DISPLAY=:0 \
--network host \
--name $CONTAINER_NAME jacobsafeer/px4-dev-harmonic-jammy-humble-opencv-rqt:latest bashNot tested on MacOS
Maybe If you running a windowing system other than X11 or notably running simulation with a GPU
you may have to make -e and -v alterations to the the windowing and display sections to allow it run.
Replace /path/to with your actual path to Jacob_Ladder, and $CONTAINER_NAME with whatever you'd like.
If you're not already in the container, start and enter
docker start $CONTAINER_NAME
docker exec -it $CONTAINER_NAME bashWhen inside the container, build the workspace:
cd /path/to/Jacob_Ladder
source /opt/ros/humble/setup.bash
colcon buildCustomize a launch script using the template in the existing launch_scripts/ directory. Each script opens multiple terminal tabs — one for PX4 SITL, one for the DDS agent, one for each ROS node — so everything starts together.
# On the host machine
./QGroundControl.AppImage # Start QGC
docker start $CONTAINER_NAME # Start the container
./launch_scripts/precision_land.sh # Run the launch scriptSelect the precision landing mode from the QGroundControl dropdown:
precision_land.sh is just one of several ready-to-run scripts in launch_scripts/. Each one starts PX4 SITL in a specific Gazebo world and launches the ROS nodes needed for that flight mode. Pick whichever matches what you want to test.
| Script | Flight Mode | Gazebo World | Purpose |
|---|---|---|---|
precision_land.sh |
precision_land/front_to_precision_land.launch.py |
gz_x500_dual_cam_aruco_dual_ids |
Front-camera approach, then downward precision landing on an ArUco tag |
approach_aruco.sh |
precision_land/front_approach.launch.py |
gz_x500_dual_cam_aruco_dual_ids |
Front-camera ArUco tag approach only (no landing) |
jacob_manual.sh |
jacob_manual/front_approach.launch.py |
gz_x500_dual_cam_aruco_dual_ids |
Same as approach_aruco.sh, but runs the newer jacob_manual package version |
takeoff_hover.sh |
precision_land/takeoff_hold.launch.py |
gz_x500_dual_cam |
Takeoff and hold indefinitely — good for hover tuning |
takeoff_hover_land.sh |
precision_land/takeoff_land.launch.py |
gz_x500_dual_cam |
Takeoff, hold briefly, then land — simplest end-to-end mode |
offboard_blank.sh |
precision_land/blank_mode.launch.py |
gz_x500_dual_cam |
Empty external-mode template — registers with PX4 and does nothing, for when you already know the framework |
moving_launch.sh |
precision_land/track_follow.launch.py |
Custom moving platform world | Track and land on a moving ArUco platform (uses the v1_16_tracker aruco launch) |
real_launch.sh |
(edit before use) | Real hardware | DDS agent + RViz shell for Jetson/Pixhawk flight testing — all mode commands are commented out so you can uncomment the one you want |
fake_drogue.sh |
(no flight mode) | Real hardware | Drogue camera feed sanity check — runs the DDS agent and ros2 topic hz on the camera topic |
Writing a new mode rather than running an existing one? Start from example_autonomous_mode — a minimal mode that actually flies (take off, hold, land), documented line by line, with its own launch file:
ros2 launch example_autonomous_mode example_autonomous_mode.launch.pyEvery script follows the same shape: the first tab is PX4 SITL (or omitted for real hardware), then the DDS agent, the translation node, any trackers, and the mode itself. If a script doesn't match your container name or PX4 version, open it and edit the container_name variable at the top — it's the same in every script.
Paths: no script hardcodes a home directory. Each one sources
jl_env.shfrom the repo root, which derives the workspace root from its own location, so a clone works under any username at any path. Override any of these in the environment when the defaults don't fit:
Variable Default JL_WS_ROOTdirectory containing jl_env.sh(this repo)JL_PX4_DIRPX4-Autopilotbeside or inside the workspace, else~/PX4-AutopilotJL_ROS_DISTRO$ROS_DISTRO, elsehumbleJL_VENV$JL_WS_ROOT/.venvJL_DOCKER_WS_DIR/JL_DOCKER_PX4_DIRthe host paths above — set these if the container mounts them elsewhere JL_TRACKTOR_BEAM_DIRtracktor-beambeside the workspace, else under$HOMEJL_DROGUE_MODELYOLO weights from the installed drogue_flightshare dir, else the source treeThe systemd units are templates (
services/*.service.in) rendered by./services/install_services.sh, which fills in the real workspace path and owning user for the machine it runs on.
| Tab | Options |
|---|---|
| precision_land | precision_land.launch.py (default ArUco world) or track_follow.launch.py (moving platform world) |
| aruco_tracker | aruco_tracker.launch.py (PX4 1.15.x), v1_16_tracker.launch.py (PX4 1.16.x default), or moving_aruco.launch.py (PX4 1.16.x moving platform) |
For more detailed tutorials checkout the general_docs section of the project
which contains information on how to:
- Playback Data after the flights
- Running on real hardware
general_docs/external_modes.md— Building your own modes- More to Come
That way you have access to more in-depth tutorials.
Also, all of the code in the repository can be totalled expanded.
Different configurations of physical drones may require different PX4 Parameter configurations
any and all tested forms of parameters are labelled in the config/params directory.
- Manual flight first — Get the drone flying reliably in Stabilized and Position modes before running any autonomy
- Networked sim test — Run PX4 SITL on your PC and the ROS nodes on the Jetson over the same network to verify communication
- Props-off test — Run your flight mode with propellers removed and verify PX4 receives setpoints via QGroundControl
- Verify topics — Confirm PX4 topics are visible:
ros2 topic listshould show/fmu/out/vehicle_odometry,/fmu/out/vehicle_status, etc.
Email: lwendlan@umd.edu
See LICENSE for details.
