Publishers and Subscribers: using messages#
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 messages work 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 one or more
Publisher(s).A program that reads (subscribes) information from a topic has one or more
Subscriber(s).
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. An example is shown below.
Diagram#
Note
This is an abstraction. As long as the information flows in this manner, it does not mean that an entity called topic must exist.
In ROS2, this type of communication happens, in fact, peer-to-peer.
Package structure#
This will be the structure of the package. The main elements are highlighted.
python_package_that_uses_the_messages/
|-- package.xml
|-- python_package_that_uses_the_messages
| |-- __init__.py
| |-- amazing_quote_publisher_node.py
| `-- amazing_quote_subscriber_node.py
|-- resource
| `-- python_package_that_uses_the_messages
|-- setup.cfg
|-- setup.py
`-- test
|-- test_copyright.py
|-- test_flake8.py
`-- test_pep257.py
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
ros2 pkg create output
going to create a new package
package name: python_package_that_uses_the_messages
destination directory: /root/ros2_tutorial_workspace/src
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['root <murilo.marinho@manchester.ac.uk>']
licenses: ['TODO: License declaration']
build type: ament_python
dependencies: ['rclpy', 'package_with_interfaces']
creating folder ./python_package_that_uses_the_messages
creating ./python_package_that_uses_the_messages/package.xml
creating source folder
creating folder ./python_package_that_uses_the_messages/python_package_that_uses_the_messages
creating ./python_package_that_uses_the_messages/setup.py
creating ./python_package_that_uses_the_messages/setup.cfg
creating folder ./python_package_that_uses_the_messages/resource
creating ./python_package_that_uses_the_messages/resource/python_package_that_uses_the_messages
creating ./python_package_that_uses_the_messages/python_package_that_uses_the_messages/__init__.py
creating folder ./python_package_that_uses_the_messages/test
creating ./python_package_that_uses_the_messages/test/test_copyright.py
creating ./python_package_that_uses_the_messages/test/test_flake8.py
creating ./python_package_that_uses_the_messages/test/test_pep257.py
[WARNING]: Unknown license 'TODO: License declaration'. This has been set in the package.xml, but no LICENSE file has been created.
It is recommended to use one of the ament license identifiers:
Apache-2.0
BSL-1.0
BSD-2.0
BSD-2-Clause
BSD-3-Clause
GPL-3.0-only
LGPL-3.0-only
MIT
MIT-0
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
Create the Node with a publisher.
Create the Node with a subscriber.
Update the
setup.pyso that ros2 run finds these programs.
Create the Node with a publisher#
TL;DR Creating a publisher
Add new dependencies to
package.xmlImport new messages
from <package_name>.msg import <msg_name>In a subclass of
NodeCreate a publisher with
self.publisher = self.create_publisher(...)Send messages with
self.publisher.publish(....)
Add the new Node to
setup.py
For the publisher, create a file called amazing_quote_publisher_node.py, with the following contents
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)
|
A class, namely the message that will be used in the topic. In this case, |
|
The topic through which the communication will occur. Can be arbitrarily chosen, but to make sense |
|
The simplest interpretation for this parameter is the maximum number of messages that will be stored in a buffer if your node (including |
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
Add new dependencies to
package.xmlImport new messages
from <package_name>.msg import <msg_name>In a subclass of
NodeCreate a callback
def callback(self, msg):Create a subscriber
self.subscriber = self.create_subscription(...)
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
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 AmazingQuote and prints out its info."""
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 called when a new msg is received by the node."""
19
20 self.get_logger().info(f"""
21 I have received the most amazing of quotes.
22 It says
23
24 '{msg.quote}'
25
26 And was thought by the following genius
27
28 -- {msg.philosopher_name}
29
30 This latest quote had the id={msg.id}.
31 """)
32
33
34def main(args=None):
35 """
36 The main function.
37 :param args: Not used directly by the user, but used by ROS2 to configure
38 certain aspects of the Node.
39 """
40 try:
41 rclpy.init(args=args)
42
43 amazing_quote_subscriber_node = AmazingQuoteSubscriberNode()
44
45 rclpy.spin(amazing_quote_subscriber_node)
46 except KeyboardInterrupt:
47 pass
48 except Exception as e:
49 print(e)
50
51
52if __name__ == '__main__':
53 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_subscription(...) 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 called when a new msg is received by the node."""
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.
~/ros2_tutorial_workspace/src/python_package_that_uses_the_messages/setup.py
1from setuptools import find_packages, setup
2
3package_name = 'python_package_that_uses_the_messages'
4
5setup(
6 name=package_name,
7 version='0.0.0',
8 packages=find_packages(exclude=['test']),
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='root',
17 maintainer_email='murilo.marinho@manchester.ac.uk',
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
For additional explanation and troubleshooting tips, see Always source after you build.
Warning
colcon will not work properly if your terminal has an active venv.
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] [1753664072.638312553] [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=37.
[INFO] [1753664073.121886428] [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=38.
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>0. 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.