How to Load a World File into Gazebo – ROS 2

In this tutorial, I will show you how to load a .world file into Gazebo. 

You can learn all about world files at this link.

Real-World Applications

Simulating a world has a number of real-world robotic applications: 

  • Ground Delivery
  • Hospitals and Medical Centers
  • Hotels (Room Service)
  • Offices
  • Restaurants
  • Warehouses
  • And more…

Roboticists like to simulate robots in simulated worlds in order to test out different algorithms. You can imagine the cost of making mistakes with a physical robot can be high (e.g. crashing a mobile robot into a wall at high speed means lost money).

You can get all the code for this tutorial here.

Let’s get started!

Prerequisites

  • ROS 2 Foxy Fitzroy installed on Ubuntu Linux 20.04 or newer. I am using ROS 2 Galactic, which is the latest version of ROS 2 as of the date of this post.
    • If you are using another ROS 2 distribution, you will need to replace ‘galactic’ with the name of your distribution everywhere I mention ‘galactic’ in this tutorial. 
    • I highly recommend you get the newest version of ROS 2. If you are using a newer version of ROS 2, you can still follow all the steps in this tutorial. Everything will work just fine.
  • You have already created a ROS 2 workspace. The name of our workspace is “dev_ws”, which stands for “development workspace.” 
  • (Optional) You have a package named two_wheeled_robot inside your ~/dev_ws/src folder, which I set up in this post. If you have another package, that is fine.

Manually 

Quick Start

The easiest way to load a world is via the command line. 

Open a new terminal window.

Type the following command to open up a world named cafe.world. This file is one of the built-in worlds that comes with the Gazebo installation (I am using Gazebo 11).

gazebo worlds/cafe.world

You can see a list of all the built-in worlds in this GitHub repository.

The world files are located in your /usr/share/gazebo-11/worlds directory.

The models (i.e. models for the objects that are inside your world) are located inside this directory:

~/.gazebo/models

Where focalfossa is the name of your Linux environment.

When you want to close the world, go back to the terminal, and type:

CTRL + C

Your Own World

If you have your own world, you can follow the instructions on this post to test it in Gazebo.

Alternatively, you can launch Gazebo using the following command.

gazebo

Then go to the Insert tab, and start inserting items inside the empty world. 

2-click-insert-tab
3-insert-an-airplane

Once you’re done, you can save the world file by going to File ->Save World As.

Using a Launch File

Example – Cafe World

Let’s launch the cafe world we saw earlier using a ROS 2 launch file.

I want to copy the cafe.world file to the worlds folder of my ROS 2 package.

cd /usr/share/gazebo-11/worlds
cp cafe.world ~/dev_ws/src/two_wheeled_robot/worlds
3b-worlds-file-cafe

The syntax is:

cp <world file> <path to the worlds folder inside your ROS 2 package>

cd ~/dev_ws/src/two_wheeled_robot/worlds
gedit cafe.world

You will see the cafe.world file has various links to other models and mesh files in it. Let’s move those files over to our package so that everything is all inside our two_wheeled_robot package (or whatever ROS 2 package you’re using).

Start from the top of the cafe.world file.

The first model we need to move over is the ground_plane model.

cd ~/.gazebo/models
cp -r ground_plane ~/dev_ws/src/two_wheeled_robot/models

Now let’s copy over the other models.

cp -r cafe ~/dev_ws/src/two_wheeled_robot/models
cp -r cafe_table ~/dev_ws/src/two_wheeled_robot/models
4-models-files

Now let’s create a launch file.

colcon_cd two_wheeled_robot
cd launch
gedit load_world_into_gazebo.launch.py

Write this code inside the file.

# Author: Addison Sears-Collins
# Date: September 19, 2021
# Description: Load a world file into Gazebo.
# https://automaticaddison.com

import os
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.conditions import IfCondition, UnlessCondition
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import Command, LaunchConfiguration, PythonExpression
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare

def generate_launch_description():

  # Set the path to the Gazebo ROS package
  pkg_gazebo_ros = FindPackageShare(package='gazebo_ros').find('gazebo_ros')   
  
  # Set the path to this package.
  pkg_share = FindPackageShare(package='two_wheeled_robot').find('two_wheeled_robot')

  # Set the path to the world file
  world_file_name = 'cafe.world'
  world_path = os.path.join(pkg_share, 'worlds', world_file_name)
  
  # Set the path to the SDF model files.
  gazebo_models_path = os.path.join(pkg_share, 'models')
  os.environ["GAZEBO_MODEL_PATH"] = gazebo_models_path

  ########### YOU DO NOT NEED TO CHANGE ANYTHING BELOW THIS LINE ##############  
  # Launch configuration variables specific to simulation
  headless = LaunchConfiguration('headless')
  use_sim_time = LaunchConfiguration('use_sim_time')
  use_simulator = LaunchConfiguration('use_simulator')
  world = LaunchConfiguration('world')

  declare_simulator_cmd = DeclareLaunchArgument(
    name='headless',
    default_value='False',
    description='Whether to execute gzclient')
    
  declare_use_sim_time_cmd = DeclareLaunchArgument(
    name='use_sim_time',
    default_value='true',
    description='Use simulation (Gazebo) clock if true')

  declare_use_simulator_cmd = DeclareLaunchArgument(
    name='use_simulator',
    default_value='True',
    description='Whether to start the simulator')

  declare_world_cmd = DeclareLaunchArgument(
    name='world',
    default_value=world_path,
    description='Full path to the world model file to load')
   
  # Specify the actions
  
  # Start Gazebo server
  start_gazebo_server_cmd = IncludeLaunchDescription(
    PythonLaunchDescriptionSource(os.path.join(pkg_gazebo_ros, 'launch', 'gzserver.launch.py')),
    condition=IfCondition(use_simulator),
    launch_arguments={'world': world}.items())

  # Start Gazebo client    
  start_gazebo_client_cmd = IncludeLaunchDescription(
    PythonLaunchDescriptionSource(os.path.join(pkg_gazebo_ros, 'launch', 'gzclient.launch.py')),
    condition=IfCondition(PythonExpression([use_simulator, ' and not ', headless])))

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

  # Declare the launch options
  ld.add_action(declare_simulator_cmd)
  ld.add_action(declare_use_sim_time_cmd)
  ld.add_action(declare_use_simulator_cmd)
  ld.add_action(declare_world_cmd)

  # Add any actions
  ld.add_action(start_gazebo_server_cmd)
  ld.add_action(start_gazebo_client_cmd)

  return ld

Save the file, and close it.

Go to your root directory.

cd ~/dev_ws

Build the package.

colcon build

Open a new terminal window.

Run the launch file using the following command:

ros2 launch two_wheeled_robot load_world_into_gazebo.launch.py

Here is the output. You should see the actors walking around the cafe.

5-cafe-world

Summary

In summary, the procedure for creating a world entails the following steps:

  1. Create the .world file inside the worlds folder of your package.
  2. Add the SDF models to the models folder of your package.
  3. Edit the launch file.
  4. Build the package.
  5. Launch the world.

How to Add a Python ROS2 Node to a C++ ROS 2 Package

In this tutorial, I will show you how to add a Python ROS 2 node to a C++ package. We will go through the entire process, step-by-step.

Let’s say you have a ROS 2 package that is a standard C++ ROS 2 package. What this means is that it is of the ament_cmake (CMake) build type. You can verify this by going to the package.xml file inside your package and checking the <buildtool_depend> tag.

For example, the name of my workspace is dev_ws, and I have a package named two_wheeled_robot inside that package. I go to my terminal window, and type:

cd ~/dev_ws/src/two_wheeled_robot
gedit package.xml

Here is what I see:

1-build-tool

I have a Python node that I created called lift_controller.py, and I want to add it to my package. I am using this Python node to lift the platform on top of a mobile robot that I want to visualize in RViz.

Create the Python Node

The first thing I am going to do is to write my Python node. I want to create this code inside a folder named scripts.

cd ~/dev_ws/src/two_wheeled_robot 
mkdir scripts
cd scripts
gedit lift_controller.py

Write your Python code inside that file. Here is my code (don’t worry about trying to understand the code…this is just a demo):

#!/usr/bin/env python3 

# Test code for controlling the lift mechanism on a warehouse robot
# Author: Addison Sears-Collins
# Website: https://automaticaddison.com

# ROS Client Library for Python
import rclpy
 
# Handles the creation of nodes
from rclpy.node import Node
 
# Enables usage of the standard message
import std_msgs.msg

# Enables use of sensor messages
import sensor_msgs.msg 

 
class LiftController(Node):
  """
  Create a LiftController class, which is a subclass of the Node class.
  """
  def __init__(self):
    """
    Class constructor to set up the node
    """
    # Initiate the Node class's constructor and give it a name
    super().__init__('lift_controller')
     
    # Create the publisher. This publisher will publish a JointState message
    # to a topic. The queue size is 10 messages.
    self.publisher_ = self.create_publisher(sensor_msgs.msg.JointState, 'joint_states', 10)

    # Create the subscriber. This subscriber will subscribe to a JointState message
    self.subscription = self.create_subscription(sensor_msgs.msg.JointState, 'joint_states', self.listener_callback, 10)
         
  def listener_callback(self, msg):
    """
    Callback function.
    """
    # Create a JointStates message
    new_msg = sensor_msgs.msg.JointState()
 
    # Set the message's data
    new_msg.header.stamp = self.get_clock().now().to_msg()
    new_msg.name = msg.name
    new_msg.position = msg.position
    new_msg.position[2] = 0.30 
     
    # Publish the message to the topic
    self.publisher_.publish(new_msg)
     
def main(args=None):
 
  # Initialize the rclpy library
  rclpy.init(args=args)
 
  # Create the node
  lift_controller = LiftController()
 
  # Spin the node so the callback function is called.
  rclpy.spin(lift_controller)
 
  # Destroy the node explicitly
  # (optional - otherwise it will be done automatically
  # when the garbage collector destroys the node object)
  lift_controller.destroy_node()
 
  # Shutdown the ROS client library for Python
  rclpy.shutdown()
 
if __name__ == '__main__':
  main()

Save the file and close it.

Make sure this shebang line is on the first line of your Python node.

#!/usr/bin/env python3

Make the file executable using the following command.

chmod +x lift_controller.py

Set Up the Package

Now we need to set up the package so that it can build Python executables.

First, we need to create a folder for any Python libraries or modules that we want to import into our Python node. This folder needs to have an empty __init__.py file inside it. It will also have the same name as the package.

cd ~/dev_ws/src/two_wheeled_robot 
mkdir two_wheeled_robot
touch two_wheeled_robot/__init__.py

If you have a module that you want to import into your Python node, you can place it inside the folder as follows.

touch two_wheeled_robot/module_to_import.py

If you want to import that module into your Python node, you would write this at the top of your Python node:

from two_wheeled_robot.module_to_import import ...

Here is an example folder tree of what my two_weeled_robot package looks like.

two_wheeled_robot/
# --> package information, configuration, and compilation
├── CMakeLists.txt
├── package.xml
# --> Python contents
├── two_wheeled_robot
│   ├── __init__.py
│   └── module_to_import.py
├── scripts
│   └──  lift_controller.py
# --> C++ contents
├── include
│   └── two_wheeled_robot
│       └── cpp_node.hpp
└── src
    └── cpp_node.cpp

Update the Package.xml File

Here is my package.xml file. You will see that I have added the ament_cmake_python build tool. I have also added dependencies, including the ROS2 Python library (rclpy).

<?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>two_wheeled_robot</name>
  <version>1.0.0</version>
  <description>A two-wheeled mobile robot with an IMU sensor and LIDAR</description>
  <maintainer email="automaticaddison@todo.todo">AutomaticAddison</maintainer>
  <license>MIT License</license>

  <buildtool_depend>ament_cmake</buildtool_depend>
  <buildtool_depend>ament_cmake_python</buildtool_depend>

  <exec_depend>rclcpp</exec_depend>
  <exec_depend>rclpy</exec_depend>
  <exec_depend>std_msgs</exec_depend>
  <exec_depend>sensor_msgs</exec_depend>
  <exec_depend>joint_state_publisher</exec_depend>
  <exec_depend>robot_state_publisher</exec_depend>
  <exec_depend>rviz</exec_depend>
  <exec_depend>xacro</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>

Update the CMakeLists.txt File

Here is my CMakeLists.txt File.

cmake_minimum_required(VERSION 3.5)
project(two_wheeled_robot)

# Default to C99
if(NOT CMAKE_C_STANDARD)
  set(CMAKE_C_STANDARD 99)
endif()

# Default to C++14
if(NOT CMAKE_CXX_STANDARD)
  set(CMAKE_CXX_STANDARD 14)
endif()

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# Find dependencies
find_package(ament_cmake REQUIRED)
find_package(ament_cmake_python REQUIRED)
find_package(rclcpp REQUIRED)
find_package(rclpy REQUIRED)
# uncomment the following section in order to fill in
# further dependencies manually.
# find_package(<dependency> REQUIRED)

# Include cpp "include" directory
include_directories(include)

install(
  DIRECTORY config include launch maps media meshes models params rviz scripts src two_wheeled_robot urdf worlds
  DESTINATION share/${PROJECT_NAME}
)

# Install Python modules
ament_python_install_package(${PROJECT_NAME})

# Install Python executables
install(PROGRAMS
  scripts/lift_controller.py
  DESTINATION lib/${PROJECT_NAME}
)

if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  # the following line skips the linter which checks for copyrights
  # uncomment the line when a copyright and license is not present in all source files
  #set(ament_cmake_copyright_FOUND TRUE)
  # the following line skips cpplint (only works in a git repo)
  # uncomment the line when this package is not in a git repo
  #set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()

ament_package()

I have added dependencies for ament_cmake_python, rclpy, etc.

I added the path to the include directories in case I want to create C++ header files later on. 

I include the extra folders in the directory.

I install Python modules.

I then provide the path to the Python executables.

If you have C++ nodes, these executables would be located under the include_directories(include) line.

# Create Cpp executable
add_executable(cpp_executable src/cpp_node.cpp)
ament_target_dependencies(cpp_executable rclcpp)

# Install Cpp executables
install(TARGETS
  cpp_executable
  DESTINATION lib/${PROJECT_NAME}
)

Build and Run the Node

To build the node, go back to the root of your workspace.

cd ~/dev_ws/

Build the package.

colcon build

Now, open a new terminal window, and run the node.

ros2 run two_wheeled_robot lift_controller.py

To see active ROS 2 topics, you can open a new terminal window and type:

ros2 topic list

References

This blog post over at RoboticsBackend was very helpful.

How to Convert a Xacro File to URDF and Then to SDF

If you are using ROS and Gazebo and have a URDF robot file that has Xacro in it (i.e. XML macros) like this one, you can convert this file to a pure URDF file in Ubuntu Linux using the following command (everything below goes on the same line inside the Linux terminal window):

xacro two_wheeled_robot.xacro > two_wheeled_robot.urdf

You can then convert the URDF file without Xacro into an SDF file if you wish. Here is the command to do that:

gz sdf -p two_wheeled_robot.urdf > two_wheeled_robot.sdf

That’s it!