In this post, I’m going to provide some straight-forward pointers for getting started using ROS 2.

There’s a minimal repository with the code from this post here.

Introduction

I recently started a new job which involves me working on the Stretch robot from Hello Robots. Since my background is principally in machine learning and not robotics, this is my first time using ROS. However, when I searched for “ROS 2 API” or “How to use ROS 2” or “Getting Started with ROS 2” most of the sites I found looked like they had been insanely SEO’d to the point of being unusable. To wit, I figured I would document my progress as I am learning so that others who may be making the same transition might benefit as well.

What this Post Covers

In this blog post I hope to do the following:

  1. Provide a simple on-boarding guide for ROS 2
    • I’m going to be somewhat opinionated about the “right” way to do things (mostly based on my background in building other types of software) so I may leave out some alternative approaches, but I’ll try to be very thorough in describing how to do things
  2. Make a searchable reference for how to do simple things
  3. Provide some more in-depth technical explanations for how different ROS components work, and why they work that way
    • This is more because I think it’s important to understand the inner workings of things in order to debug and optimize them, but I’ll try to structure the post in such a way that these parts are easy to skip over if you just want to get started building something.

If I miss anything, please leave a comment (note that the commenting tool requires a Github account).

What this Post Doesn’t Cover

In this post I won’t be covering:

Getting Started

Throughout this post, I will be using RoboStack, which is a way of installing ROS as a Conda bundle. This makes it easier to manage multiple versions of ROS and install new packages.

Create a New Conda Environment

# Robostack's ROS 2 Humble channel only works with Python 3.9.
# Galactic can work with either version.
conda create --name ros-blog-post python=3.9
conda activate ros-blog-post

Install Mamba

conda install -c conda-forge mamba

Mamba is a drop-in replacement for the Conda CLI which is a lot faster and makes working with Conda packages a lot easier.

If you want to avoid adding specific Conda channels, such as -c conda-forge, you can add them to your ~/.condarc file using conda config --add channels conda-forge. Then you can just do something like conda install mamba and it will look in the conda-forge channel automatically. In particular for this post I suggest adding the robostack-humble channel if you plan on using ROS 2 Humble.

Install the Humble distro of ROS 2

Following the installation instructions here:

# Install Humble distro.
mamba install \
  -c robostack-humble \
  -c conda-forge \
  spdlog=1.9.2 \
  foonathan-memory=0.7.2 \
  ros-humble-desktop

# These are the instructions to install the Galactic
# distro, which worked fine for me on an Ubuntu machine
# but failed on my M1 Mac.
mamba install \
  -c robostack-experimental \
  ros-galactic-desktop

Note that installing ROS 2 this way adds some scripts to the directory in ${CONDA_PREFIX}/etc/conda/activate.d/. These set some environment variables which are important. In order to get these scripts to run, you have to restart your Conda environment, like so:

conda deactivate
conda activate ros-blog-post

Install Colcon

ROS 2 packages are built using colcon, so you will need to install it as well:

mamba install \
  -c conda-forge \
  colcon-core \
  colcon-common-extensions

Check that Installation Succeeeded

You can double-check that the installation was successful by running the dummy programs. In one terminal session, run:

ros2 run demo_nodes_cpp talker

If everything was installed correctly, you should see something like this:

[INFO] [1667791882.591136612] [talker]: Publishing: 'Hello World: 1'
[INFO] [1667791883.591488073] [talker]: Publishing: 'Hello World: 2'
[INFO] [1667791884.595503927] [talker]: Publishing: 'Hello World: 3'
[INFO] [1667791885.591661410] [talker]: Publishing: 'Hello World: 4'
[INFO] [1667791886.593317378] [talker]: Publishing: 'Hello World: 5'

In another terminal session, run:

ros2 run demo_nodes_cpp listener

If everything worked as expected, you should see something like this:

[INFO] [1667791947.122910896] [listener]: I heard: [Hello World: 13]
[INFO] [1667791948.122430228] [listener]: I heard: [Hello World: 14]
[INFO] [1667791949.123189150] [listener]: I heard: [Hello World: 15]
[INFO] [1667791950.119661010] [listener]: I heard: [Hello World: 16]
[INFO] [1667791951.121229768] [listener]: I heard: [Hello World: 17]

Turtlesim

The introductory ROS node is Turtlesim. To get started, simply run

ros2 run turtlesim turtlesim_node

If you are running this on your local machine, it should pop up a window that looks like this:

The command above starts a new node which manages the window. In another terminal, start another node which takes keyboard inputs and communicates with the associated topics and actions on the first node:

ros2 run turtlesim turtle_teleop_key

You can also directly call a topic by using the command below:

ros2 topic pub /turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 1.0}, angular: {z: -3.1415926535}}"

If everything is working as expected, this should make the turtle spin around in circles on your screen.

Writing a Custom Node

Let’s write a new node to interact with the Turtlesim node. First, let’s create a new workspace somewhere:

mkdir ros-blog-post
cd ros-blog-post
git init .

Next, create a new package using the command:

ros2 pkg create --build-type ament_python custom_turtlesim

If you run the tree command (if it’s not installed, see these instructions), you should get a directory structure that looks like this:

$ tree
.
└── custom_turtlesim
    ├── custom_turtlesim
    │   └── __init__.py
    ├── package.xml
    ├── resource
    │   └── custom_turtlesim
    ├── setup.cfg
    ├── setup.py
    └── test
        ├── test_copyright.py
        ├── test_flake8.py
        └── test_pep257.py

Next, create a new file in custom_turtlesim/custom_turtlesim/controller.py and add the following:

import math
from typing import List, Optional

import rclpy
from geometry_msgs.msg import Twist
from rclpy.node import Node


class TurtlesimController(Node):
    def __init__(self, run_every_n_seconds: float = 0.5, queue_size: int = 10) -> None:
        """Defines a simple controller for Turtlesim.

        Args:
            run_every_n_seconds: Run the publisher every N seconds
            queue_size: The number of messages to queue up if the subscriber
                is not receiving them fast enough; this is a quality-of-service
                setting in ROS
        """

        super().__init__(node_name="turtle_sim_controller")

        self.turtle_pub = self.create_publisher(Twist, "/turtle1/cmd_vel", qos_profile=queue_size)
        self.timer = self.create_timer(run_every_n_seconds, self.timer_callback)

    def timer_callback(self) -> None:
        """Defines a callback which is called every time the timer spins."""

        msg = Twist()
        msg.linear.x = 1.0
        msg.angular.z = math.pi
        self.turtle_pub.publish(msg)
        self.get_logger().info("Published a message")


def run_turtlesim_controller(args: Optional[List[str]] = None) -> None:
    rclpy.init(args=args)

    controller = TurtlesimController()

    try:
        rclpy.spin(controller)
    finally:
        controller.destroy_node()
        rclpy.shutdown()


if __name__ == "__main__":
    run_turtlesim_controller()

Running the Custom Node

Next, let’s run the node that we’ve written.

Ad-hoc

There are two ways to run this. The simplest is to simply run the Python script directly:

# Run the script.
python custom_turtlesim/custom_turtlesim/controller.py

# Run as a module.
cd custom_turtlesim/
python -m custom_turtlesim.controller

This way you can do quick debugging without having to rebuild your entire package

Full Build

When you’re done debugging your controller, you can build the whole package and add it to your environment.

Add the run_turtlesim_controller function as an entry point in the file custom_turtlesim/setup.py:

entry_points={
    "console_scripts": [
        "controller = custom_turtlesim.controller:run_turtlesim_controller",
    ],
},

From your root ros-blog-post/ directory, build the package by running:

colcon build

Now your directory tree should look something like this:

$ tree -L 1
.
├── build
├── custom_turtlesim
├── install
└── log

Install the newly-built package:

. install/setup.bash  # If using bash
. install/setup.zsh   # If using zsh

Finally, you can run your service using:

ros2 run custom_turtlesim controller

You can run the auto-generated tests using

colcon test

Network Teleop

This part will require having two machines, which I’ll call the host and client. The host is going to be running the main Turtlesim node, while the client is going to run our custom controller. On the host, run

ros2 run turtlesim turtlesim_node

You should be able to see the standard view of the turtle. Next, in a different terminal, get the host hostname:

ROS_HOSTNAME=$(hostname)
Example output
$ hostname
benjamins-mini.lan

Next, from the client, make sure you can ping the host hostname (note that this requires being on the same network):

export ROS_HOSTNAME=<hostname-from-host>
ping $ROS_HOSTNAME
Example output
$ export ROS_HOSTNAME=ben-computer.lan
$ ping $ROS_HOSTNAME
PING host-hostname (192.168.0.1): 56 data bytes
64 bytes from 192.168.0.1: icmp_seq=0 ttl=64 time=136.229 ms
64 bytes from 192.168.0.1: icmp_seq=1 ttl=64 time=17.090 ms
64 bytes from 192.168.0.1: icmp_seq=2 ttl=64 time=18.387 ms
64 bytes from 192.168.0.1: icmp_seq=3 ttl=64 time=17.272 ms

If you were able to ping from the client to the host successfully, then after setting the $ROS_HOSTNAME environment variable, you can just run the custom node:

python -m custom_turtlesim.controller

Terminology

Package

A package in ROS is a collection of code, much like a Python or C++ package.

To create a new Python package:

ros2 pkg create --build-type ament_python <package-name>

To create a new C++ package:

ros2 pkg create --build-type ament_cmake <package-name>

To prepopulate with a starter node, you can add the command line argument --node-name <node-name>

To list all currently-installed packages, run:

ros2 pkg list

package.xml

All ROS packages include a package.xml file which contains meta information about the package. However, when using RoboStack as your dependency manager, you can more or less ignore it.

Node

A node in ROS is a process that performs some computation - basically an executable. They communicate with each other using topics, services and actions. You can list running nodes using

ros2 node list

You can print out info about a particular running node using

ros2 node info <node-name>
Sample output when running turtlesim_node
$ ros2 node info /turtlesim
/turtlesim
  Subscribers:
    /parameter_events: rcl_interfaces/msg/ParameterEvent
    /turtle1/cmd_vel: geometry_msgs/msg/Twist
  Publishers:
    /parameter_events: rcl_interfaces/msg/ParameterEvent
    /rosout: rcl_interfaces/msg/Log
    /turtle1/color_sensor: turtlesim/msg/Color
    /turtle1/pose: turtlesim/msg/Pose
  Service Servers:
    /clear: std_srvs/srv/Empty
    /kill: turtlesim/srv/Kill
    /reset: std_srvs/srv/Empty
    /spawn: turtlesim/srv/Spawn
    /turtle1/set_pen: turtlesim/srv/SetPen
    /turtle1/teleport_absolute: turtlesim/srv/TeleportAbsolute
    /turtle1/teleport_relative: turtlesim/srv/TeleportRelative
    /turtlesim/describe_parameters: rcl_interfaces/srv/DescribeParameters
    /turtlesim/get_parameter_types: rcl_interfaces/srv/GetParameterTypes
    /turtlesim/get_parameters: rcl_interfaces/srv/GetParameters
    /turtlesim/list_parameters: rcl_interfaces/srv/ListParameters
    /turtlesim/set_parameters: rcl_interfaces/srv/SetParameters
    /turtlesim/set_parameters_atomically: rcl_interfaces/srv/SetParametersAtomically
  Service Clients:

  Action Servers:
    /turtle1/rotate_absolute: turtlesim/action/RotateAbsolute
  Action Clients:

How do nodes work?

The official documentation here covers this pretty well. Essentially:

Topic

A topic in ROS is a way for nodes to send messages to each other using publish/subscribe semantics. A node will publish a topic, which can then be subscribed to by other nodes. You can list available topics using:

ros2 topic list

To show the topic type, you can use:

ros2 topic list --show-types
ros2 topic list -t  # Shorthand
Sample output when running turtlesim_node
$ ros2 topic list --show-types
/parameter_events [rcl_interfaces/msg/ParameterEvent]
/rosout [rcl_interfaces/msg/Log]
/turtle1/cmd_vel [geometry_msgs/msg/Twist]
/turtle1/color_sensor [turtlesim/msg/Color]
/turtle1/pose [turtlesim/msg/Pose]

To send a message to a topic from the command line, you can use the command:

ros2 topic pub <topic-name> <message-type>

Some useful command-line flags:

Sample call to turtlesim_node
ros2 topic pub -r 0.5 /turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 2.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: -3.1415926535}}"

Messages

A message is just a type of topic, which tells ROS what kind of data is being passed around. This is so that data can be encoded and decoded correctly.

Service

A service in ROS is a way for nodes to communicate with each other using call-and-response semantics. A node will send a request to another node, and receive a response in return. You can list available services using:

ros2 service list
Sample output when running turtlesim_node
$ ros2 service list
/clear
/kill
/reset
/spawn
/turtle1/set_pen
/turtle1/teleport_absolute
/turtle1/teleport_relative
/turtlesim/describe_parameters
/turtlesim/get_parameter_types
/turtlesim/get_parameters
/turtlesim/list_parameters
/turtlesim/set_parameters
/turtlesim/set_parameters_atomically

Bag

A bag in ROS is just a recording of a session.

Action

A action in ROS has three parts:

  1. Goal: A service which can be called to kick off the action
  2. Feedback: A topic which lets the actor give constant feedback to the controller
  3. Result: A long-running service which ultimately sends a response when the action has finished

Actions are essentially services which can be pre-empted. You can list available actions using:

ros2 action list
Sample output when running turtlesim_node
$ ros2 action list
/turtle1/rotate_absolute

run

ros2 run is used to run a single executable (simply a node). This can be invoked using:

ros2 run <package-name> <node-name>

For example, to run Turtlesim, use:

ros2 run turtlesim turtlesim_node

This will then print the logged output from the running process.

Sometimes you might get an error message like this:

Package '<package-name>' not found

In this case, you can check if your package is installed by using:

ros2 pkg list | grep <package-name>

launch

ros2 launch is used to run multiple nodes at once, as defined by a launch file. A launch file can be written in Python, XML or YAML. For example, you can create a launch file in Python that looks like this:

import launch_ros.actions
from launch import LaunchDescription


def generate_launch_description() -> LaunchDescription:
    return LaunchDescription(
        [
            launch_ros.actions.Node(
                namespace="turtlesim1",
                package="turtlesim",
                executable="turtlesim_node",
                output="screen",
            ),
            launch_ros.actions.Node(
                namespace="turtlesim2",
                package="turtlesim",
                executable="turtlesim_node",
                output="screen",
            ),
        ]
    )

If you save this somewhere (for example, launch_turtlesim.py) you can then run it using:

ros2 launch launch_turtlesim.py

In another terminal, you can then see two running Turtlesim nodes, each in their own respective namespace:

$ ros2 node list
/turtlesim1/turtlesim
/turtlesim2/turtlesim

colcon

Colcon is the command line tool that ROS uses for building and testing. To build a Colcon project, you can run:

colcon build

This looks at each subdirectory in whichever directory you’re in, checks if it is a ROS directory, and if so builds it. There are additional command-line options for choosing which subdirectories to include and ignore, such as:

To run Colcon tests, you can use:

colcon test

After finishing, you can see any failures by running:

colcon test-result            # To show the test result summary files
colcon test-result --verbose  # To show all errors

ament / catkin

Ament and Catkin are build tools for ROS. Catkin was used for ROS 1, Ament has been used for ROS 2. Both have been superseded by Colcon (see here and here).

Gazebo

Gazebo is a simulator package which interacts nicely with ROS.

Cheat Sheet

Here’s a reference ROS 2 command cheat sheet.

Base Command Command Description
action info Output information about an action
  list Output a list of action names
  list -t List actions with types
  send_goal Send an action goal
bag info Output information of a bag
  play Play a bag
  record Record a bag
launch   Run a launch file
node info Output information about a node
  list Output a list of available nodes
pkg create Create a new package
  executables Output a list of executables in a package
  list Output a list of available packages
run   Run an executable in a given package
service call Call a service manually
  find Output a list of services of a given type
  list Output a list of service names
  type Output a service’s type
test   Run launch test
topic bw Show bandwidth usage for a topic
  delay Display delay of topic from timestamp in header
  echo Output messages of a given topic
  find Find topics of a given type
  hz Get publishing rate of a topic
  info Output information about a topic
  list Output list of active topics
  pub Publish data to a topic
  type Output type of a topic

More Resources

Some other resources that I found helpful are listed below.

Resume Github Twitter Email Feed Directory Home uBlock