Publishers and Subscribers: using messages

Note

Except for the particulars of the setup.py file, the way that publishers and subscribers in ROS2 work in Python, i.e. the explanation in this section, does not depend on ament_python or ament_cmake.

Finally, we reached the point where ROS2 becomes appealing. As you saw in the last section, we can easily create complex interface types using an easy and generic description. We can use those to provide interprocess communication, i.e. two different programs talking to each other, which otherwise can be error-prone and very difficult to implement.

ROS2 works on a model in which any number of processes can communicate over a Topic that only accepts one message type. Each topic is uniquely identified by a string.

Then

  • A program that sends (publishes) information to the topic has a Publisher.

  • A program that reads (subscribes) information from a topic has a Subscriber.

Each Node can have any number of Publishers and Subscribers and a combination thereof, connecting to an arbitrary number of Nodes. This forms part of the connections in the so-called ROS graph.

Create the package

First, let us create an ament_python package that depends on our newly developed packages_with_interfaces and build from there.

cd ~/ros2_tutorial_workspace/src
ros2 pkg create python_package_that_uses_the_messages \
--build-type ament_python \
--dependencies rclpy package_with_interfaces

Overview

Note

By no coincidence, I am using the terminology Node with a publisher, and Node with a subscriber. In general, each Node will have a combination of publishers, subscribers, and other interfaces.

Before we start exploring the elements of the package, let us

  1. Create the Node with a publisher.

  2. Create the Node with a subscriber.

  3. Update the setup.py so that ros2 run finds these programs.

Create the Node with a publisher

TL;DR Creating a publisher

  1. Add new dependencies to package.xml

  2. Import new messages from <package_name>.msg import <msg_name>

  3. In a subclass of Node

    1. Create a publisher with self.publisher = self.create_publisher(...)

    2. Send messages with self.publisher.publish(....)

  4. Add the new Node to setup.py

For the publisher, create a file in python_package_that_uses_the_messages/python_package_that_uses_the_messages called amazing_quote_publisher_node.py, with the following contents

amazing_quote_publisher_node.py

 1import rclpy
 2from rclpy.node import Node
 3from package_with_interfaces.msg import AmazingQuote
 4
 5
 6class AmazingQuotePublisherNode(Node):
 7    """A ROS2 Node that publishes an amazing quote."""
 8
 9    def __init__(self):
10        super().__init__('amazing_quote_publisher_node')
11
12        self.amazing_quote_publisher = self.create_publisher(
13            msg_type=AmazingQuote,
14            topic='/amazing_quote',
15            qos_profile=1)
16
17        timer_period: float = 0.5
18        self.timer = self.create_timer(timer_period, self.timer_callback)
19
20        self.incremental_id: int = 0
21
22    def timer_callback(self):
23        """Method that is periodically called by the timer."""
24
25        amazing_quote = AmazingQuote()
26        amazing_quote.id = self.incremental_id
27        amazing_quote.quote = 'Use the force, Pikachu!'
28        amazing_quote.philosopher_name = 'Uncle Ben'
29
30        self.amazing_quote_publisher.publish(amazing_quote)
31
32        self.incremental_id = self.incremental_id + 1
33
34
35def main(args=None):
36    """
37    The main function.
38    :param args: Not used directly by the user, but used by ROS2 to configure
39    certain aspects of the Node.
40    """
41    try:
42        rclpy.init(args=args)
43
44        amazing_quote_publisher_node = AmazingQuotePublisherNode()
45
46        rclpy.spin(amazing_quote_publisher_node)
47    except KeyboardInterrupt:
48        pass
49    except Exception as e:
50        print(e)
51
52
53if __name__ == '__main__':
54    main()

When we built our package_with_interfaces in the last section, what ROS2 did for us, among other things, was create a Python library called package_with_interfaces.msg containing the Python implementation of the AmazingQuote.msg. Because of that, we can use it by importing it like so

import rclpy
from rclpy.node import Node
from package_with_interfaces.msg import AmazingQuote

The publisher must be created with the Node.create_publisher(...) method, which has the three parameters defined in the publisher and subscriber parameter table.

        self.amazing_quote_publisher = self.create_publisher(
            msg_type=AmazingQuote,
            topic='/amazing_quote',
            qos_profile=1)

msg_type

A class, namely the message that will be used in the topic. In this case, AmazingQuote.

topic

The topic through which the communication will occur. Can be arbitrarily chosen, but to make sense /amazing_quote.

qos_profile

The simplest interpretation for this parameter is the number of messages that will be stored in the spin(...) takes too long to process them. (See more on docs for QoSProfile.)

Warning

All the arguments in publisher and subscriber parameter table should be EXACTLY the same in the Publishers and Subscribers of the same topic.

Then, each message is handled much like any other class in Python. We instantiate and initialize the message as follows

        amazing_quote = AmazingQuote()
        amazing_quote.id = self.incremental_id
        amazing_quote.quote = 'Use the force, Pikachu!'
        amazing_quote.philosopher_name = 'Uncle Ben'

Lastly, the message needs to be published using Node.publish(msg).

        self.amazing_quote_publisher.publish(amazing_quote)

Note

In general, the message will NOT be published instantaneously after Node.publish() is called. It is usually fast, but entirely dependent on rclpy.spin() and how much work it is doing.

Create the Node with a subscriber

TL;DR Creating a subscriber

  1. Add new dependencies to package.xml

  2. Import new messages from <package_name>.msg import <msg_name>

  3. In a subclass of Node

    1. Create a callback def callback(self, msg):

    2. Create a subscriber self.subscriber = self.create_subscription(...)

  4. Add the new Node to setup.py

For the subscriber Node, create a file in python_package_that_uses_the_messages/python_package_that_uses_the_messages called amazing_quote_subscriber_node.py, with the following contents

amazing_quote_subscriber_node.py

 1import rclpy
 2from rclpy.node import Node
 3from package_with_interfaces.msg import AmazingQuote
 4
 5
 6class AmazingQuoteSubscriberNode(Node):
 7    """A ROS2 Node that receives and prints an amazing quote."""
 8
 9    def __init__(self):
10        super().__init__('amazing_quote_subscriber_node')
11        self.amazing_quote_subscriber = self.create_subscription(
12            msg_type=AmazingQuote,
13            topic='/amazing_quote',
14            callback=self.amazing_quote_subscriber_callback,
15            qos_profile=1)
16
17    def amazing_quote_subscriber_callback(self, msg: AmazingQuote):
18        """Method that is periodically called by the timer."""
19        self.get_logger().info(f"""
20        I have received the most amazing of quotes.
21        It says
22            
23               '{msg.quote}'
24               
25        And was thought by the following genius
26            
27            -- {msg.philosopher_name}
28            
29        This latest quote had the id={msg.id}.
30        """)
31
32
33def main(args=None):
34    """
35    The main function.
36    :param args: Not used directly by the user, but used by ROS2 to configure
37    certain aspects of the Node.
38    """
39    try:
40        rclpy.init(args=args)
41
42        amazing_quote_subscriber_node = AmazingQuoteSubscriberNode()
43
44        rclpy.spin(amazing_quote_subscriber_node)
45    except KeyboardInterrupt:
46        pass
47    except Exception as e:
48        print(e)
49
50
51if __name__ == '__main__':
52    main()

Similarly to the publisher, in the subscriber, we start by importing the message in question

import rclpy
from rclpy.node import Node
from package_with_interfaces.msg import AmazingQuote

Then, in our subclass of Node, we call Node.create_publisher(...) as follows

        self.amazing_quote_subscriber = self.create_subscription(
            msg_type=AmazingQuote,
            topic='/amazing_quote',
            callback=self.amazing_quote_subscriber_callback,
            qos_profile=1)

where the only difference with respect to the publisher is the third argument, namely callback, in which a method that receives a msg_type and returns nothing is expected. For example, the amazing_quote_subscriber_callback.

    def amazing_quote_subscriber_callback(self, msg: AmazingQuote):
        """Method that is periodically called by the timer."""
        self.get_logger().info(f"""
        I have received the most amazing of quotes.
        It says
            
               '{msg.quote}'
               
        And was thought by the following genius
            
            -- {msg.philosopher_name}
            
        This latest quote had the id={msg.id}.
        """)

That callback method will be automatically called by ROS2, as one of the tasks performed by rclpy.spin(Node). Depending on the qos_profile, it will not necessarily be the latest message.

Note

The message will ALWAYS take some time between being published and being received by the subscriber. The speed in which that will happen will depend not only on this Node’s rclpy.spin(), but also on the rclpy.spin() of the publisher node and the communication channel.

Update the setup.py

As we already learned in Making ros2 run work, we must adjust the setup.py to refer to the Nodes we just created.

setup.py

 1from setuptools import setup
 2
 3package_name = 'python_package_that_uses_the_messages'
 4
 5setup(
 6    name=package_name,
 7    version='0.0.0',
 8    packages=[package_name],
 9    data_files=[
10        ('share/ament_index/resource_index/packages',
11         ['resource/' + package_name]),
12        ('share/' + package_name, ['package.xml']),
13    ],
14    install_requires=['setuptools'],
15    zip_safe=True,
16    maintainer='murilo',
17    maintainer_email='murilomarinho@ieee.org',
18    description='TODO: Package description',
19    license='TODO: License declaration',
20    tests_require=['pytest'],
21    entry_points={
22        'console_scripts': [
23            'amazing_quote_publisher_node = python_package_that_uses_the_messages.amazing_quote_publisher_node:main',
24            'amazing_quote_subscriber_node = python_package_that_uses_the_messages.amazing_quote_subscriber_node:main'
25        ],
26    },
27)

Build and source

Before we proceed, let us build and source once.

cd ~/ros2_tutorial_workspace
colcon build
source install/setup.bash

Note

If you don’t remember why we’re building with these commands, see Always source after you build.

Testing Publisher and Subscriber

Whenever we need to open two or more terminal windows, remember that Terminator is life.

Let us open two terminals.

In the first terminal, we run

ros2 run python_package_that_uses_the_messages amazing_quote_publisher_node

Nothing, in particular, should happen now. The publisher is sending messages through the specific topic we defined, but we need at least one subscriber to interact with those messages.

Hence, in the second terminal, we run

ros2 run python_package_that_uses_the_messages amazing_quote_subscriber_node

which outputs

[INFO] [1684215672.344532584] [amazing_quote_subscriber_node]:
     I have received the most amazing of quotes.
     It says

            'Use the force, Pikachu!'

     And was thought by the following genius

         -- Uncle Ben

     This latest quote had the id=3.

[INFO] [1684215672.844618237] [amazing_quote_subscriber_node]:
     I have received the most amazing of quotes.
     It says

            'Use the force, Pikachu!'

     And was thought by the following genius

         -- Uncle Ben

     This latest quote had the id=4.

[INFO] [1684215673.344514856] [amazing_quote_subscriber_node]:
     I have received the most amazing of quotes.
     It says

            'Use the force, Pikachu!'

     And was thought by the following genius

         -- Uncle Ben

     This latest quote had the id=5.

Note

If there are any issues with either the publisher or the subscriber, this connection will not work. In the next section, we’ll see strategies to help us troubleshoot and understand communication through topics.

Warning

Unless instructed otherwise, the publisher does NOT wait for a subscriber to connect before it starts publishing the messages. As shown in the case above, the first message we received started with id=3. If we delayed longer to start the publisher, we would have received later messages only.

Let’s close each node with CTRL+C on each terminal before we proceed to the next tutorial.