Spawning a robot entity using a node with Gazebo and ROS 2

I thought this might be helpful for newbies wanting to use ros2 and Gazebo.

Instead of using a service call or generating a .world file containing the robot, you can create a node that can place robots in gazebo and use them in a launch file. For this example, I’m going to assume you have the turtlebot3 ros2 files installed and setup and have done the simulations section of the guide. To start, go into the src folder of the ros2 workspace that you created for the turtlebot3 (As an additional assumption, I will assume the workspace is the same name as the one created in the turtlebot3 guide):

~$ cd ~/turtlebot3_ws/src
~$ ros2 pkg create robot_spawner_pkg

Next, we are going to make this a python file only, so we will need to change delete the CMakeLists.txt file and create a setup.py and setup.cfg file containing the following:

from setuptools import setup

PACKAGE_NAME = 'robot_spawner_pkg'

setup(
    name=PACKAGE_NAME,
    version='1.0.0',
    package_dir={'': 'src'}
    packages=[PACKAGE_NAME],
    install_requires=['setuptools'],
    zip_safe=True,
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            'spawn_turtlebot = robot_spawner_pkg.spawn_turtlebot:main',
        ],
    },
)
[develop]
script-dir=$base/lib/robot_spawner_pkg
[install]
install-scripts=$base/lib/robot_spawner_pkg

Now, we edit the package.xml file to add the necessary dependencies to run the program:

<?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>robot_controller_utils</name>
  <version>0.0.0</version>
  <description>TODO: Package description</description>
  <maintainer email="your_email@email.com">your_name</maintainer>
  <license>TODO: License declaration</license>

  <exec_depend>rclpy</exec_depend>

  <export>
    <build_type>ament_python</build_type>
  </export>
</package>

Finally, we create our spawn_turtlebot.py file. Before we do that, however, we need to look at the current structure of our package.

robot_spawner_pkg/
    |
    ----include/
    |
    ---src/
    |
    ----setup.cfg
    ----setup.py
    ----package.xml

because in our setup.py file we defined the package_dir={'': 'src'} and the packages=[PACKAGE_NAME], we need to make our package structure look like this:

robot_spawner_pkg/
    |
    ----include/
    |
    ---src/
    |  |
    |  ----robot_spawner_pkg/
    |
    ----setup.cfg
    ----setup.py
    ----package.xml

Within the second robot_spwaner_pkg folder will we be placing our spawn_turtlebot.py file!

The Robot Spawner Script

In this node, we will essentially be making the service call within the node like the following:

"""
spawn_turtlebot.py

Script used to spawn a turtlebot in a generic position
"""
import os
import sys
import rclpy
from ament_index_python.packages import get_package_share_directory
from gazebo_msgs.srv import SpawnEntity

def main():
    """ Main for spwaning turtlebot node """
    # Get input arguments from user
    argv = sys.argv[1:]

    # Start node
    rclpy.init()
    node = rclpy.create_node("entity_spawner")

    node.get_logger().info(
        'Creating Service client to connect to `/spawn_entity`')
    client = node.create_client(SpawnEntity, "/spawn_entity")

    node.get_logger().info("Connecting to `/spawn_entity` service...")
    if not client.service_is_ready():
        client.wait_for_service()
        node.get_logger().info("...connected!")

    # Get path to the turtlebot3 burgerbot
    sdf_file_path = os.path.join(
        get_package_share_directory("turtlebot3_gazebo"), "models",
        "turtlebot3_burger", "model.sdf")

    # Set data for request
    request = SpawnEntity.Request()
    request.name = argv[0]
    request.xml = open(sdf_file_path, 'r').read()
    request.robot_namespace = argv[1]
    request.initial_pose.position.x = float(argv[2])
    request.initial_pose.position.y = float(argv[3])
    request.initial_pose.position.z = float(argv[4])

    node.get_logger().info("Sending service request to `/spawn_entity`")
    future = client.call_async(request)
    rclpy.spin_until_future_complete(node, future)
    if future.result() is not None:
        print('response: %r' % future.result())
    else:
        raise RuntimeError(
            'exception while calling service: %r' % future.exception())

    node.get_logger().info("Done! Shutting down node.")
    node.destroy_node()
    rclpy.shutdown()


if __name__ == "__main__":
    main()
    

I want to give credit to Dirk Thomas (can’t seem to find the link to the code…) for this, because it is a modification of his code that inspired me to do this. Now build using colcon, source the install, and export the path to the turtlebot3 model like this:

~$ cd ~/turtlebot3_ws
~$ colcon build --symlink-install
~$ . install/setup.bash
~$ export GAZEBO_MODEL_PATH=$GAZEBO_MODEL_PATH:~/turlebot3_ws/src/turtlebot3/turtlebot3_simulations/turtlebot3_gazebo/models

To run the node, we first start a new terminal, source the installation, and start gazebo:

~$ cd ~/turtlebot3_ws
~$ . install/setup.bash
~$ export GAZEBO_MODEL_PATH=$GAZEBO_MODEL_PATH:~/turlebot3_ws/src/turtlebot3/turtlebot3_simulations/turtlebot3_gazebo/models
~$ gazebo --verbose -s libgazebo_ros_factory.so

Notice that we are using libgazebo_ros_factory.so instead of libgazebo_ros_init.so as used in the turtlebot3 and other tutorials. That is because only libgazebo_ros_factory.so contains the service call to /spawn_entity! Finally, back in your original terminal, use the following command to add a robot!

~$ ros2 run robot_spawner_pkg spawn_turtlebot the_robot_name robot_namespace 0.0 0.0 0.1

The first two arguments are the robot’s name in gazebo and a namespace for the robot if you would like to add more than one. Hope that helps!

EDIT: An initial version of this post used the model-1_4.sdf for the model loaded in gazebo. This version of the sdf file does not work. If you try giving a linear velocity to the model in gazebo, it will results in the wheels rotating off axis…it’s weird. Anyways, the one that works is model.sdf which I fixed in the spawn_turtlebot code above.

2 Likes

Thanks for writing up this tutorial :smiley:

You may be interested in this pull request, where @shiveshkhaitan is adding a similar script to the gazebo_ros package. It’s a port from the spawn_model node from ROS 1, see the migration guide.

Oh nice, thanks Louise! The last time I looked at the guide that section wasn’t on it, but I’m glad that you guys are going to add it. Do you need any help with the pull request?

Btw, I’m probably going to post more of these guides as I start progressing with my ros2 project. I hope that is ok with everyone.

You’re more than welcome to try it out and leave a review! Thanks :smile:

1 Like

You can also launch it directly from launch file. Hope this helps!

import os

from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import ExecuteProcess
from launch.substitutions import LaunchConfiguration

#TURTLEBOT3_MODEL = os.environ['TURTLEBOT3_MODEL']

def generate_launch_description():
    use_sim_time = LaunchConfiguration('use_sim_time', default='true')
    robot_name = 'my_robot'
    world_file_name = 'empty.world'
    
    world = os.path.join(get_package_share_directory(robot_name), 'worlds', world_file_name)

    urdf = os.path.join(get_package_share_directory(robot_name), 'urdf', 'my_robot.urdf')

    xml = open(urdf, 'r').read()

    xml = xml.replace('"', '\\"')

    swpan_args = '{name: \"my_robot\", xml: \"'  +  xml + '\" }'

    return LaunchDescription([
        ExecuteProcess(
            cmd=['gazebo', '--verbose', world, '-s', 'libgazebo_ros_factory.so'],
            output='screen'),

        ExecuteProcess(
            cmd=['ros2', 'param', 'set', '/gazebo', 'use_sim_time', use_sim_time],
            output='screen'),

        ExecuteProcess(
            cmd=['ros2', 'service', 'call', '/spawn_entity', 'gazebo_msgs/SpawnEntity', swpan_args],
            output='screen'),
    ])
1 Like