PX4 drone simulation project using ROS 2 Jazzy, Gazebo Harmonic, SLAM Toolbox, and Nav2.
This project demonstrates:
- PX4 drone simulation in Gazebo Harmonic
- ROS 2 bridge from Gazebo to ROS 2
- 2D lidar mapping with SLAM Toolbox
- Saved-map navigation with Nav2
- AMCL localization
/cmd_velto PX4 control using MAVSDK- Custom Gazebo world:
outdoor_slam_test.sdf
PX4-Autopilot is not fully included in this repository.
Clone PX4-Autopilot separately, then copy the custom world file from this repository.
Tested with:
- Ubuntu 24.04
- ROS 2 Jazzy
- Gazebo Harmonic
- PX4 SITL
- SLAM Toolbox
- Nav2
- MAVSDK Python
- ros_gz_bridge
unknown_px4_ws/
├── README.md
├── .gitignore
├── px4_custom_files/
│ └── Tools/
│ └── simulation/
│ └── gz/
│ └── worlds/
│ └── outdoor_slam_test.sdf
└── src/
└── unknown_px4_mapping/
├── config/
│ ├── nav2_map_params.yaml
│ ├── nav2_params.yaml
│ └── slam_toolbox.yaml
├── launch/
│ ├── px4_bridge_odom_tf.launch.py
│ ├── px4_cmd_vel_to_px4.launch.py
│ ├── px4_nav2_map.launch.py
│ └── px4_slam_toolbox.launch.py
├── maps/
│ ├── outdoor_px4_slam_map.yaml
│ └── outdoor_px4_slam_map.pgm
├── package.xml
├── setup.py
└── unknown_px4_mapping/
├── cmd_vel_to_px4.py
└── dynamic_pose_to_odom.py
cd ~
git clone https://github.com/wattanatum/unknown_PX4-Autopilot.git unknown_px4_ws
cd ~/unknown_px4_wscd ~/unknown_px4_ws
colcon build --symlink-install
source install/setup.bashTo source automatically every new terminal:
echo "source ~/unknown_px4_ws/install/setup.bash" >> ~/.bashrc
source ~/.bashrcThis project only needs the ROS 2, Gazebo, Nav2, SLAM Toolbox, bridge, and MAVSDK packages used by the launch files.
sudo apt update
sudo apt install -y \
ros-jazzy-ros-gz-bridge \
ros-jazzy-ros-gz-sim \
ros-jazzy-slam-toolbox \
ros-jazzy-navigation2 \
ros-jazzy-nav2-bringup \
ros-jazzy-tf2-ros \
ros-jazzy-tf2-tools \
ros-jazzy-rviz2 \
ros-jazzy-nav-msgs \
ros-jazzy-geometry-msgs \
ros-jazzy-sensor-msgs \
ros-jazzy-tf2-msgs \
python3-colcon-common-extensions \
python3-rosdep \
python3-venv \
python3-pipcd ~/unknown_px4_ws
python3 -m venv mavsdk_ros_env
source ~/unknown_px4_ws/mavsdk_ros_env/bin/activate
pip install --upgrade pip
pip install mavsdk
deactivatePX4-Autopilot must be cloned separately.
cd ~
git clone https://github.com/PX4/PX4-Autopilot.git --recursive
cd ~/PX4-Autopilot
bash ./Tools/setup/ubuntu.shAfter installation, reboot if PX4 setup asks you to.
This repository includes a custom Gazebo world:
px4_custom_files/Tools/simulation/gz/worlds/outdoor_slam_test.sdf
Copy it into PX4-Autopilot:
cp ~/unknown_px4_ws/px4_custom_files/Tools/simulation/gz/worlds/outdoor_slam_test.sdf \
~/PX4-Autopilot/Tools/simulation/gz/worlds/Check:
ls ~/PX4-Autopilot/Tools/simulation/gz/worlds | grep outdoor_slam_testUse this when starting the PX4 drone simulation.
cd ~/PX4-Autopilot
PX4_GZ_WORLD=outdoor_slam_test make px4_sitl gz_x500_lidar_2dAlternative default command:
cd ~/PX4-Autopilot
make px4_sitl gz_x500_lidar_2dKeep this terminal running.
Open a new terminal:
source ~/unknown_px4_ws/install/setup.bash
ros2 launch unknown_px4_mapping px4_bridge_odom_tf.launch.pyThis launch file runs:
ros_gz_bridge- Gazebo lidar scan bridge
- Gazebo dynamic pose bridge
/clockbridgedynamic_pose_to_odom.py- static TF
base_link -> link
Expected TF tree:
odom -> base_link -> link
Check /odom:
ros2 topic echo /odom --onceExpected:
header.frame_id: odom
child_frame_id: base_link
Check TF:
ros2 run tf2_ros tf2_echo odom base_linkCheck lidar TF:
ros2 run tf2_ros tf2_echo base_link linkCheck /clock:
ros2 topic echo /clock --onceCheck dynamic pose:
ros2 topic echo /world/outdoor_slam_test/dynamic_pose/info --onceCheck lidar scan:
ros2 topic echo /world/outdoor_slam_test/model/x500_lidar_2d_0/link/link/sensor/lidar_2d_v2/scan --onceCheck topic list:
ros2 topic listUse this method when creating a new map.
Do not run Nav2 saved-map navigation at the same time as SLAM Toolbox.
cd ~/PX4-Autopilot
PX4_GZ_WORLD=outdoor_slam_test make px4_sitl gz_x500_lidar_2dsource ~/unknown_px4_ws/install/setup.bash
ros2 launch unknown_px4_mapping px4_slam_toolbox.launch.pysource ~/unknown_px4_ws/install/setup.bash
ros2 launch unknown_px4_mapping px4_nav2.launch.pysource ~/unknown_px4_ws/install/setup.bash
ros2 launch unknown_px4_mapping px4_cmd_vel_to_px4.launch.pysource ~/unknown_px4_ws/install/setup.bash
rviz2Open RViz2 config in this path:
~/unknown_px4_ws/src/rviz2/px4_slam_toolbox.rviz
After mapping, save the map:
cd ~/unknown_px4_ws/src/unknown_px4_mapping/maps
ros2 run nav2_map_server map_saver_cli -f outdoor_px4_slam_mapExpected files:
outdoor_px4_slam_map.yaml
outdoor_px4_slam_map.pgm
Rebuild after saving maps:
cd ~/unknown_px4_ws
colcon build --packages-select unknown_px4_mapping --symlink-install
source install/setup.bashUse this method after a map has already been created.
Do not run SLAM Toolbox when using saved-map navigation.
cd ~/PX4-Autopilot
PX4_GZ_WORLD=outdoor_slam_test make px4_sitl gz_x500_lidar_2dsource ~/unknown_px4_ws/install/setup.bash
ros2 launch unknown_px4_mapping px4_bridge_odom_tf.launch.pyBefore starting Nav2, check:
ros2 topic echo /odom --once
ros2 run tf2_ros tf2_echo odom base_linksource ~/unknown_px4_ws/install/setup.bash
ros2 launch unknown_px4_mapping px4_nav2_map.launch.pyThis launch file uses:
config/nav2_map_params.yaml
maps/outdoor_px4_slam_map.yaml
source ~/unknown_px4_ws/install/setup.bash
ros2 launch unknown_px4_mapping px4_cmd_vel_to_px4.launch.pysource ~/unknown_px4_ws/install/setup.bash
rviz2Open RViz2 config in this path:
~/unknown_px4_ws/src/rviz2/px4_nav2_map.rviz
Check if Nav2 nodes are active:
ros2 lifecycle get /map_server
ros2 lifecycle get /amcl
ros2 lifecycle get /controller_server
ros2 lifecycle get /planner_server
ros2 lifecycle get /behavior_server
ros2 lifecycle get /bt_navigatorExpected:
active [3]
Nav2 requires:
map -> odom -> base_link -> link
Check odom -> base_link:
ros2 run tf2_ros tf2_echo odom base_linkCheck map -> odom:
ros2 run tf2_ros tf2_echo map odomCheck map -> base_link:
ros2 run tf2_ros tf2_echo map base_linkNotes:
map -> odomis published by AMCL.odom -> base_linkis published bydynamic_pose_to_odom.py.base_link -> linkis published bystatic_transform_publisher.
Check map:
ros2 topic echo /map --onceCheck AMCL pose:
ros2 topic echo /amcl_pose --onceCheck odom:
ros2 topic echo /odom --onceCheck Nav2 velocity command:
ros2 topic echo /cmd_velCheck global plan:
ros2 topic echo /plan --onceCheck lidar:
ros2 topic echo /world/outdoor_slam_test/model/x500_lidar_2d_0/link/link/sensor/lidar_2d_v2/scan --onceNormally use the launch file:
ros2 launch unknown_px4_mapping px4_cmd_vel_to_px4.launch.pyDirect run method:
source ~/unknown_px4_ws/mavsdk_ros_env/bin/activate
python3 ~/unknown_px4_ws/src/unknown_px4_mapping/unknown_px4_mapping/cmd_vel_to_px4.py \
--ros-args \
-p use_sim_time:=true \
-p auto_arm:=true \
-p target_altitude:=1.5 \
-p altitude_kp:=0.18 \
-p max_vertical_speed:=0.10 \
-p max_forward_speed:=0.22 \
-p max_backward_speed:=0.00 \
-p min_forward_speed:=0.00 \
-p max_yaw_speed_deg:=20.0 \
-p min_yaw_speed_deg:=0.0 \
-p yaw_deadband_deg:=0.5 \
-p invert_yaw:=true \
-p smoothing_alpha:=0.20 \
-p cmd_vel_topic:=/cmd_vel \
-p path_topic:=/plan \
-p odom_topic:=/odom \
-p reset_on_new_path:=false \
-p reset_if_stuck_after_new_path:=true \
-p stuck_check_duration:=3.0 \
-p stuck_movement_threshold:=0.03 \
-p stuck_cmd_threshold:=0.005 \
-p stuck_reset_duration:=0.50 \
-p stuck_reset_cooldown:=5.0If base_link -> link is missing:
ros2 run tf2_ros static_transform_publisher \
0.12 0 0.02 \
0 0 0 \
base_link linkNormally use:
ros2 launch unknown_px4_mapping px4_bridge_odom_tf.launch.pyManual bridge command:
ros2 run ros_gz_bridge parameter_bridge \
/world/outdoor_slam_test/model/x500_lidar_2d_0/link/link/sensor/lidar_2d_v2/scan@sensor_msgs/msg/LaserScan[gz.msgs.LaserScan \
/world/outdoor_slam_test/dynamic_pose/info@tf2_msgs/msg/TFMessage[gz.msgs.Pose_V \
/clock@rosgraph_msgs/msg/Clock[gz.msgs.ClockManual dynamic pose to odom:
ros2 run unknown_px4_mapping dynamic_pose_to_odom \
--ros-args \
-p use_sim_time:=true \
-p pose_topic:=/world/outdoor_slam_test/dynamic_pose/info \
-p pose_index:=0Check dynamic pose:
ros2 topic echo /world/outdoor_slam_test/dynamic_pose/info --onceCheck node:
ros2 node list | grep dynamicCheck topic:
ros2 topic list | grep odomRun dynamic pose node manually:
ros2 run unknown_px4_mapping dynamic_pose_to_odom \
--ros-args \
-p use_sim_time:=true \
-p pose_topic:=/world/outdoor_slam_test/dynamic_pose/info \
-p pose_index:=0Check:
ros2 run tf2_ros tf2_echo odom base_linkIf missing, start:
ros2 launch unknown_px4_mapping px4_bridge_odom_tf.launch.pyAMCL has not published map -> odom.
Check:
ros2 run tf2_ros tf2_echo map odomFix:
- Make sure AMCL is running.
- Set initial pose in RViz2 using 2D Pose Estimate.
- Check
/amcl_pose.
ros2 topic echo /amcl_pose --onceCorrect plugin names use ::, not /.
Correct:
plugin: "nav2_behaviors::Spin"
plugin: "nav2_behaviors::BackUp"
plugin: "nav2_behaviors::DriveOnHeading"
plugin: "nav2_behaviors::Wait"Wrong:
plugin: "nav2_behaviors/Spin"Correct:
plugin: "nav2_navfn_planner::NavfnPlanner"Wrong:
plugin: "nav2_navfn_planner/NavfnPlanner"Open RViz2:
rviz2Set:
Fixed Frame: map
Click:
2D Pose Estimate
Set the drone position on the map.
Close heavy displays in RViz2.
Use lower update rates in Nav2 config:
global_costmap:
global_costmap:
ros__parameters:
update_frequency: 1.0
publish_frequency: 1.0
local_costmap:
local_costmap:
ros__parameters:
update_frequency: 5.0
publish_frequency: 2.0Kasiphat Uppaphak
GitHub: wattanatum


