How to Simulate a Mobile Robot in Gazebo – ROS 2 Jazzy

In this tutorial, we will simulate and control a mobile robot in Gazebo. I will show you how to set up and control a mecanum wheel robot using ROS 2 Control and Gazebo

By the end of this tutorial, you will be able to build this:

gazebo-500-square-mecanum-controller

If you prefer to learn by video, you can follow the entire tutorial here:

Prerequisites

All my code for this project is located here on GitHub.

Create Packages

Navigate to your workspace, and create the following packages. You can replace the maintainer-name and maintainer email with your own information.

cd ~/ros2_ws/src/yahboom_rosmaster/
ros2 pkg create --build-type ament_cmake \
                --license BSD-3-Clause \
                --maintainer-name ubuntu \
                --maintainer-email automaticaddison@todo.com \
                yahboom_rosmaster_bringup
ros2 pkg create --build-type ament_cmake \
                --license BSD-3-Clause \
                --maintainer-name ubuntu \
                --maintainer-email automaticaddison@todo.com \
               yahboom_rosmaster_gazebo
ros2 pkg create --build-type ament_cmake \
                --license BSD-3-Clause \
                --maintainer-name ubuntu \
                --maintainer-email automaticaddison@todo.com \
                yahboom_rosmaster_system_tests

Update the package.xml files for all packages, including the metapackage. Be sure to add a good description line for each.

You can also update the metapackage with the new packages you just created.

cd yahboom_rosmaster
gedit package.xml
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>yahboom_rosmaster</name>
  <version>0.0.0</version>
  <description>ROSMASTER series robots by Yahboom (metapackage).</description>
  <maintainer email="automaticaddison@todo.todo">ubuntu</maintainer>
  <license>BSD-3-Clause</license>

  <buildtool_depend>ament_cmake</buildtool_depend>

  <exec_depend>yahboom_rosmaster_bringup</exec_depend>
  <exec_depend>yahboom_rosmaster_description</exec_depend>
  <exec_depend>yahboom_rosmaster_gazebo</exec_depend>
  <exec_depend>yahboom_rosmaster_system_tests</exec_depend>

  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

Edit package.xml

Now let’s make sure some key packages are installed to handle the integration between ROS 2 and Gazebo. One important package is ros_gz. Here is the GitHub repository, and here are the official installation instructions. Let’s walk through this together.

Open a terminal window, and go to your package.xml folder inside the yahboom_rosmaster_gazebo package.

cd ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_gazebo

Open the package.xml file.

Make sure it has this block:

<depend>controller_manager</depend>
<depend>gz_ros2_control</depend>
<depend>python3-numpy</depend>
<depend>rclcpp</depend>
<depend>ros_gz</depend>
<depend>ros_gz_bridge</depend>
<depend>ros_gz_image</depend>
<depend>ros_gz_sim</depend>
<depend>ros2_control</depend>
<depend>ros2_controllers</depend>
<depend>trajectory_msgs</depend>
<depend>xacro</depend>
<exec_depend>gz_ros2_control_demos</exec_depend>
<exec_depend>rqt_robot_steering</exec_depend>
<exec_depend>rviz_imu_plugin</exec_depend>

Edit CMakeLists.txt

Now open the CMakeLists.txt file, and add this block:

find_package(ament_cmake REQUIRED)
find_package(controller_manager REQUIRED)
find_package(gz_ros2_control REQUIRED)
find_package(rclcpp REQUIRED)
find_package(ros_gz REQUIRED)
find_package(ros_gz_bridge REQUIRED)
find_package(ros_gz_image REQUIRED)
find_package(ros_gz_sim REQUIRED)
find_package(ros2_control REQUIRED)
find_package(ros2_controllers REQUIRED)
find_package(trajectory_msgs REQUIRED)
find_package(xacro REQUIRED)

Build the Workspace

Now let’s build our workspace.

cd ~/ros2_ws/
rosdep install -i --from-path src --rosdistro $ROS_DISTRO -y
colcon build && source ~/.bashrc

Open a terminal window, and type:

echo "alias build='cd ~/ros2_ws/ && colcon build && source ~/.bashrc'" >> ~/.bashrc

Now going forward, any time you want to build you workspace, just type:

build

Test Your Installation

Let’s test to see if Gazebo and ROS 2 Control are working properly:

ros2 launch gz_ros2_control_demos diff_drive_example.launch.py

To move the robot from the terminal window, you can type:

ros2 topic pub /diff_drive_base_controller/cmd_vel geometry_msgs/msg/TwistStamped "{header: {stamp: now, frame_id: 'base_link'}, twist: {linear: {x: 0.2, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.5}}}" --rate 5

To stop the robot:

CTRL + C

ros2 topic pub /diff_drive_base_controller/cmd_vel geometry_msgs/msg/TwistStamped "{header: {stamp: now, frame_id: 'base_link'}, twist: {linear: {x: 0.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}}" --rate 5

Create World Files

Now we will create an environment for our simulated robot.

Open a terminal window, and type:

cd ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_gazebo/

Create a worlds folder and other folders we will need later:

mkdir -p worlds launch models

Now let’s create our world. The first world we will create is an empty world.

cd worlds
gedit empty.world

Add this code.

Save the file, and close it.

Now launch your Gazebo world:

cd ~/ros2_ws/src/mycobot_ros2/mycobot_gazebo/worlds/
gz sim empty.world

Press CTRL + C to close everything.

Now let’s create a world called pick_and_place_demo.world.

gedit pick_and_place_demo.world

Add this code.

Now let’s create a world called house.world.

gedit house.world

Add this code.

Save the file, and close it.

Now let’s create a world called cafe.world.

gedit cafe.world

Add this code.

Save the file, and close it.

Add all these models to your models folder. 

Create a Mecanum Drive Controller

We need to create a controller for a mobile robot with mecanum omnidirectional wheels since none exists in the official ros2_controllers package. The official instructions for creating a new controller are on this page. We will adapt these instructions to your use case.

Create a Package

cd ~/ros2_ws/src/yahboom_rosmaster/
ros2 pkg create --build-type ament_cmake \
                --license BSD-3-Clause \
                --maintainer-name ubuntu \
                --maintainer-email automaticaddison@todo.com \
                mecanum_drive_controller

Now let’s prepare our source files. Move into the new package directory and create the necessary folders:

cd mecanum_drive_controller
mkdir -p test config

Prepare Files

Let’s create the header files:

touch include/mecanum_drive_controller/mecanum_drive_controller.hpp
touch include/mecanum_drive_controller/odometry.hpp
touch include/mecanum_drive_controller/speed_limiter.hpp
touch include/mecanum_drive_controller/visibility_control.h

Next, create the source files we need to implement:

touch src/mecanum_drive_controller.cpp
touch src/mecanum_drive_controller_parameter.yaml
touch src/odometry.cpp
touch src/speed_limiter.cpp

Create the test files we need to create:

mkdir -p test/config/
touch test/config/test_mecanum_drive_controller.yaml
touch test/test_mecanum_drive_controller.cpp
touch test/test_load_mecanum_drive_controller.cpp

Create other files we need to implement:

touch mecanum_drive_plugin.xml

Add Declarations into the Header File

Open mecanum_drive_controller.hpp inside the include/mecanum_drive_controller folder.

Add this code.

Save the file, and close it.

Open odometry.hpp inside the include/mecanum_drive_controller folder.

Add this code.

Save the file, and close it.

Open speed_limiter.hpp inside the include/mecanum_drive_controller folder.

Add this code.

Save the file, and close it.

Open visibility_control.h inside the include/mecanum_drive_controller folder.

Add this code.

Save the file, and close it.

Mecanum Wheel Forward and Inverse Kinematics

Before we write our source code, let’s dive into the mathematics that make omnidirectional movement possible. This foundation will be important when we implement our controller in the next section.

1-mecanum-wheel-symbol-definitions

The coordinate system for our robot follows ROS 2 conventions. The base frame’s X-axis points forward, Y-axis points left, and rotations follow the right-hand rule with positive counterclockwise rotation. We number our wheels starting from the front-left as Wheel 1 (FL), moving clockwise to Wheel 2 (FR), Wheel 3 (RL), and Wheel 4 (RR).

Understanding Wheel Rotation Direction

Before diving into the kinematics equations, we need to understand how wheel rotations are defined in our system. 

Each wheel’s rotation axis aligns with its positive y-axis, which runs parallel to the robot base’s y-axis. You can see this in the mecanum_wheel.urdf.xacro file. A positive wheel velocity value indicates counterclockwise rotation when viewed down this y-axis (imagine looking at the wheel from its side with the y-axis pointing towards you).

This convention has practical implications. For example, when commanding the robot to move forward, each wheel rotates counterclockwise around its y-axis, creating a positive velocity value. Think of the wheel’s 2D plane (formed by its positive z and x axes) rotating counterclockwise around the y-axis. This coordinated motion drives the robot base forward.

Forward Kinematics: From Wheel Speeds to Robot Motion

2-mecanum-wheel-forward-kinematics

The forward kinematics equations tell us how individual wheel velocities combine to create the robot’s overall motion. These equations are particularly useful when we need to estimate the robot’s actual movement based on wheel encoder feedback.

Looking at the equations, we can see how each wheel contributes to the robot’s linear velocities (vx and vy) and angular velocity (ωz). The wheel radius R scales the contribution of each wheel’s angular velocity (ω1 through ω4).

Inverse Kinematics for Motion Control

3-mecanum-wheel-inverse-kinematics

In our ROS 2 controller, we’ll primarily use inverse kinematics. These equations help us calculate the required wheel velocities to achieve desired robot motion. When we receive velocity commands (typically from the /cmd_vel topic), we’ll use these equations to determine appropriate wheel speeds.

The inverse kinematics equations consider:

  • The robot’s desired linear velocities (vx and vy)
  • The desired angular velocity (ωz)
  • The robot’s physical dimensions (L and W)
  • The wheel radius (R)

In our mecanum drive controller we transform velocity commands into actual wheel motions. We also handle odometry feedback using the forward kinematics equations to provide accurate motion estimation of the full robot.

Mecanum Wheel Kinematics in ROS 2: Translating Equations into Code

Now that we understand the mathematics behind forward and inverse kinematics, let’s map these equations to ROS 2 concepts. This step bridges the gap between theory and implementation, ensuring a smooth transition into developing a functional mecanum drive controller.

Forward Kinematics in ROS 2

4-forward-kinematics-mecanum-wheel

These equations estimate the robot’s motion based on wheel encoder feedback, providing odometry as linear and angular velocities in meters per second and radians per second.

Inverse Kinematics in ROS 2

5-inverse-kinematics-mecanum-wheel

These equations allow us to translate velocity commands into individual wheel speeds.

Create Source Files

Now let’s create the source files.

cd ~/ros2_ws/src/yahboom_rosmaster/mecanum_drive_controller/

Open speed_limiter.cpp inside the src folder.

Add this code.

Save the file, and close it.

Open mecanum_drive_controller_parameter.yaml inside the src folder.

Add this code.

Save the file, and close it.

Open odometry.cpp inside the src folder.

Add this code.

Save the file, and close it.

Open mecanum_drive_controller.cpp inside the src folder.

Add this code.

Save the file, and close it.

Write Export Definition for Pluginlib

Now we need to create the pluginlib XML file in the package root.

cd ~/ros2_ws/src/yahboom_rosmaster/mecanum_drive_controller/

Open mecanum_drive_plugin.xml

Add this code.

Save the file, and close it.

Write a Test to Check if the Controller Can Be Found and Loaded

Now we are going to write tests to confirm the controller can be found and loaded.

cd ~/ros2_ws/src/yahboom_rosmaster/mecanum_drive_controller/test

Open test_mecanum_drive_controller.yaml.

Add this code.

Save the file, and close it.

We are not going to use this yaml file. I just put it there as a reference to list all the available parameters for this controller.

Now let’s write the code to test if the mecanum drive controller can be loaded into the ROS 2 Control framework.

Open test_load_mecanum_drive_controller.cpp.

Add this code.

Save the file, and close it.

Add Dependencies into package.xml File

Now we need to update our package dependencies.

cd ~/ros2_ws/src/yahboom_rosmaster/mecanum_drive_controller/

Open the package.xml file.

Add this code.

Save the file, and close it.

Add Compile Directives into the CMakeLists.txt File

Now we need to update our CMakeLists.txt file.

cd ~/ros2_ws/src/yahboom_rosmaster/mecanum_drive_controller/

Open the CMakeLists.txt file.

Add this code.

Save the file, and close it.

Compile and Test the Controller

Let’s build our package to make sure everything is set up correctly:

cd ~/ros2_ws
rosdep install --from-paths src --ignore-src -r -y
colcon build --packages-select mecanum_drive_controller
source ~/.bashrc

Finally, let’s run the tests to ensure our controller can be found and loaded:

colcon test --packages-select mecanum_drive_controller
6-mecanum-controller-build-and-loaded

OR (for detailed output)

colcon test --packages-select mecanum_drive_controller --event-handlers console_direct+

You can also see your new controller if you type:

ros2 pkg list
7-ros2-mecanum-drive-controller

Connect Gazebo to ROS 2 Control 

Create a YAML Configuration File for the Controller Manager

After setting up our simulation environment in Gazebo, we need to tell ROS 2 Control how to manage our robot’s movements in this virtual world. Let’s create a configuration file that defines how our robot’s wheels should be controlled.

Open a terminal window, and type:

cd ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_description/
mkdir -p config/rosmaster_x3/

Create a new file inside the config/rosmaster_x3 directory called:

ros2_controllers.yaml

Add this code:

# controller_manager provides the necessary infrastructure to manage multiple controllers
# efficiently and robustly using ROS 2 Control.
controller_manager:
 ros__parameters:
   update_rate: 50  # Hz

   # Declare the controllers
   joint_state_broadcaster:
     type: joint_state_broadcaster/JointStateBroadcaster

   mecanum_drive_controller:
     type: mecanum_drive_controller/MecanumDriveController

# Parameters for the mecanum drive controller
mecanum_drive_controller:
 ros__parameters:
   # Joint names
   front_left_joint_name: front_left_wheel_joint
   front_right_joint_name: front_right_wheel_joint
   back_left_joint_name: back_left_wheel_joint
   back_right_joint_name: back_right_wheel_joint

   # Robot physical parameters
   wheel_base: 0.16
   wheel_separation: 0.169
   wheel_radius: 0.0325
   wheel_separation_multiplier: 1.0
   front_left_wheel_radius_multiplier: 1.0
   front_right_wheel_radius_multiplier: 1.0
   back_left_wheel_radius_multiplier: 1.0
   back_right_wheel_radius_multiplier: 1.0

   # TF configuration
   tf_frame_prefix_enable: false
   tf_frame_prefix: ""
   odom_frame_id: odom
   base_frame_id: base_footprint

   # Odometry parameters
   pose_covariance_diagonal: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
   twist_covariance_diagonal: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
   position_feedback: true
   open_loop: true
   enable_odom_tf: true

   # Command handling
   cmd_vel_timeout: 0.5
   publish_limited_velocity: false
   velocity_rolling_window_size: 10
   publish_rate: 50.0

   # Velocity limits
   linear.x.has_velocity_limits: false
   linear.x.has_acceleration_limits: false
   linear.x.has_jerk_limits: false
   linear.x.max_velocity: 0.0
   linear.x.min_velocity: 0.0
   linear.x.max_acceleration: 0.0
   linear.x.max_jerk: 0.0
   linear.x.min_jerk: 0.0
   linear.y.has_velocity_limits: false
   linear.y.has_acceleration_limits: false
   linear.y.has_jerk_limits: false
   linear.y.max_velocity: 0.0
   linear.y.min_velocity: 0.0
   linear.y.max_acceleration: 0.0
   linear.y.max_jerk: 0.0
   linear.y.min_jerk: 0.0
   angular.z.has_velocity_limits: false
   angular.z.has_acceleration_limits: false
   angular.z.has_jerk_limits: false
   angular.z.max_velocity: 0.0
   angular.z.min_velocity: 0.0
   angular.z.max_acceleration: 0.0
   angular.z.min_acceleration: 0.0
   angular.z.max_jerk: 0.0
   angular.z.min_jerk: 0.0

Save the file, and close it.

Create a new file inside the config/rosmaster_x3 directory called:

ros2_controllers_template.yaml

Add this code, and save the file.

This file will help us substitute a prefix should we desire.

Update CMakeLists.txt with the new config folder.

Update yahboom_rosmaster_desciption/launch/robot_state_publisher.py with the relevant part to handle the finding and replacing.

cd ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_description/launch

Save the file, and close it.

Create the XACRO/URDF Files

Next, we need to add three essential XACRO/URDF files that bridge Gazebo simulation with ROS 2 Control. These files work alongside your controller configuration YAML file to create a complete control system.

gazebo_sim_ros2_control.urdf.xacro

Open a terminal window.

Type the following command:

cd ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_description/urdf/
mkdir control

Create the file named:

gazebo_sim_ros2_control.urdf.xacro

Add this code.

Save the file, and close it.

rosmaster_x3_ros2_control.urdf.xacro:

Open a terminal window.

Type the following command:

cd ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_description/urdf/control

Create the file named:

rosmaster_x3_ros2_control.urdf.xacro

Add this code.

Save the file, and close it.

velocity_control_plugin.urdf.xacro

One more file to add. Add this one. This plugin is used to send velocity commands to the robot.

<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro">
    <xacro:macro name="load_velocity_control_plugin" params="use_gazebo">
        <xacro:if value="${use_gazebo}">
            <gazebo>
                <plugin
                    filename="gz-sim-velocity-control-system"
                    name="gz::sim::systems::VelocityControl">
                    <topic>mecanum_drive_controller/cmd_vel</topic>
                    <initial_linear>0 0 0</initial_linear>
                    <initial_angular>0 0 0</initial_angular>
                </plugin>
            </gazebo>
        </xacro:if>
    </xacro:macro>
</robot>

Include the Files in rosmaster_x3.urdf.xacro

Next we need to include these files inside rosmaster_x3.urdf.xacro. 

cd ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_description/urdf/robots/

Open the rosmaster_x3_urdf.xacro file.

Add this code at the end before the </robot> tag:

     <xacro:include filename="$(find yahboom_rosmaster_description)/urdf/control/gazebo_sim_ros2_control.urdf.xacro" />
      <xacro:load_gazebo_sim_ros2_control_plugin
        robot_name="$(arg robot_name)"
        use_gazebo="$(arg use_gazebo)"/>

      <xacro:include filename="$(find yahboom_rosmaster_description)/urdf/control/rosmaster_x3_ros2_control.urdf.xacro" />
      <xacro:yahboom_rosmaster_ros2_control
        prefix="$(arg prefix)"
        use_gazebo="$(arg use_gazebo)"/>

Save the file, and close it.

Create the Launch Files

Now let’s create the launch files.

load_ros2_controllers.launch.py

cd  ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_bringup/
mkdir launch
cd launch

Add this code called load_ros2_controllers.launch.py:

#!/usr/bin/env python3
"""
Launch ROS 2 controllers for the mecanum wheel robot.

This script creates a launch description that starts the necessary controllers
for operating the mecanum wheel robot in a specific sequence.

Launched Controllers:
    1. Joint State Broadcaster: Publishes joint states to /joint_states
    2. Mecanum Drive Controller: Controls the robot's mecanum drive movements via ~/cmd_vel

:author: Addison Sears-Collins
:date: November 20, 2024
"""

from launch import LaunchDescription
from launch.actions import ExecuteProcess, RegisterEventHandler, TimerAction
from launch.event_handlers import OnProcessExit


def generate_launch_description():
    """Generate a launch description for sequentially starting robot controllers.

    The function creates a launch sequence that ensures controllers are started
    in the correct order.

    Returns:
        LaunchDescription: Launch description containing sequenced controller starts
    """
    # Start mecanum drive controller
    start_mecanum_drive_controller_cmd = ExecuteProcess(
        cmd=['ros2', 'control', 'load_controller', '--set-state', 'active',
             'mecanum_drive_controller'],
        output='screen'
    )

    # Start joint state broadcaster
    start_joint_state_broadcaster_cmd = ExecuteProcess(
        cmd=['ros2', 'control', 'load_controller', '--set-state', 'active',
             'joint_state_broadcaster'],
        output='screen'
    )

    # Add delay to joint state broadcaster
    delayed_start = TimerAction(
        period=20.0,
        actions=[start_joint_state_broadcaster_cmd]
    )

    # Register event handler for sequencing
    load_joint_state_broadcaster_cmd = RegisterEventHandler(
        event_handler=OnProcessExit(
            target_action=start_joint_state_broadcaster_cmd,
            on_exit=[start_mecanum_drive_controller_cmd]))

    # Create and populate the launch description
    ld = LaunchDescription()

    # Add the actions to the launch description in sequence
    ld.add_action(delayed_start)
    ld.add_action(load_joint_state_broadcaster_cmd)

    return ld

Save the file, and close it.

Update CMakeLists.txt for the yahboom_rosmaster_bringup package to include the new launch folder. Make sure the block below, looks like this:

# Copy necessary files to designated locations in the project
install (
  DIRECTORY launch
  DESTINATION share/${PROJECT_NAME}
)

Create YAML File for ROS 2 Bridge

We now need to create a YAML file that bridges between ROS 2 and Gazebo topics. This file will specify how sensor data (camera info, point clouds, IMU data, and LIDAR data) should be translated between the Gazebo simulation environment and the ROS 2 ecosystem. The configuration ensures that simulated sensor data can be used by ROS 2 nodes for perception and planning tasks.

cd ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_gazebo/
mkdir config

Update CMakeLists.txt with the new config folder.

# Copy necessary files to designated locations in the project
install (
  DIRECTORY config
  DESTINATION share/${PROJECT_NAME}
)

Create a file called ros_gz_bridge.yaml inside the config folder.

Add this code.

# RGBD Camera topics
- ros_topic_name: "cam_1/color/camera_info"
  gz_topic_name: "cam_1/camera_info"
  ros_type_name: "sensor_msgs/msg/CameraInfo"
  gz_type_name: "gz.msgs.CameraInfo"
  direction: GZ_TO_ROS
  lazy: true # Determines whether connections are created immediately at startup (when false) or only when data is actually requested by a subscriber (when true), helping to conserve system resources at the cost of potential initial delays in data flow.

- ros_topic_name: "cam_1/depth/color/points"
  gz_topic_name: "cam_1/points"
  ros_type_name: "sensor_msgs/msg/PointCloud2"
  gz_type_name: "gz.msgs.PointCloudPacked"
  direction: GZ_TO_ROS
  lazy: true

  # LIDAR configuration
- ros_topic_name: "scan"
  gz_topic_name: "scan"
  ros_type_name: "sensor_msgs/msg/LaserScan"
  gz_type_name: "gz.msgs.LaserScan"
  direction: GZ_TO_ROS
  lazy: false

# IMU configuration
- ros_topic_name: "imu/data"
  gz_topic_name: "imu/data"
  ros_type_name: "sensor_msgs/msg/Imu"
  gz_type_name: "gz.msgs.IMU"
  direction: GZ_TO_ROS
  lazy: false

# Sending velocity commands from ROS 2 to Gazebo
- ros_topic_name: "mecanum_drive_controller/cmd_vel"
  gz_topic_name: "mecanum_drive_controller/cmd_vel"
  ros_type_name: "geometry_msgs/msg/TwistStamped"
  gz_type_name: "gz.msgs.Twist"
  direction: ROS_TO_GZ
  lazy: false

Save the file, and close it.

Create an RViz configuration file.

cd ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_gazebo/
mkdir rviz

Add this file to the rviz folder: yahboom_rosmaster_gazebo_sim.rviz.

Update CMakeLists.txt

Go to the CMakeLists.txt.

cd ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_gazebo/

Make sure it has this block of code:

# Copy necessary files to designated locations in the project
install (
  DIRECTORY launch models worlds rviz
  DESTINATION share/${PROJECT_NAME}
)
gedit CMakeLists.txt

Save the file, and close it.

yahboom_rosmaster.gazebo.launch.py

cd ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_gazebo/launch

Create a new file called yahboom_rosmaster.gazebo.launch.py.

Add this code.

Save the file, and close it.

Create a Bash Script for Quick Launching

Create a bash script to launch multiple launch files:

cd  ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_bringup/
mkdir scripts
Add a bash file called: rosmaster_x3_gazebo.sh.

Add this code.

Save the file, and close it.

Make it executable.

sudo chmod +x rosmaster_x3_gazebo.sh

I will add an alias for quick launch:

echo "alias x3='bash ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_bringup/scripts/rosmaster_x3_gazebo.sh'" >> ~/.bashrc && source ~/.bashrc

Make sure to update CMakeLists.txt for the yahboom_rosmaster_bringup folder.

Build the Workspace

Now let’s build the package. Open a terminal window, and type:

build

Launch Everything and Move the Robot

Now let’s launch everything.

Open a terminal window, and type the following command:

x3

or

bash ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_bringup/scripts/rosmaster_x3_gazebo.sh

To change the default camera view in Gazebo, we will need to use the /gui/move_to/pose service (found via gz service -l):

gz service -s /gui/move_to/pose --reqtype gz.msgs.GUICamera --reptype gz.msgs.Boolean --timeout 2000 --req "pose: {position: {x: 0.0, y: -2.0, z: 2.0} orientation: {x: -0.2706, y: 0.2706, z: 0.6533, w: 0.6533}}"

I added this command directly to the bash startup script.

Be patient. It can take up to 60 seconds to load everything.

9-gazebo-rviz-simulation-twin

To move the robot, type this:

ros2 topic pub /mecanum_drive_controller/cmd_vel geometry_msgs/msg/TwistStamped "{header: {stamp: {sec: $(date +%s), nanosec: 0}, frame_id: ''}, twist: {linear: {x: 0.0, y: 0.1, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}}"

Press CTRL + C.

Now stop the robot:

ros2 topic pub /mecanum_drive_controller/cmd_vel geometry_msgs/msg/TwistStamped "{header: {stamp: {sec: $(date +%s), nanosec: 0}, frame_id: ''}, twist: {linear: {x: 0.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}}"

Here is the output using the empty.world file.

8-mecanum-wheel-robot-gazebo-ros2-simulation

To see a list of active topics, type:

ros2 topic list
10-ros2-topics

To see a list of active topics, type:

gz topic -l
11-gz-topics

To see the options, you can type:

gz topic --help

See the active controllers:

ros2 control list_controllers
12-active-controllers

To get more information, type:

ros2 control list_controllers -v
13-list-controllers

Let’s see the information coming across some of the ROS 2 topics.

ros2 topic echo /mecanum_drive_controller/cmd_vel displays the velocity commands sent to the robot, telling it how fast and in which direction to move.

ros2 topic echo /joint_states shows the current positions (in radians) and velocities (in radians per second) of the robot’s wheels.

ros2 topic echo/mecanum_drive_controller/odom provides information about the robot’s estimated position, orientation, linear, and angular velocity based on wheel rotation data.

ros2 topic echo /imu/data outputs readings from the robot’s inertial measurement unit (IMU), which includes data about angular velocity and linear acceleration.

ros2 topic echo /scan displays data from the robot’s laser scanner or LiDAR, providing information about the distances to obstacles in the environment.

You can see the /scan data, if you go to RViz, and click the Add button in the Displays panel on the left.

14-click-add

Click the “By topic” tab, and scroll down to LaserScan. Click on that, and click OK to add it.

15-click-scan

You can see the LIDAR scan data.

16-scans

Here is what things look like when launching the robot in pick_and_place_demo.world. You can see how the scans match with the objects in the environment.

17-scans

To see the frequency of publishing, type:

ros2 topic hz /scan

To get information about this topic, type:

ros2 topic info /scan

I also recommend checking out /cam_1/color/image_raw in RViz to see if the camera transformation for the cam_1_depth_optical_frame is oriented correctly. Compare Gazebo (on the left) to what the camera is actually seeing in ROS 2 (on the right).

17-color-image-raw

Click the “By topic” tab.

Click /cam_1 -> /depth -> /color -> /points -> PointCloud2 so you can see the point cloud in RViz.

While the point cloud data shows perfectly fine in my robotic arm applications with Gazebo and RViz, I had big time rendering issues when trying to get it to display in RViz. You can see these issues in the image below. I suspect it is related to the amount of memory on my virtual machine given all the sensor data that is flowing through from Gazebo to ROS 2.

18-rendering-issues-point-cloud-gazebo

If you want to move the robot forward, type this:

forward-motion
ros2 topic pub /mecanum_drive_controller/cmd_vel geometry_msgs/msg/TwistStamped "{
  header: {
    stamp: {sec: `date +%s`, nanosec: 0},
    frame_id: ''
  },
  twist: {
    linear: {
      x: 0.05,
      y: 0.0,
      z: 0.0
    },
    angular: {
      x: 0.0,
      y: 0.0,
      z: 0.0
    }
  }
}"

If you want to move the robot backward, type this:

backward_motion

ros2 topic pub /mecanum_drive_controller/cmd_vel geometry_msgs/msg/TwistStamped "{
  header: {
    stamp: {sec: `date +%s`, nanosec: 0},
    frame_id: ''
  },
  twist: {
    linear: {
      x: -0.05,
      y: 0.0,
      z: 0.0
    },
    angular: {
      x: 0.0,
      y: 0.0,
      z: 0.0
    }
  }
}"

If you want to move the robot to the left, type this:

left-motion
ros2 topic pub /mecanum_drive_controller/cmd_vel geometry_msgs/msg/TwistStamped "{
  header: {
    stamp: {sec: `date +%s`, nanosec: 0},
    frame_id: ''
  },
  twist: {
    linear: {
      x: 0.0,
      y: 0.05,
      z: 0.0
    },
    angular: {
      x: 0.0,
      y: 0.0,
      z: 0.0
    }
  }
}"

If you want to move the robot to the right, type this:

right-motion
ros2 topic pub /mecanum_drive_controller/cmd_vel geometry_msgs/msg/TwistStamped "{
  header: {
    stamp: {sec: `date +%s`, nanosec: 0},
    frame_id: ''
  },
  twist: {
    linear: {
      x: 0.0,
      y: -0.05,
      z: 0.0
    },
    angular: {
      x: 0.0,
      y: 0.0,
      z: 0.0
    }
  }
}"

If you want to rotate the robot in place clockwise, type this:

clockwise-negative-rotation-in-place
ros2 topic pub /mecanum_drive_controller/cmd_vel geometry_msgs/msg/TwistStamped "{
  header: {
    stamp: {sec: `date +%s`, nanosec: 0},
    frame_id: ''
  },
  twist: {
    linear: {
      x: 0.0,
      y: 0.0,
      z: 0.0
    },
    angular: {
      x: 0.0,
      y: 0.0,
      z: -0.4
    }
  }
}"

Counter-clockwise:

counter-clockwise-positive-rotation-in-place
ros2 topic pub /mecanum_drive_controller/cmd_vel geometry_msgs/msg/TwistStamped "{
  header: {
    stamp: {sec: `date +%s`, nanosec: 0},
    frame_id: ''
  },
  twist: {
    linear: {
      x: 0.0,
      y: 0.0,
      z: 0.0
    },
    angular: {
      x: 0.0,
      y: 0.0,
      z: 0.4
    }
  }
}"

Diagonal forward-left:

front-left-diagonal
ros2 topic pub /mecanum_drive_controller/cmd_vel geometry_msgs/msg/TwistStamped "{
  header: {
    stamp: {sec: `date +%s`, nanosec: 0},
    frame_id: ''
  },
  twist: {
    linear: {
      x: 0.05,
      y: 0.05,
      z: 0.0
    },
    angular: {
      x: 0.0,
      y: 0.0,
      z: 0.0
    }
  }
}"

Diagonal forward-right:

front-right-diagonal
ros2 topic pub /mecanum_drive_controller/cmd_vel geometry_msgs/msg/TwistStamped "{
  header: {
    stamp: {sec: `date +%s`, nanosec: 0},
    frame_id: ''
  },
  twist: {
    linear: {
      x: 0.05,
      y: -0.05,
      z: 0.0
    },
    angular: {
      x: 0.0,
      y: 0.0,
      z: 0.0
    }
  }
}"

Diagonal backward left:

back-left-diagonal
ros2 topic pub /mecanum_drive_controller/cmd_vel geometry_msgs/msg/TwistStamped "{
  header: {
    stamp: {sec: `date +%s`, nanosec: 0},
    frame_id: ''
  },
  twist: {
    linear: {
      x: -0.05,
      y: 0.05,
      z: 0.0
    },
    angular: {
      x: 0.0,
      y: 0.0,
      z: 0.0
    }
  }
}"

Diagonal backward-right:

back-right-diagonal
ros2 topic pub /mecanum_drive_controller/cmd_vel geometry_msgs/msg/TwistStamped "{
  header: {
    stamp: {sec: `date +%s`, nanosec: 0},
    frame_id: ''
  },
  twist: {
    linear: {
      x: -0.05,
      y: -0.05,
      z: 0.0
    },
    angular: {
      x: 0.0,
      y: 0.0,
      z: 0.0
    }
  }
}"

Press CTRL + C to close everything out.

Control the Robot with Code

Let’s create a ROS 2 node that can make the robot move repeatedly in a square pattern. 

Let’s start by creating a program.

cd ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_system_tests/src

Add a file called square_mecanum_controller.cpp.

/**
 * @file square_mecanum_controller.cpp
 * @brief Controls a mecanum wheeled robot to move in a square pattern
 *
 * This program creates a ROS 2 node that publishes velocity commands to make a
 * mecanum wheeled robot move in a square pattern. It takes advantage of the
 * omnidirectional capabilities of mecanum wheels to move in straight lines
 * along both X and Y axes.
 *
 * Publishing Topics:
 *     /mecanum_drive_controller/cmd_vel (geometry_msgs/TwistStamped):
 *         Velocity commands for the robot's motion
 *
 * @author Addison Sears-Collins
 * @date November 22, 2024
 */

#include <chrono>
#include <functional>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "geometry_msgs/msg/twist_stamped.hpp"

using namespace std::chrono_literals;

class SquareController : public rclcpp::Node
{
public:
    SquareController() : Node("square_controller")
    {
        // Create publisher for velocity commands
        publisher_ = this->create_publisher<geometry_msgs::msg::TwistStamped>(
            "/mecanum_drive_controller/cmd_vel", 10);

        // Create timer that calls our control function every 200ms
        timer_ = this->create_wall_timer(
            200ms, std::bind(&SquareController::timer_callback, this));

        // Initialize variables
        current_side_ = 0;  // Which side of square we're on (0-3)
        elapsed_time_ = 0.0;  // Time spent on current side
        robot_speed_ = 0.2;  // Speed in meters/second
        side_length_ = 2.0;  // Length of each side in meters
        time_per_side_ = side_length_ / robot_speed_;  // Time to complete one side
    }

    // Send stop command when node is shut down
    ~SquareController()
    {
        stop_robot();
    }

    // Function to stop the robot
    void stop_robot()
    {
        auto msg = geometry_msgs::msg::TwistStamped();
        msg.header.stamp = this->now();
        publisher_->publish(msg);  // All velocities default to 0
    }

private:
    void timer_callback()
    {
        // Create velocity command message
        auto msg = geometry_msgs::msg::TwistStamped();
        msg.header.stamp = this->now();

        // Set velocity based on which side of the square we're on
        switch (current_side_) {
            case 0:  // Moving forward (positive X)
                msg.twist.linear.x = robot_speed_;
                break;
            case 1:  // Moving left (positive Y)
                msg.twist.linear.y = robot_speed_;
                break;
            case 2:  // Moving backward (negative X)
                msg.twist.linear.x = -robot_speed_;
                break;
            case 3:  // Moving right (negative Y)
                msg.twist.linear.y = -robot_speed_;
                break;
        }

        // Publish the velocity command
        publisher_->publish(msg);

        // Update time tracking
        elapsed_time_ += 0.2;  // 200ms in seconds

        // Check if we've completed the current side
        if (elapsed_time_ >= time_per_side_) {
            elapsed_time_ = 0.0;
            current_side_ = (current_side_ + 1) % 4;  // Move to next side (0-3)
        }
    }

    // Class member variables
    rclcpp::TimerBase::SharedPtr timer_;
    rclcpp::Publisher<geometry_msgs::msg::TwistStamped>::SharedPtr publisher_;
    int current_side_;
    double elapsed_time_;
    double side_length_;
    double robot_speed_;
    double time_per_side_;
};

int main(int argc, char * argv[])
{
    rclcpp::init(argc, argv);
    auto node = std::make_shared<SquareController>();
    rclcpp::spin(node);
    rclcpp::shutdown();
    return 0;
}

Update your CMakeLists.txt file and package.xml file.

build

Launch your robot in Gazebo.

x3

Now run your node:

ros2 run yahboom_rosmaster_system_tests square_mecanum_controller

Your robot will move in a pattern that resembles a square.

That’s it! You made it to the end! Congratulations!

Keep building!

Create Launch Files to Display URDF Files – ROS 2 Jazzy

In this tutorial, I will guide you through the process of creating a custom launch file to launch a robotic arm and a mobile robot in RViz.

RViz (short for “ROS Visualization”) is a 3D visualization tool for robots that allows you to view the robot’s sensors, environment, and state. I use it all the time to visualize and debug my robots.

Launch files in ROS 2 are powerful tools that allow you to start multiple nodes and set parameters with a single command, simplifying the process of managing your robot’s complex systems.

Prerequisites

All my code for this project is located here on GitHub (robotic arm) and here on GitHub (mobile robot).

Create the Launch File for the Robotic Arm

Open a terminal window.

Move to your robotic arm directory.

cd ~/ros2_ws/src/mycobot_ros2/mycobot_description/ && mkdir launch && cd launch

Add a file in the launch folder called robot_state_publisher.launch.py.

Add this code.

#!/usr/bin/env python3
#
# Author: Addison Sears-Collins
# Date: November 10, 2024
# Description: Display the robotic arm with RViz
#
# This file launches the robot state publisher, joint state publisher,
# and RViz2 for visualizing the mycobot robot.

from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.conditions import IfCondition, UnlessCondition
from launch.substitutions import Command, LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.parameter_descriptions import ParameterValue
from launch_ros.substitutions import FindPackageShare

# Define the arguments for the XACRO file
ARGUMENTS = [
    DeclareLaunchArgument('prefix', default_value='',
                          description='Prefix for robot joints and links'),
    DeclareLaunchArgument('add_world', default_value='true',
                          choices=['true', 'false'],
                          description='Whether to add world link'),
    DeclareLaunchArgument('base_link', default_value='base_link',
                          description='Name of the base link'),
    DeclareLaunchArgument('base_type', default_value='g_shape',
                          description='Type of the base'),
    DeclareLaunchArgument('flange_link', default_value='link6_flange',
                          description='Name of the flange link'),
    DeclareLaunchArgument('gripper_type', default_value='adaptive_gripper',
                          description='Type of the gripper'),
    DeclareLaunchArgument('use_gazebo', default_value='false',
                          choices=['true', 'false'],
                          description='Whether to use Gazebo simulation'),
    DeclareLaunchArgument('use_gripper', default_value='true',
                          choices=['true', 'false'],
                          description='Whether to attach a gripper')
]


def generate_launch_description():
    # Define filenames
    urdf_package = 'mycobot_description'
    urdf_filename = 'mycobot_280.urdf.xacro'
    rviz_config_filename = 'mycobot_280_description.rviz'

    # Set paths to important files
    pkg_share_description = FindPackageShare(urdf_package)
    default_urdf_model_path = PathJoinSubstitution(
        [pkg_share_description, 'urdf', 'robots', urdf_filename])
    default_rviz_config_path = PathJoinSubstitution(
        [pkg_share_description, 'rviz', rviz_config_filename])

    # Launch configuration variables
    jsp_gui = LaunchConfiguration('jsp_gui')
    rviz_config_file = LaunchConfiguration('rviz_config_file')
    urdf_model = LaunchConfiguration('urdf_model')
    use_rviz = LaunchConfiguration('use_rviz')
    use_sim_time = LaunchConfiguration('use_sim_time')

    # Declare the launch arguments
    declare_jsp_gui_cmd = DeclareLaunchArgument(
        name='jsp_gui',
        default_value='true',
        choices=['true', 'false'],
        description='Flag to enable joint_state_publisher_gui')

    declare_rviz_config_file_cmd = DeclareLaunchArgument(
        name='rviz_config_file',
        default_value=default_rviz_config_path,
        description='Full path to the RVIZ config file to use')

    declare_urdf_model_path_cmd = DeclareLaunchArgument(
        name='urdf_model',
        default_value=default_urdf_model_path,
        description='Absolute path to robot urdf file')

    declare_use_rviz_cmd = DeclareLaunchArgument(
        name='use_rviz',
        default_value='true',
        description='Whether to start RVIZ')

    declare_use_sim_time_cmd = DeclareLaunchArgument(
        name='use_sim_time',
        default_value='false',
        description='Use simulation (Gazebo) clock if true')

    robot_description_content = ParameterValue(Command([
        'xacro', ' ', urdf_model, ' ',
        'prefix:=', LaunchConfiguration('prefix'), ' ',
        'add_world:=', LaunchConfiguration('add_world'), ' ',
        'base_link:=', LaunchConfiguration('base_link'), ' ',
        'base_type:=', LaunchConfiguration('base_type'), ' ',
        'flange_link:=', LaunchConfiguration('flange_link'), ' ',
        'gripper_type:=', LaunchConfiguration('gripper_type'), ' ',
        'use_gazebo:=', LaunchConfiguration('use_gazebo'), ' ',
        'use_gripper:=', LaunchConfiguration('use_gripper')
    ]), value_type=str)

    # Subscribe to the joint states of the robot, and publish the 3D pose of each link.
    start_robot_state_publisher_cmd = Node(
        package='robot_state_publisher',
        executable='robot_state_publisher',
        name='robot_state_publisher',
        output='screen',
        parameters=[{
            'use_sim_time': use_sim_time,
            'robot_description': robot_description_content}])

    # Publish the joint state values for the non-fixed joints in the URDF file.
    start_joint_state_publisher_cmd = Node(
        package='joint_state_publisher',
        executable='joint_state_publisher',
        name='joint_state_publisher',
        parameters=[{'use_sim_time': use_sim_time}],
        condition=UnlessCondition(jsp_gui))

    # Depending on gui parameter, either launch joint_state_publisher or joint_state_publisher_gui
    start_joint_state_publisher_gui_cmd = Node(
        package='joint_state_publisher_gui',
        executable='joint_state_publisher_gui',
        name='joint_state_publisher_gui',
        parameters=[{'use_sim_time': use_sim_time}],
        condition=IfCondition(jsp_gui))

    # Launch RViz
    start_rviz_cmd = Node(
        condition=IfCondition(use_rviz),
        package='rviz2',
        executable='rviz2',
        name='rviz2',
        output='screen',
        arguments=['-d', rviz_config_file],
        parameters=[{'use_sim_time': use_sim_time}])

    # Create the launch description and populate
    ld = LaunchDescription(ARGUMENTS)

    # Declare the launch options
    ld.add_action(declare_jsp_gui_cmd)
    ld.add_action(declare_rviz_config_file_cmd)
    ld.add_action(declare_urdf_model_path_cmd)
    ld.add_action(declare_use_rviz_cmd)
    ld.add_action(declare_use_sim_time_cmd)

    # Add any actions
    ld.add_action(start_joint_state_publisher_cmd)
    ld.add_action(start_joint_state_publisher_gui_cmd)
    ld.add_action(start_robot_state_publisher_cmd)
    ld.add_action(start_rviz_cmd)

    return ld

Create the Launch File for the Mobile Robot

Open a terminal window.

Move to your mobile robot directory.

cd ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_description/ && mkdir launch && cd launch

Add a file in the launch folder called robot_state_publisher.launch.py.

Add this code.

#!/usr/bin/env python3
#
# Author: Addison Sears-Collins
# Date: November 11, 2024
# Description: Display the Yahboom (ROSMASTER) robot in RViz
#
# This file launches the robot state publisher, joint state publisher,
# and RViz2 for visualizing the ROSMASTER robot.

from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.conditions import IfCondition, UnlessCondition
from launch.substitutions import Command, LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.parameter_descriptions import ParameterValue
from launch_ros.substitutions import FindPackageShare

# Define the arguments for the XACRO file
ARGUMENTS = [
    DeclareLaunchArgument('prefix', default_value='',
                          description='Prefix for robot joints and links'),
    DeclareLaunchArgument('use_gazebo', default_value='false',
                          choices=['true', 'false'],
                          description='Whether to use Gazebo simulation')
]


def generate_launch_description():
    # Define filenames
    urdf_package = 'yahboom_rosmaster_description'
    urdf_filename = 'rosmaster_x3.urdf.xacro'
    rviz_config_filename = 'yahboom_rosmaster_description.rviz'

    # Set paths to important files
    pkg_share_description = FindPackageShare(urdf_package)
    default_urdf_model_path = PathJoinSubstitution(
        [pkg_share_description, 'urdf', 'robots', urdf_filename])
    default_rviz_config_path = PathJoinSubstitution(
        [pkg_share_description, 'rviz', rviz_config_filename])

    # Launch configuration variables
    jsp_gui = LaunchConfiguration('jsp_gui')
    rviz_config_file = LaunchConfiguration('rviz_config_file')
    urdf_model = LaunchConfiguration('urdf_model')
    use_rviz = LaunchConfiguration('use_rviz')
    use_sim_time = LaunchConfiguration('use_sim_time')

    # Declare the launch arguments
    declare_jsp_gui_cmd = DeclareLaunchArgument(
        name='jsp_gui',
        default_value='true',
        choices=['true', 'false'],
        description='Flag to enable joint_state_publisher_gui')

    declare_rviz_config_file_cmd = DeclareLaunchArgument(
        name='rviz_config_file',
        default_value=default_rviz_config_path,
        description='Full path to the RVIZ config file to use')

    declare_urdf_model_path_cmd = DeclareLaunchArgument(
        name='urdf_model',
        default_value=default_urdf_model_path,
        description='Absolute path to robot urdf file')

    declare_use_rviz_cmd = DeclareLaunchArgument(
        name='use_rviz',
        default_value='true',
        description='Whether to start RVIZ')

    declare_use_sim_time_cmd = DeclareLaunchArgument(
        name='use_sim_time',
        default_value='false',
        description='Use simulation (Gazebo) clock if true')

    robot_description_content = ParameterValue(Command([
        'xacro', ' ', urdf_model, ' ',
        'prefix:=', LaunchConfiguration('prefix'), ' ',
        'use_gazebo:=', LaunchConfiguration('use_gazebo')
    ]), value_type=str)

    # Subscribe to the joint states of the robot, and publish the 3D pose of each link.
    start_robot_state_publisher_cmd = Node(
        package='robot_state_publisher',
        executable='robot_state_publisher',
        name='robot_state_publisher',
        output='screen',
        parameters=[{
            'use_sim_time': use_sim_time,
            'robot_description': robot_description_content}])

    # Publish the joint state values for the non-fixed joints in the URDF file.
    start_joint_state_publisher_cmd = Node(
        package='joint_state_publisher',
        executable='joint_state_publisher',
        name='joint_state_publisher',
        parameters=[{'use_sim_time': use_sim_time}],
        condition=UnlessCondition(jsp_gui))

    # Depending on gui parameter, either launch joint_state_publisher or joint_state_publisher_gui
    start_joint_state_publisher_gui_cmd = Node(
        package='joint_state_publisher_gui',
        executable='joint_state_publisher_gui',
        name='joint_state_publisher_gui',
        parameters=[{'use_sim_time': use_sim_time}],
        condition=IfCondition(jsp_gui))

    # Launch RViz
    start_rviz_cmd = Node(
        condition=IfCondition(use_rviz),
        package='rviz2',
        executable='rviz2',
        output='screen',
        arguments=['-d', rviz_config_file],
        parameters=[{'use_sim_time': use_sim_time}])

    # Create the launch description and populate
    ld = LaunchDescription(ARGUMENTS)

    # Declare the launch options
    ld.add_action(declare_jsp_gui_cmd)
    ld.add_action(declare_rviz_config_file_cmd)
    ld.add_action(declare_urdf_model_path_cmd)
    ld.add_action(declare_use_rviz_cmd)
    ld.add_action(declare_use_sim_time_cmd)

    # Add any actions
    ld.add_action(start_joint_state_publisher_cmd)
    ld.add_action(start_joint_state_publisher_gui_cmd)
    ld.add_action(start_robot_state_publisher_cmd)
    ld.add_action(start_rviz_cmd)

    return ld

Add the RViz Configuration File

Now that we have written our launch file, let’s add the RViz configuration file.

cd ~/ros2_ws/src/mycobot_ros2/mycobot_description/ && mkdir rviz && cd rviz

Create a file named mycobot_280_description.rviz

Add this code.

cd ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_description/ && mkdir rviz && cd rviz

Create a file named yahboom_rosmaster_description.rviz 

Add this code.

You don’t need to understand all the nitty gritty details of this configuration file. You can generate one automatically through RViz.

On a high-level, RViz configuration files end with the extension .rviz. These files set up an RViz configuration with a grid, a robot model, and coordinate frame visualizations. 

This configuration file also enables the camera movement tool and sets the initial camera view to an orbit view, which allows orbiting around a focal point in the scene. 

When RViz is launched with this configuration file, it will display the robot model and allow interaction and visualization of the robot.

Edit CMakeLists.txt

Now we need to edit CMakeLists.txt so the build system can find our new folders, launch and rviz.

cd ~/ros2_ws/src/mycobot_ros2/mycobot_description/ 
gedit CMakeLists.txt

Add this code:

# Copy necessary files to designated locations in the project
install (
  DIRECTORY launch meshes urdf rviz
  DESTINATION share/${PROJECT_NAME}
)
cd ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_description/ 
gedit CMakeLists.txt

Add this code:

# Copy necessary files to designated locations in the project
install (
  DIRECTORY launch meshes urdf rviz
  DESTINATION share/${PROJECT_NAME}
)

Save the file, and close it.

Now build your workspace.

cd ~/ros2_ws/
colcon build
source ~/.bashrc

Launch the Launch Files

Launch your launch files:

ros2 launch mycobot_description robot_state_publisher.launch.py
1-mycobot-custom-launch-rviz
ros2 launch yahboom_rosmaster_description robot_state_publisher.launch.py

You can also add launch arguments. To see the available launch arguments, type:

ros2 launch mycobot_description robot_state_publisher.launch.py --show-args

For example, to disable the gripper, type:

ros2 launch mycobot_description robot_state_publisher.launch.py use_gripper:=false

To add a prefix to the robot (e.g. left arm of a dual arm robot), type:

ros2 launch mycobot_description robot_state_publisher.launch.py use_gripper:=false prefix:=left_
2-remove-gripper-and-add-prefix

If you want to launch the robots with no GUIs, do this:

ros2 launch mycobot_description robot_state_publisher.launch.py jsp_gui:=false use_rviz:=false
ros2 launch yahboom_rosmaster_description robot_state_publisher.launch.py jsp_gui:=false use_rviz:=false
3-yahboom-robot

And there you have it…your first custom launch files.

Launch File Walkthrough

Let’s walk through this ROS 2 launch file that visualizes a robotic arm in RViz.

Starting with the imports, we bring in essential ROS 2 launch utilities.

from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.conditions import IfCondition, UnlessCondition

The launch file’s core purpose is orchestrating multiple nodes – think of it as a conductor coordinating different musicians in an orchestra. Here, our musicians are:

  • Robot State Publisher: Broadcasts the robot’s current pose
  • Joint State Publisher: Manages the robot’s joint positions
  • RViz: Provides the 3D visualization

The ARGUMENTS list defines the robot’s configurable parameters:

ARGUMENTS = [
    DeclareLaunchArgument('prefix', default_value='',
                          description='Prefix for robot joints and links'),
    DeclareLaunchArgument('add_world', default_value='true',
                          description='Whether to add world link'),
    # ... other arguments
]

These arguments allow users to customize the robot’s configuration without changing the code – like using command-line switches to modify a program’s behavior.

The generate_launch_description() function is where the magic happens. First, it sets up the file paths:

urdf_package = 'mycobot_description'
urdf_filename = 'mycobot_280.urdf.xacro'
rviz_config_filename = 'mycobot_280_description.rviz'

The robot’s description is loaded from a XACRO file (an XML macro file) and converted into a robot_description parameter:

robot_description_content = ParameterValue(Command([
    'xacro', ' ', urdf_model, ' ',
    'prefix:=', LaunchConfiguration('prefix'),
    # ... other parameters
]), value_type=str)

Then we create three key nodes:

1. Robot State Publisher:

start_robot_state_publisher_cmd = Node(
    package='robot_state_publisher',
    executable='robot_state_publisher',
    name='robot_state_publisher',
    output='screen',
    parameters=[{
        'use_sim_time': use_sim_time,
        'robot_description': robot_description_content}])

This node takes the robot description and joint states and publishes the 3D poses of all robot links – like a GPS system for each part of the robot.

2. Joint State Publisher (with optional GUI):

start_joint_state_publisher_cmd = Node(
    package='joint_state_publisher',
    executable='joint_state_publisher',
    name='joint_state_publisher',
    parameters=[{'use_sim_time': use_sim_time}],
    condition=UnlessCondition(jsp_gui))

This publishes joint positions – imagine it as the robot’s muscle control center. The GUI version allows manual control of these joints.

3. RViz:

start_rviz_cmd = Node(
    condition=IfCondition(use_rviz),
    package='rviz2',
    executable='rviz2',
    name='rviz2',
    output='screen',
    arguments=['-d', rviz_config_file])

This is our visualization tool, loading a pre-configured layout specified in the RViz config file.

Finally, everything is assembled into a LaunchDescription and returned:

ld = LaunchDescription(ARGUMENTS)
# Add all the actions...
return ld

This launch file structure is common in ROS 2 applications, providing a clean way to start multiple nodes simultaneously.

That’s it.

Keep building, and I will see you in the next tutorial!

Create and Visualize a Mobile Robot with URDF – ROS 2 Jazzy

In this tutorial, we will create a model of a mobile robot from scratch. Our robot model will be in the standard Unified Robot Description Format (URDF). 

By the end of this tutorial, you will be able to build this:

12-full-robot

We will then visualize the robot in RViz, a 3D visualization tool for ROS 2.

The official tutorial for creating a URDF file is here on the ROS 2 website; but that tutorial only deals with a fictitious robot.

It is far more fun and helpful to show you how to create a URDF file for a real-world robot, like the ones you will work with at your job or at school…like this one, for example…the Kuka omniMove robot used in an Airbus facility in Germany to move aircraft parts around the factory floor:

You can see this Kuka robot has mecanum wheels. The robot we will build in this tutorial will have mecanum wheels, also known as an omnidirectional robot. A mecanum wheel robot uses special wheels with rollers attached at an angle, allowing it to move in any direction by rotating the wheels independently. 

Compared to robots with standard wheels that can only move forward, backward, and turn, mecanum wheel robots have greater maneuverability and can move sideways without changing orientation.

Within ROS 2, defining the URDF file of your mobile robot is important because it allows software tools to understand the robot’s structure, enabling tasks like simulation, motion planning, and sensor data interpretation. It is like giving the robot a digital body that software can interact with.

I will walk through all the steps below for creating the URDF for the ROSMASTER X3 by Yahboom, a company that makes educational robots. 

Follow along with me click by click, keystroke by keystroke.  

If you prefer to learn by video instead of text, you can follow the complete tutorial here:

Prerequisites

You can find all the code here on GitHub.

References

Here is my GitHub repository for this project.

Create a Package

The first step is to create a ROS 2 package to store all your files.

Open a new terminal window, and create a new folder named yahboom_rosmaster.

cd ~/ros2_ws/src
mkdir yahboom_rosmaster
cd yahboom_rosmaster

Now create the package where we will store our URDF file.

ros2 pkg create --build-type ament_cmake --license BSD-3-Clause yahboom_rosmaster_description

Now, let’s create a metapackage.

I discuss the purpose of a metapackage in this post.

A metapackage doesn’t contain anything except a list of dependencies to other packages. You can use a metapackage to make it easier to install multiple related packages at once. 

If you were to make your package available to install publicly using the apt-get package manager on Ubuntu for example, a metapackage would enable someone to automatically install all the ROS2 packages that are referenced in your metapackage. 

ros2 pkg create --build-type ament_cmake --license BSD-3-Clause yahboom_rosmaster
cd yahboom_rosmaster

Configure your package.xml file.

gedit package.xml

Make your package.xml file look like this:

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>yahboom_rosmaster</name>
  <version>0.0.0</version>
  <description>ROSMASTER series robots by Yahboom (metapackage).</description>
  <maintainer email="automaticaddison@todo.todo">ubuntu</maintainer>
  <license>BSD-3-Clause</license>

  <buildtool_depend>ament_cmake</buildtool_depend>
  
  <exec_depend>yahboom_rosmaster_description</exec_depend>

  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

Add a README.md to describe what the package is about.

cd ..
gedit README.md
# yahboom_rosmaster #
![OS](https://img.shields.io/ubuntu/v/ubuntu-wallpapers/noble)
![ROS_2](https://img.shields.io/ros/v/jazzy/rclcpp)

I also recommend adding placeholder README.md files to the yahboom_rosmaster folder.

# yahboom_rosmaster #

The yahboom_rosmaster package is a metapackage. It contains lists of dependencies to other packages.

… as well as the yahboom_rosmaster_description folder.

# yahboom_rosmaster_description #

The yahboom_rosmaster_description package contains the robot description files that define the physical aspects of a robot, including its geometry, kinematics, dynamics, and visual aspects.

Now let’s build our new package:

cd ~/ros2_ws
colcon build

Let’s see if our new package is recognized by ROS 2.

Either open a new terminal window or source the bashrc file like this:

source ~/.bashrc
ros2 pkg list

You can see the newly created packages right there at the bottom.

1-new-yahboom-packages

Now let’s create the following folders:

mkdir -p ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_description/meshes/rosmaster_x3/visual
mkdir -p ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_description/urdf/control
mkdir -p ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_description/urdf/mech
mkdir -p ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_description/urdf/sensors
mkdir -p ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_description/urdf/robots/

Add the Meshes

Mesh files are what make your robot look realistic in robotics simulation and visualization programs.

Mesh files visually represent the 3D shape of the robot parts. These files are typically in formats such as STL (Stereo Lithography – .stl) or COLLADA (.dae).

The mesh files we are going to use were already available in the GitHub repository managed by Yahboom. We didn’t have to create these files from scratch. I got them from the manufacturer’s website.

However, if you want to create your own custom 3D printed mobile robot in the future, for example, you can make your own mesh file. Here is how:

  • Design the robot’s body using CAD programs like Onshape, Fusion 360, AutoCAD, or Solidworks. These tools help you create 3D models of the robot parts.
  • Export the 3D models as mesh files in formats like STL or COLLADA. These files contain information about the robot’s shape, including vertices, edges, and faces.
  • If needed, use a tool like Blender to simplify the mesh files. This makes them easier to use in simulations and visualizations.
  • Add the simplified mesh files to your URDF file to visually represent what the robot looks like.

Let’s pull these mesh files off GitHub. 

First, open a new terminal window, and type:

cd ~/Downloads/

Clone the yahboom_rosmaster repository to your machine.

git clone https://github.com/automaticaddison/yahboom_rosmaster.git

Move to the mesh files for the robot we are going to model:

cp -r yahboom_rosmaster/yahboom_rosmaster_description/meshes/* ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_description/meshes/
ls ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_description/meshes/rosmaster_x3/visual/

You can see the mesh files (.stl) for the robot.

Configure CMakeLists.txt

Let’s open Visual Studio Code.

cd ~/ros2_ws/src/yahboom_rosmaster/
code .

Configure the CMakeLists.txt for the yahboom_rosmaster_description package. Make sure it looks like this:

cmake_minimum_required(VERSION 3.8)
project(yahboom_rosmaster_description)
 
# Check if the compiler being used is GNU's C++ compiler (g++) or Clang.
# Add compiler flags for all targets that will be defined later in the 
# CMakeLists file. These flags enable extra warnings to help catch
# potential issues in the code.
# Add options to the compilation process
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()
 
# Locate and configure packages required by the project.
find_package(ament_cmake REQUIRED)
find_package(urdf_tutorial REQUIRED)
 
# Copy necessary files to designated locations in the project
install (
  DIRECTORY meshes urdf
  DESTINATION share/${PROJECT_NAME}
)
 
# Automates the process of setting up linting for the package, which
# is the process of running tools that analyze the code for potential
# errors, style issues, and other discrepancies that do not adhere to
# specified coding standards or best practices.
if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  # the following line skips the linter which checks for copyrights
  # comment the line when a copyright and license is added to all source files
  set(ament_cmake_copyright_FOUND TRUE)
  # the following line skips cpplint (only works in a git repo)
  # comment the line when this package is in a git repo and when
  # a copyright and license is added to all source files
  set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()
 
ament_package()

Configure package.xml

Make sure your package.xml for the yahboom_rosmaster_description package looks like this:

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>yahboom_rosmaster_description</name>
  <version>0.0.0</version>
  <description>Contains the robot description files that define the physical
    aspects of a robot, including its geometry, kinematics, dynamics, and
    visual aspects.</description>
  <maintainer email="automaticaddison@todo.todo">ubuntu</maintainer>
  <license>BSD-3-Clause</license>

  <buildtool_depend>ament_cmake</buildtool_depend>
  <depend>urdf_tutorial</depend>

  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

Build the Package

Now let’s build the package.

cd ~/ros2_ws/
rosdep install -i --from-path src --rosdistro $ROS_DISTRO -y

You should see:

#All required rosdeps installed successfully

If not, type your password, and install the required dependencies.

Open a terminal window, and type:

build

If this command doesn’t work, type these commands:

echo "alias build='cd ~/ros2_ws/ && colcon build && source ~/.bashrc'" >> ~/.bashrc
build

Create the URDF File

Now let’s create our URDF file. We will actually create it in XACRO format. I will use the terms URDF and XACRO interchangeably going forward.

XACRO files are like blueprints for URDF files, using macros and variables to simplify complex robot descriptions.

Imagine XACRO as the architect drawing up plans, and URDF as the final, ready-to-use construction document. Both file types represent the robotic arm, but XACRO offers more flexibility and organization.

Before a ROS tool or component can use the information in a XACRO file, it must first be processed (translated) into a URDF file. This step allows for the dynamic generation of robot descriptions based on the specific configurations defined in the XACRO file.

Open a terminal window, and type this command to create all the files we need:

touch ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_description/urdf/mech/{rosmaster_x3_base.urdf.xacro,mecanum_wheel.urdf.xacro} ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_description/urdf/sensors/{rgbd_camera.urdf.xacro,imu.urdf.xacro,lidar.urdf.xacro} ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_description/urdf/robots/rosmaster_x3.urdf.xacro

Robot Base

Let’s start with creating our base: rosmaster_x3_base.urdf.xacro. Add this code.

Robot Wheels

Now let’s create a generic mecanum wheel: mecanum_wheel.urdf.xacro. Add this code.

RGBD Camera

Now let’s create the RGBD camera: rgbd_camera.urdf.xacro. An RGBD camera is like a regular digital camera that not only captures colors (RGB) but also measures how far away everything in the scene is from the camera (the D stands for depth). This added depth information allows the camera to create 3D maps of its surroundings.

You can find a big repository of sensors that can be implemented in simulation for Gazebo in this GitHub repository.

Add this code.

Robot LIDAR

Now let’s create the LIDAR: lidar.urdf.xacro. We will add the LIDAR plugin so we can generate simulated LIDAR data in a future tutorial.

Add this code.

Robot Inertial Measurement Unit (IMU)

Now let’s create the IMU: imu.urdf.xacro. An IMU (Inertial Measurement Unit) is a sensor that measures movement, specifically acceleration, rotation, and sometimes magnetic fields, to help determine an object’s position and motion. 

Add this code.

Full Robot

Now let’s create the full robot, bringing together all the components we have created: rosmaster_x3.urdf.xacro. Add this code.

Understanding the URDF

Let’s walk through each file so we can understand what is going on.

rosmaster_x3_base.urdf.xacro

At the top, we start with an XML declaration and define that this is a robot description using xacro, which is like a macro language for robot descriptions:

<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro">

Then we have a bunch of properties that define the robot’s dimensions in meters:

<xacro:property name="total_width" value="0.19940" />  <!-- About 20cm wide -->
<xacro:property name="wheel_width" value="0.0304" />   <!-- About 3cm wheel width -->
<xacro:property name="wheel_radius" value="0.0325" />  <!-- About 3.25cm wheel radius -->
3-diagram

The code defines a macro called “rosmaster_x3_base” – think of this like a template for the robot’s base. The base has several important parts.

The base_link is the main body of the robot. It has three important sections:

  • visual: This defines how the robot looks in simulation using a 3D model file (STL)
  • collision: This is a simplified box shape used to detect when the robot hits things
  • inertial: This defines the robot’s mass and how its weight is distributed
<link name="${prefix}base_link">
    <visual>
        <!-- The robot's appearance -->
        <geometry>
            <mesh filename="file://$(find yahboom_rosmaster_description)/meshes/rosmaster_x3/visual/base_link_X3.STL"/>
        </geometry>
        <!-- Makes it green -->
        <material name="green">
          <color rgba="0 0.7 0 1"/>
        </material>
    </visual>
    <!-- Other parts... -->
</link>

The gazebo tag adds special properties for the Gazebo simulator, like how the material looks in different lighting:

<gazebo reference="${prefix}base_link">
    <visual>
        <material>
            <ambient>0 0.7 0 1</ambient>
            <diffuse>0 0.7 0 1</diffuse>
            <specular>0 0.7 0 1</specular>
        </material>
    </visual>
</gazebo>

The math in the inertial section (ixx, iyy, izz) describes how the robot resists rotation around different axes – this is important for realistic physics simulation. The formula used is the standard equation for the moment of inertia of a rectangular box.

4-weight

mecanum_wheel.urdf.xacro

6-robot-with-mecanum-wheels-and-axes-rviz
I will show you how to launch this later in this tutorial

Unlike regular wheels that can only move forward and backward, mecanum wheels have small rollers arranged at 45-degree angles around their circumference. This unique design allows a robot to move sideways, diagonally, or even rotate in place while remaining stationary – similar to how a crab can walk sideways.

In this file, we start with the basic properties of our wheel. These measurements define the physical characteristics:

<xacro:property name="wheel_radius" value="0.0325" />      <!-- Wheel is 6.5cm in diameter -->
<xacro:property name="wheel_separation" value="0.169" />   <!-- Distance between left and right wheels -->
<xacro:property name="wheel_width" value="0.03040" />     <!-- How thick the wheel is -->
<xacro:property name="wheel_mass" value="0.1" />          <!-- Wheel weighs 0.1 kg -->
<xacro:property name="wheel_xoff" value="0.08" />         <!-- How far forward/back the wheel is -->
<xacro:property name="wheel_yoff" value="-0.01" />        <!-- Small sideways offset -->

The wheel radius affects how fast the robot moves for a given motor speed. 

The separation between wheels influences turning behavior – wider-set wheels provide more stability but require more torque to turn. 

The mass and dimensions affect the robot’s physics simulation, including momentum and inertia.

Next, we define a macro – think of it as a template – that we can use to create wheels:

<xacro:macro name="mecanum_wheel" params="prefix side x_reflect y_reflect">

This macro is clever – instead of writing separate code for each wheel, we write it once and use parameters to customize it. The prefix parameter helps us name each wheel uniquely (like “front_left_wheel” or “rear_right_wheel”). The x_reflect and y_reflect parameters are either 1 or -1, letting us mirror the wheel’s position and orientation for different corners of the robot.

The visual component defines what we see in the simulator:

<visual>
    <origin xyz="0 0 0" rpy="${pi/2} 0 0"/>   <!-- Rotated 90 degrees around X axis -->
    <geometry>
        <mesh filename="file://$(find yahboom_rosmaster_description)/meshes/rosmaster_x3/visual/${side}_wheel_X3.STL"/>
    </geometry>
    <material name="dark_gray">
        <color rgba="0.2 0.2 0.2 1.0"/>       <!-- Dark gray color -->
    </material>
</visual>

This section loads a 3D model (STL file) of the wheel. The rotation parameters (rpy = roll, pitch, yaw) ensure the wheel is oriented correctly. The dark gray color makes it easy to distinguish from other robot parts.

For physics simulation, we need a collision model. While the visual model can be complex and detailed, the collision model is kept simple for computational efficiency:

<collision>
    <geometry>
        <cylinder radius="${wheel_radius}" length="${wheel_width}"/>
    </geometry>
</collision>

Instead of using the detailed STL model for collision detection, we use a simple cylinder. This significantly speeds up physics calculations while maintaining reasonable accuracy.

The inertial properties define how the wheel behaves physically:

<inertial>
    <mass value="${wheel_mass}"/>
    <inertia
        ixx="${(wheel_mass/12.0) * (3*wheel_radius*wheel_radius + wheel_width*wheel_width)}" 
        iyy="${(wheel_mass/2.0) * (wheel_radius*wheel_radius)}"
        izz="${(wheel_mass/12.0) * (3*wheel_radius*wheel_radius + wheel_width*wheel_width)}"/>
</inertial>

These seemingly complex formulas are based on physics equations for a cylinder’s moment of inertia. They determine how the wheel resists changes in rotation around different axes. 

The joint configuration defines how the wheel connects to the robot:

<joint name="${prefix}${side}_wheel_joint" type="continuous">
    <axis xyz="0 1 0"/>                <!-- Spins around Y axis -->
    <parent link="${prefix}base_link"/>
    <child link="${prefix}${side}_wheel_link"/>
    <!-- Position is calculated based on x_reflect and y_reflect to place wheel correctly -->
    <origin xyz="${x_reflect*wheel_xoff} ${y_reflect*(wheel_separation/2+wheel_yoff)} ${-wheel_radius}" rpy="0 0 0"/>
</joint>

A “continuous” joint means it can rotate indefinitely in either direction. The axis specification (0 1 0) means it rotates around the Y axis of the parent link.

The origin calculation uses our x_reflect and y_reflect parameters to position each wheel correctly relative to the robot’s center.

Finally, we have Gazebo-specific settings:

<gazebo reference="${prefix}${side}_wheel_link">
    <mu1>0.01</mu1>    <!-- Friction coefficients -->
    <mu2>0.01</mu2>    <!-- Low values for smooth rolling -->
</gazebo>

The mu1 and mu2 values are friction coefficients. For mecanum wheels, we keep these values low because the rollers should allow easy sideways movement. Higher values would make the wheels grip too much and resist the sliding motion that makes mecanum wheels special.

To implement this in your own robot, you’d call this macro four times, once for each wheel. 

7-robot-with-mecanum-wheels

rgbd.camera.urdf.xacro

An RGBD (Red, Green, Blue + Depth) camera combines a regular color camera with depth sensing capabilities. This allows robots to not just see colors and shapes, but also understand how far away objects are – important for navigation and manipulation tasks.

The code starts by defining a macro called “rgbd_camera” with numerous parameters that make the camera highly configurable:

<xacro:macro name="rgbd_camera" params="
    prefix:=''
    camera_name:='cam_1'
    parent:='base_link'
    mesh_file:='file://$(find yahboom_rosmaster_description)/meshes/intel_realsense/visual/d435.stl'
    xyz_offset:='0.105 0 0.05'
    rpy_offset:='0 -0.50 0'

The parameters control everything from basic naming and positioning to detailed physics properties. The default values are carefully chosen to match real-world RGBD cameras. 

For instance, the camera’s mass is set to 0.072 kg and includes precise inertial properties that match the Intel RealSense D435’s physical characteristics.

The camera’s physical structure consists of multiple “frames” or coordinate systems, each serving a specific purpose:

<link name="${prefix}${camera_name}_link">
    <visual>
        <origin xyz="${mesh_xyz_offset}" rpy="${mesh_rpy_offset}"/>
        <geometry>
            <mesh filename="${mesh_file}" />
        </geometry>
        ...
    </visual>

The main camera link holds the visual 3D model (loaded from an STL file) and can optionally include collision geometry for physical simulation. The aluminum material gives it a realistic appearance.

What makes this implementation particularly sophisticated is its handling of multiple camera frames:

  • Depth frame: Captures distance information
  • Infrared frames (infra1 and infra2): Used for stereo depth perception
  • Color frame: Regular RGB camera
  • Optical frames: Aligned with standard ROS conventions

Each frame is connected by fixed joints with specific offsets:

<joint name="${prefix}${camera_name}_depth_joint" type="fixed">
    <origin xyz="${depth_frame_xyz_offset}" rpy="${depth_frame_rpy_offset}"/>
    <parent link="${prefix}${camera_name}_link"/>
    <child link="${prefix}${camera_name}_depth_frame" />
</joint>

The Gazebo simulation settings at the end define how the camera operates in simulation:

<gazebo reference="${prefix}${camera_name}_link">
    <sensor name="${prefix}${camera_name}" type="rgbd_camera">
        <camera>
            <horizontal_fov>${horizontal_fov}</horizontal_fov>
            <image>
                <width>${image_width}</width>
                <height>${image_height}</height>
            </image>

imu.urdf.xacro

This file defines an IMU sensor for a robot simulation in ROS. An IMU measures a robot’s orientation, acceleration, and angular velocity. It tells the robot which way is up, how fast it’s moving, and how it’s rotating in space – just like the sensor in your smartphone that knows when you turn the screen.

10-mecanum-wheel-with-imu

The core macro definition starts with configurable parameters that let you customize how the IMU is attached to your robot:

<xacro:macro name="imu_sensor" params="
    prefix:=''
    parent:='base_link'
    frame_id:='imu'
    xyz_offset:='0 0 0'
    rpy_offset:='0 0 0'
    ...

The physical properties of the IMU are carefully defined to match real-world characteristics. The sensor weighs 31 grams (0.031 kg) and measures approximately 39mm × 38mm × 13mm. It updates its measurements 15 times per second (15 Hz) and is rendered in a dark black color to match typical IMU hardware.

One of the most interesting parts is how the code calculates the moment of inertia – a measure of how hard it is to rotate the sensor around different axes:

<xacro:property name="ixx" value="${(mass/12.0) * (height*height + width*width)}" />
<xacro:property name="iyy" value="${(mass/12.0) * (length*length + height*height)}" />
<xacro:property name="izz" value="${(mass/12.0) * (length*length + width*width)}" />

These calculations come from physics equations for rectangular objects. The 1/12 factor appears because of how mass is distributed in a rectangular shape. Each axis needs different calculations because rotating a rectangular object requires different amounts of force depending on which way you’re turning it.

The visual representation keeps things simple with a basic black box shape:

<visual>
    <origin xyz="0 0 0" rpy="0 0 0"/>
    <geometry>
        <box size="${length} ${width} ${height}"/>
    </geometry>
    <material name="${material_name}">
        <color rgba="${material_color}"/>
    </material>
</visual>

The IMU attaches to your robot using a fixed joint, meaning it doesn’t move relative to whatever part of the robot you attach it to:

<joint name="${prefix}${frame_id}_joint" type="fixed">
    <parent link="${prefix}${parent}"/>
    <child link="${prefix}${frame_id}_link" />
    <origin xyz="${xyz_offset}" rpy="${rpy_offset}"/>
</joint>

The Gazebo simulation settings at the end define how the IMU behaves in the virtual world:

<gazebo reference="${prefix}${frame_id}_link">
    <sensor name="${prefix}imu_sensor" type="imu">
        <topic>${prefix}${topic_name}</topic>
        <update_rate>${update_rate}</update_rate>
        <always_on>${always_on}</always_on>
        <visualize>${visualize}</visualize>
        <gz_frame_id>${prefix}${frame_id}_link</gz_frame_id>
    </sensor>
</gazebo>

This section tells Gazebo to create a virtual IMU sensor that publishes its data to a ROS topic named “imu/data” by default. The sensor stays on continuously during simulation and updates 15 times per second. The visualization is turned off by default since you typically don’t need to see sensor data graphics during simulation.

lidar.urdf.xacro

This file defines a LIDAR (Light Detection and Ranging) sensor for robot simulation in ROS, specifically modeling an RPLidar S2. A LIDAR sensor spins around and uses laser beams to measure distances to objects, creating a 2D map of its surroundings.

8-mecanum-wheel-robot-rplidars2-intel-realsense-stl

The macro starts by defining the physical and operational characteristics of the LIDAR:

<xacro:macro name="lidar_sensor" params="
    prefix:=''
    parent:='base_link'
    frame_id:='laser_frame'
    mesh_file:='file://$(find yahboom_rosmaster_description)/meshes/rplidar/rplidar_s2.stl'
    xyz_offset:='0 0 0.0825'
    ...

The physical properties match a real RPLidar S2 with a width of 77mm diameter, height of 39.8mm, and mass of 185 grams. Like most LIDAR sensors, it’s rendered in black to match the real hardware.

The sensor’s moment of inertia calculations treat the LIDAR as a cylinder:

<xacro:property name="radius" value="${lidar_width/2}" />
<xacro:property name="ixx_iyy" value="${(mass/12) * (3 * radius * radius + lidar_height * lidar_height)}" />
<xacro:property name="izz" value="${(mass/2) * (radius * radius)}" />

The visual appearance uses a detailed 3D model scaled down from millimeters to meters:

<visual>
    <origin xyz="${mesh_xyz_offset}" rpy="${mesh_rpy_offset}"/>
    <geometry>
        <mesh filename="${mesh_file}" scale="${mesh_scale}"/>
    </geometry>
    <material name="${material_name}">
        <color rgba="${material_color}"/>
    </material>
</visual>

The most interesting part is the LIDAR sensor configuration in Gazebo:

<sensor name="${prefix}lidar_sensor" type="gpu_lidar">
    <topic>${prefix}${topic_name}</topic>
    <update_rate>${update_rate}</update_rate>
    <ray>
        <scan>
            <horizontal>
                <samples>${ray_count}</samples>
                <resolution>1</resolution>
                <min_angle>${min_angle}</min_angle>
                <max_angle>${max_angle}</max_angle>
            </horizontal>

This LIDAR simulates a full 360-degree scan (from -π to π radians) with 360 individual laser beams, taking 10 scans per second. It can measure distances from 5cm to 30 meters with a resolution of 13mm, and uses GPU acceleration for better performance.

The sensor is configured as a 2D LIDAR, meaning it scans in a single plane. This is clear from the vertical configuration:

<vertical>
    <samples>1</samples>
    <resolution>1</resolution>
    <min_angle>0</min_angle>
    <max_angle>0</max_angle>
</vertical>

The sensor publishes its data to a ROS topic called “scan” and runs continuously during simulation. The GPU acceleration means it uses your computer’s graphics card to process the laser measurements, making the simulation more efficient.

The physical mounting is handled by a fixed joint that typically places the LIDAR about 8.25cm above its parent link:

<joint name="${prefix}${frame_id}_joint" type="fixed">
    <parent link="${prefix}${parent}"/>
    <child link="${prefix}${frame_id}" />
    <origin xyz="${xyz_offset}" rpy="${rpy_offset}"/>
</joint>

When you use this macro in your robot description, you get a realistic LIDAR sensor that creates accurate distance measurements in a 360-degree field of view. It updates 10 times per second, has realistic physical properties for simulation, uses GPU acceleration for efficient processing, and publishes data in the standard ROS laser scan format.

The data from this simulated LIDAR works just like a real LIDAR for navigation, mapping, obstacle avoidance, and localization tasks in your robotics applications.

rosmaster_x3.urdf.xacro

This is the main assembly file for the ROSMASTER X3 robot. Think of it as a blueprint that brings together all the individual components we looked at earlier into a complete robot. Let’s walk through how this file builds the robot piece by piece.

First, it sets up some basic information:

<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="rosmaster_x3">
    <xacro:property name="M_PI" value="3.1415926535897931" />
    <xacro:arg name="prefix" default=""/>

The prefix argument lets you create multiple robots in the same simulation by giving each one a unique name prefix.

Next, it imports all the component descriptions we’ve looked at:

<xacro:include filename="$(find yahboom_rosmaster_description)/urdf/mech/rosmaster_x3_base.urdf.xacro"/>
<xacro:include filename="$(find yahboom_rosmaster_description)/urdf/mech/mecanum_wheel.urdf.xacro"/>
<xacro:include filename="$(find yahboom_rosmaster_description)/urdf/sensors/rgbd_camera.urdf.xacro"/>
<xacro:include filename="$(find yahboom_rosmaster_description)/urdf/sensors/lidar.urdf.xacro"/>
<xacro:include filename="$(find yahboom_rosmaster_description)/urdf/sensors/imu.urdf.xacro"/>

Then it starts assembling the robot. First comes the base:

<xacro:rosmaster_x3_base prefix="$(arg prefix)"/>

Next, it adds the four mecanum wheels. Notice how it uses x_reflect and y_reflect to position each wheel correctly:

<xacro:mecanum_wheel
  prefix="$(arg prefix)"
  side="front_left"
  x_reflect="1"
  y_reflect="1"/>

<xacro:mecanum_wheel
  prefix="$(arg prefix)"
  side="front_right"
  x_reflect="1"
  y_reflect="-1"/>

The reflect values work like this:

  • Front wheels have x_reflect=”1″ because they’re at the front
  • Back wheels have x_reflect=”-1″ because they’re at the back
  • Left wheels have y_reflect=”1″
  • Right wheels have y_reflect=”-1″

Then it adds the RGBD camera (like a RealSense):

<xacro:rgbd_camera
  prefix="$(arg prefix)"
  camera_name="cam_1"
  xyz_offset="0.105 0 0.05"
  rpy_offset="0 -0.50 0"/>

The camera is positioned 10.5cm forward, 5cm up, and tilted down slightly (0.5 radians) to see the ground in front of the robot.

The LIDAR sensor comes next:

<xacro:lidar_sensor
  prefix="$(arg prefix)"
  parent="base_link"
  frame_id="laser_frame"
  xyz_offset="0 0 0.0825"
  rpy_offset="0 0 0"/>

It’s mounted 8.25cm above the base, perfectly level to scan the horizontal plane around the robot.

Finally, it adds the IMU sensor:

<xacro:imu_sensor
  prefix="$(arg prefix)"
  parent="base_link"
  frame_id="imu"
  xyz_offset="0 0 0.006"
  rpy_offset="0 0 0"/>

The IMU sits very close to the base (0.6cm up) because it needs to measure the robot’s core movement.

This assembly creates a mobile robot that can:

  • Move in any direction using its mecanum wheels
  • See objects and depth with its RGBD camera
  • Scan its surroundings with the LIDAR
  • Track its movement and orientation with the IMU

All these sensors work together – the LIDAR maps the space, the camera identifies objects, and the IMU helps keep track of the robot’s position and movement. The mecanum wheels then let it navigate precisely through its environment.

Build the Package

Now let’s build the package.

build

Visualize the URDF File

Let’s see the URDF file in RViz. 

By the way, before you run the command below, be sure to comment out these lines in your XACRO since we haven’t yet got to control with Gazebo.

yahboom

Launch the URDF file. The conversion from XACRO to URDF happens behind the scenes. Be sure to have the correct path to your XACRO file.

ros2 launch urdf_tutorial display.launch.py model:=/home/ubuntu/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_description/urdf/robots/rosmaster_x3.urdf.xacro
12-full-robot

By convention, the red axis is the x-axis, the green axis in the y-axis, and the blue axis is the z-axis.

13-by-convention

Uncheck the TF checkbox to turn off the axes.

You can use the Joint State Publisher GUI pop-up window to move the links around.

On the left panel under Displays, play around by checking and unchecking different options.

For example, under Robot Model, you can see how the mass is distributed for the robot arm by unchecking “Visual Enabled” and “Collision Enabled” and checking the “Mass” checkbox under “Mass Properties”.

15-mass-mecanum-wheel-robot

You can also see what simulation engines will use to detect collisions when the mobile robot is commanded to go to a certain point.

Uncheck “Visual Enabled” under Robot Model and check “Collision Enabled.”

14-collision-enabled

You can also see the coordinate frames. 

Open a new terminal window, and type the following commands:

cd ~/Documents/
ros2 run tf2_tools view_frames

To see the coordinate frames, type:

dir
evince frames_YYYY-MM-DD_HH.MM.SS.pdf
16-yahboom-mobile-robot
Sorry the text is so small

To close RViz, press CTRL + C.

So we can quickly visualize our robot in the future, let’s add a bash command that will enable us to quickly see our URDF.

echo "alias yahboom='ros2 launch urdf_tutorial display.launch.py model:=/home/ubuntu/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_description/urdf/robots/rosmaster_x3.urdf.xacro'" >> ~/.bashrc

To see it was added, type:

cat ~/.bashrc
source ~/.bashrc

Going forward, if you want to see your URDF file, type this command in the terminal window:

yahboom

That’s it. Keep building, and I will see you in the next tutorial!