ROS2 topics

You learned how to use ROS2 packages to start one or several nodes. You also learned how to create your own ROS2 programs with Python. In this article, you will learn how to subscribe to a Topic and how to publish to a Topic.

There are many sources covering ROS2 Topics such as the official ROS2 documentation.

What are Topics?

As already mentioned earlier, a Topic is a way of communication between ROS2 nodes. This protocol created a data stream from a Publisher to a Subscriber. It is possible that several Publishers are sending data to a Topic at the same time and several Subscribers can listen to a Topic simultaneously.

Each Topic consists of a Topic name and a message type. The name is used to refer to a specific Topic while the message type defines the actual structure of the content. A fairly common Topic name is /cmd_vel which contains a Twist message. Twist messages describe the three velocity parameters for the translation and rotation of a robot. Twist belongs to a category of ROS2 messages called geometry_msgs. This is simply the ROS2 package that contains these message definitions. Twist is defined as follows:

Vector3  linear:
    float64 x
    float64 y
    float64 z
Vector3  angular:
    float64 x
    float64 y
    float64 z

Note: You can find more references to the geometry_msgs Twist messages here and here.

This means that you can access the properties of a Twist object in the following way in Python:

my_message = Twist()

my_message.linear.x = 0
my_message.linear.y = 0
my_message.linear.z = 0
my_message.angular.x = 0
my_message.angular.y = 0
my_message.angular.z = 0

First, you define the name of the variable and set it to the variable type of Twist() which is a constructor that creates a Twist object. It initializes all the values to zero.

Topics can also be less complex data types such as Int or String which then only contain a simple integer or string value. These message types belong to the ROS package called std_msgs. Another very common type is sensor_msgs for IMU data, camera data or laser scanner data.

Using Topics in Terminal

In case you only want to see the content of a topic or see what topics are available, you don’t need to write a ROS2 program to listen to a Topic. You can do this in the terminal as well. You can even publish some data into a Topic. This is mainly used for testing purposes and not really used for actual robot control.

Starting TurtleSim

Open a terminal and start the turtlesim node with the following command:

$ ros2 run turtlesim turtlesim_node

A new window with the turtlesim application will appear.

Starting turtlesim node

The first thing you need to know is, how to find which Topics are already used by a robot. This is useful as you can use the Topics that are already available rather then creating a new Topic even though, it is not necessary.

Finding Information about the Topics

The following command (again in a new terminal) will show you a list of the Topics that are either being published or subscribed to by a node:

$ ros2 topic list

The output in your terminal should look like this:

Now, you know which topics are currently available. You can get more information about these Topics with the following command:

$ ros2 topic info /turtle1/pose

This command will provide the following information:

The information you get is that this topic is of type turtlesim/msg/Pose which means it is a message type inside the package called turtlesim. The message type is Pose and it contains the following information:

float32 x
float32 y
float32 theta

float32 linear_velocity
float32 angular_velocity

The Pose messages contain information about the current position and orientation of the turtle and the linear and angular velocity. This block of information is published by the turtlesim regularly.

Subscriber

So the first thing you want to learn is how to see what is inside a Topic. Let’s take /turtle1/pose for example. You can listen to this Topic by using your terminal with the following command:

$ ros2 topic echo /turtle1/pose

Now, you will see something like the following:

You can stop incoming messages by hitting CRTL+c on your keyboard. The Topic that you are looking at is showing you the position of the little turtle on the canvas.

Publisher

Just like you can listen to a Topic through the terminal, you can also write messages to a Topic through the terminal. Therefore, you can use the following command to write to /turtle1/cmd_vel:

$ ros2 topic pub /turtle1/cmd_vel geometry_msgs/Twist "{linear: {x: 0.25, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.25}}" -r 10

Your terminal will look like this:

TOS2 topic pub to publish cmd_vel messages

You will see that the terminal is sending messages to the turtlesim. In this case, it tells the turtle to move forward and to the left. As a result, the turtle is making a circle. The units in the command are given in m/s and rad/s. The general command is:

$ ros2 topic pub <topic_name> <message_type> "<topic_message>"

You might have noticed that we added a -r 10 at the end of the message. Many ROS2 commands have additional arguments that you can provide. The argument -r 10 instructs the command to repeat the message at a rate of 10 Hz.

Using the terminal is mainly used for quick verification or testing of a system or for a single event that doesn’t need repetition. For controlling a robot, you will probably write a program that will perform the same tasks autonomously.

More information on ROS2 message types can be found on the ROS2 overview page.

Using Topics in Python

Publisher

To start, you can go to the package you already made earlier:

$ cd ~/ros2_ws/src/my_turtlesim/my_turtlesim/

Now you can create a new file called my_simple_publisher.py in which you will write the Python code to create a Publisher node:

$ gedit my_simple_publisher.py

Add the following code to the file:

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


class MySimplePublisher(Node):

    def __init__(self):
        super().__init__('my_simple_publisher')
        self.get_logger().info('Creating simple publisher!')
        self.publisher_ = self.create_publisher(Twist, 'turtle1/cmd_vel', 10)
        timer_period = 0.5  # seconds
        self.timer = self.create_timer(timer_period, self.publish_velocity_callback)
        self.i = 0

    def publish_velocity_callback(self):
        my_velocity = Twist()
        my_velocity.linear.x = 0.5
        my_velocity.angular.z = 0.5
        self.publisher_.publish(my_velocity)
        self.get_logger().info(f"Publishing velocity: \n\t linear.x: {my_velocity.linear.x}; \n\t linear.z: {my_velocity.linear.x}")
        self.i += 1


def main(args=None):
    # initiate ROS2
    rclpy.init(args=args)

    # create an instance of the node
    my_simple_publisher = MySimplePublisher()

    # keep the node alive intil pressing CTRL+C
    rclpy.spin(my_simple_publisher)
    
    # destroy the node when it is not used anymore
    my_simple_publisher.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

You start with importing the rclpy module, the Node class, and the Twist message as we will use this message to publish to the topic “/turtle1/cmd_vel”.

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

Next, you need to create a class that inherits from the Node class object. Let’s call it MySimplePublisher. It initialises a ROS2 node to allow using ROS2 communication methods like topics. Next, we need a publisher object that we call self.publisher. It defines the topic name, the message type and the queue size. Since we want the node to publish the messages at a regular time interval, we define a timer that waits for 0.5 seconds and then we create a timer that triggers a callback function based on this 0.5 seconds.

class MySimplePublisher(Node):

    def __init__(self):
        super().__init__('my_simple_publisher')
        self.get_logger().info('Creating simple publisher!')
        self.publisher_ = self.create_publisher(Twist, 'turtle1/cmd_vel', 10)
        timer_period = 0.5  # seconds
        self.timer = self.create_timer(timer_period, self.publish_velocity_callback)
        self.i = 0

A callback is a function that is triggered by an event rather than a specified sequence in the program code. In this case, the timer will trigger the callback function every 0.5 seconds, or at a frequency of 2 Hz.

This callback function then creates a message of the type Twist and defines the value for the forward direction (linear.x) and the rotation to the left (angular.z). Lastly, the callback publishes the Twist message and writes a message to the terminal.

    def publish_velocity_callback(self):
        my_velocity = Twist()
        my_velocity.linear.x = 0.5
        my_velocity.angular.z = 0.5
        self.publisher_.publish(my_velocity)
        self.get_logger().info(f"Publishing velocity: \n\t linear.x: {my_velocity.linear.x}; \n\t linear.z: {my_velocity.linear.x}")
        self.i += 1

Now, in the main function of the program, you initialise the ROS2 library and create an instance of your custom node class. The rclpy.spin() function makes sure that this instance keeps running until it gets shut down.

def main(args=None):
    # initiate ROS2
    rclpy.init(args=args)

    # create an instance of the node
    my_simple_publisher = MySimplePublisher()

    # keep the node alive intil pressing CTRL+C
    rclpy.spin(my_simple_publisher)
    
    # destroy the node when it is not used anymore
    my_simple_publisher.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

You need to make the Python program executable before you can build the package again and run it:

$ chmod +x my_simple_publisher.py

Also, you need to add the new node to the setup.py file. Make sure the entry_point looks as follows:

    entry_points={
        'console_scripts': [
            'my_first_node = my_turtlesim.my_first_program:main',
            'my_simple_publisher = my_turtlesim.my_simple_publisher:main',
        ],
    },

To build the package, go to the root directory of your workspace and build it:

$ cd ~/ros2_ws/
$ colcon build --packages-select my_turtlesim --symlink-install

This time, we use two additional flags compared to the last time we built the workspace. --packages-select <package_name> allows you to build only a single package. In this case, it does not affect anything, since we only have a single package in our workspace, but if you have multiple packages, it allows you to build only the ones that you are currently interested in. The flag --symlink-install allows you to modify the Python code without rebuilding the package. By default, colcon makes a copy of the source code and then runs that copy when executing the program. Since this package contains Python files, it should be possible to change the code and run it without building the package. This flag makes this possible. This allows us to quickly make changes to the code.

The package should have been built without issues.

Building the ROS2 topic publisher

Before running the new node, you need to source the workspace again:

$ source install/local_setup.bash

Make sure you have a turtlesim node running. If not, you can start one with the command:

$ ros2 run turtlesim turtlesim_node

In a separate terminal, you can start your publisher node with the following command:

$ ros2 run my_turtlesim my_simple_publisher.py

You will notice that the turtle will start to move in a circle.

Simple publisher making the turtle move

You can also verify the published messages with the following command:

$ ros2 topic echo /turtle1/cmd_vel

This command will show you the messages sent to the topic /turtle1/cmd_vel, which are the Twist messages that your my_simple_publisher.py program is publishing. The command above will show the following:

Subscriber

When making a robotic system, you are more likely to create a ROS2 node that will take the role of a Subscriber. This has the advantage that you can automatically listen to a Topic and then act depending on the data your program receives.

Let’s make a node that listens to the “/turtle1/pose” topic to get the position of the turtlesim.

To start, you can go to the package you already made earlier:

$ cd ~/ros2_ws/src/my_turtlesim/my_turtlesim/

Now you can create a new file called my_simple_subscriber.py in which you will write the Python code to create a Subscriber node:

$ gedit my_simple_subscriber.py

Now, an empty text editor window will pop up where you can type down the following code:

import rclpy
from rclpy.node import Node
from turtlesim.msg import Pose


class MySimpleSubscriber(Node):

    def __init__(self):
        super().__init__('minimal_subscriber')
        self.my_subscriber = self.create_subscription(
            Pose,
            'turtle1/pose',
            self.listener_callback,
            10)
        # prevent warning that self.my_subscriber is not used
        self.my_subscriber

    def listener_callback(self, msg):
        self.get_logger().info(f'Turtle found at x: {msg.x}; y: {msg.y}')


def main(args=None):
    rclpy.init(args=args)

    my_simple_subscriber = MySimpleSubscriber()
    rclpy.spin(my_simple_subscriber)

     # destroy the node when it is not used anymore
    my_simple_subscriber.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

First, you need to import the rclpy and its Node class. Then, you also import the Pose message type.

import rclpy
from rclpy.node import Node
from turtlesim.msg import Pose

You have to create a new class that inherits from the Node class. Let’s call it MySimpleSubscriber. In its init function, you need to give it a node name. Also, you can create a subscriber object that defines the topic message type, the topic name, the callback function and the queue size. This callback function gets triggered every time a new message is coming in from the “/turtle1/pose” topic. Here, we also call the self.subscriber object once so that the program will not complain about an unused variable.

class MySimpleSubscriber(Node):

    def __init__(self):
        super().__init__('minimal_subscriber')
        self.my_subscriber = self.create_subscription(
            Pose,
            'turtle1/pose',
            self.listener_callback,
            10)
        # prevent warning that se.fmy_subscriber is not used
        self.my_subscriber

The callback for the subscriber is very simple as we just want to print the message in the terminal, using the self.get_logger().info() function.

    def listener_callback(self, msg):
        self.get_logger().info(f'Turtle found at x: {msg.x}; y: {msg.y}')

In the main function, we can find the initialisation of the rclpy and the instance of our subscriber node class. Then, the node is kept alive with the rclpy.spin() function. Lastly, we clean up the node when the program is finished and close the program.

def main(args=None):
    rclpy.init(args=args)

    my_simple_subscriber = MySimpleSubscriber()
    rclpy.spin(my_simple_subscriber)

     # destroy the node when it is not used anymore
    my_simple_subscriber.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

Save the code and then make the file executable with the following command:

$ chmod +x ~/ros2_ws/src/my_turtlesim/my_turtlesim/my_simple_subscriber.py

Add the new so it will look as followsnode to the setup.py file:

    entry_points={
        'console_scripts': [
            'my_first_node = my_turtlesim.my_first_program:main',
            'my_simple_publisher = my_turtlesim.my_simple_publisher:main',
            'my_simple_subscriber = my_turtlesim.my_simple_subscriber:main',
        ],
    },

You need to return to the workspace root directory to build the package once again. This is necessary since we added a new file. Also here, we can use the --symlink-install flag to allow changing the code later if necessary:

$ cd ~/ros2_ws/
$ colcon build --symlink-install

Now, source the package and run the new node:

$ source install/local_setup.bash
$ ros2 run my_turtlesim my_simple_subscriber.py

The subscriber will print the pose data of the turtle in the terminal. If this is not the case, make sure that the turtlesim node is running in another terminal.

My simple subscriber receiving the pose messages

Currently, the message will always be the same unless you make the turtle move. For this, you could use the publisher from earlier.

When using Python to access Topics, you can have two different kinds of programs: a Subscriber or a Publisher. In addition, you can also have a program that implements several Subscribers or several Publishers or even both.

A common use case is that a publisher is being called in the callback function of a subscriber. As a result, the node will send a new message for each incoming message. This can be handy when one topic contains information related to the subscribed topic. This could be an image filter or a node that verifies the distance towards an object.

That’s it. Now you are able to write nodes that can subscribe or publish to topics. In some cases, you need to combine both in a single program. For now, you can continue with Services and how to set up a Service Server and how to create a Service Client.