Note: This is part of a series where I follow along with the ROS 2 Humble and Gazebo Fortress tutorials and demos. This is not original content.
Today we’ll follow along with this tutorial from the ROS 2 Humble docs. Like yesterday’s post, we’ll be creating a publisher and a subscriber, but this time in Python.
Shall we?
Basics Link to heading
Once again we’re going to be creating a workspace, creating a package, and then writing code for ROS 2 nodes which will send and receive messages via a topic. The sender is the publisher and the receiver is the subscriber.
Creating a workspace and a package Link to heading
Let’s start by creating a new workspace and a new package within said workspace:
mkdir -p ~/pubsub_py_ws
mkdir -p ~/pubsub_py_ws/src
cd ~/pubsub_py_ws/src
ros2 pkg create --build-type ament_python --license Apache-2.0 py_pubsub
Per usual, some messages will be printed out to let us know everything worked as expected:
going to create a new package
package name: py_pubsub
destination directory: /home/ubuntu/pubsub_py_ws/src
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['ubuntu <ubuntu@todo.todo>']
licenses: ['Apache-2.0']
build type: ament_python
dependencies: []
creating folder ./py_pubsub
creating ./py_pubsub/package.xml
creating source folder
creating folder ./py_pubsub/py_pubsub
creating ./py_pubsub/setup.py
creating ./py_pubsub/setup.cfg
creating folder ./py_pubsub/resource
creating ./py_pubsub/resource/py_pubsub
creating ./py_pubsub/py_pubsub/__init__.py
creating folder ./py_pubsub/test
creating ./py_pubsub/test/test_copyright.py
creating ./py_pubsub/test/test_flake8.py
creating ./py_pubsub/test/test_pep257.py
We need to navigate down a couple levels in the directory hierarchy (right now we are in ~/pubsub_py_ws/src
) and fetch the exmaple code from the good folks who write and maintain ROS:
cd py_pubsub/py_pubsub
wget https://raw.githubusercontent.com/ros2/examples/humble/rclpy/topics/minimal_publisher/examples_rclpy_minimal_publisher/publisher_member_function.py
A new file publisher_member_function.py
should now be located in ~/pubsub_py_ws/src/py_pubsub/py_pubsub
.
Here’s the full code:
# Copyright 2016 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class MinimalPublisher(Node):
def __init__(self):
super().__init__('minimal_publisher')
self.publisher_ = self.create_publisher(String, 'topic', 10)
timer_period = 0.5 # seconds
self.timer = self.create_timer(timer_period, self.timer_callback)
self.i = 0
def timer_callback(self):
msg = String()
msg.data = 'Hello World: %d' % self.i
self.publisher_.publish(msg)
self.get_logger().info('Publishing: "%s"' % msg.data)
self.i += 1
def main(args=None):
rclpy.init(args=args)
minimal_publisher = MinimalPublisher()
rclpy.spin(minimal_publisher)
# Destroy the node explicitly
# (optional - otherwise it will be done automatically
# when the garbage collector destroys the node object)
minimal_publisher.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
As before, there are some import statements at the top which fetch important ROS builtins:
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
Again, we will use the “string” message type, one of the standard ROS 2 message types.
Again, we set up a MinimalPublisher
class that sets a counter and creates a timer which triggers twice a second. The code is extremely similar to the C++ code we wrote yesterday (as it should be! this code does the exact same thing).
Adding dependencies Link to heading
Just as before, we need to update a couple different files to make sure ROS 2 will pull in the required dependencies for our package. In this casee, we need to do a couple of things:
- Add dependencies to
package.xml
- Add an entry point to indicate where our Python code should start running from
- Confirm there are no issues with
setup.cfg
(it should have been generated correctly already when we created our pack age)
Below are my copies of those files, which should work just fine.
Complete code for package.xml
Link to heading
<?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>py_pubsub</name>
<version>0.0.0</version>
<description>TODO: Package description</description>
<maintainer email="ubuntu@todo.todo">ubuntu</maintainer>
<license>Apache-2.0</license>
<exec_depend>rclpy</exec_depend>
<exec_depend>std_msgs</exec_depend>
<test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend>
<test_depend>ament_pep257</test_depend>
<test_depend>python3-pytest</test_depend>
<export>
<build_type>ament_python</build_type>
</export>
</package>
Complete code for setup.py
Link to heading
from setuptools import find_packages, setup
package_name = 'py_pubsub'
setup(
name=package_name,
version='0.0.0',
packages=find_packages(exclude=['test']),
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='YourName',
maintainer_email='you@email.com',
description='Examples of minimal publisher/subscriber using rclpy',
license='Apache License 2.0',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'talker = py_pubsub.publisher_member_function:main',
],
},
)
Complete code for setup.cfg
Link to heading
[develop]
script_dir=$base/lib/py_pubsub
[install]
install_scripts=$base/lib/py_pubsub
Writing the subscriber code Link to heading
Now we return to ~/pubsub_py_ws/src/py_pubsub/py_pubsub
and add the code for the subscriber:
cd ~/pubsub_py_ws/src/py_pubsub/py_pubsub
wget https://raw.githubusercontent.com/ros2/examples/humble/rclpy/topics/minimal_subscriber/examples_rclpy_minimal_subscriber/subscriber_member_function.py
Behold! A new subscriber_member_function.py
appears. Here it is:
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class MinimalSubscriber(Node):
def __init__(self):
super().__init__('minimal_subscriber')
self.subscription = self.create_subscription(
String,
'topic',
self.listener_callback,
10)
self.subscription # prevent unused variable warning
def listener_callback(self, msg):
self.get_logger().info('I heard: "%s"' % msg.data)
def main(args=None):
rclpy.init(args=args)
minimal_subscriber = MinimalSubscriber()
rclpy.spin(minimal_subscriber)
# Destroy the node explicitly
# (optional - otherwise it will be done automatically
# when the garbage collector destroys the node object)
minimal_subscriber.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
I won’t go through it in detail as it’s very similar to yesterday’s C++ subscriber example.
Update setup.py
again
Link to heading
One thing we must do is make sure that we add another entrypoint to setup.py
for our listener code, so that it starts when we run our node. Here’s the updated setup.py
in its entirety (not we have only added a single line to the entry_points
field…that’s all we need to do):
from setuptools import find_packages, setup
package_name = 'py_pubsub'
setup(
name=package_name,
version='0.0.0',
packages=find_packages(exclude=['test']),
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='YourName',
maintainer_email='you@email.com',
description='Examples of minimal publisher/subscriber using rclpy',
license='Apache License 2.0',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'talker = py_pubsub.publisher_member_function:main',
'listener = py_pubsub.subscriber_member_function:main',
],
},
)
Build and run Link to heading
Once again we need to return to the parent directory (in my case ~/pubsub_py_ws/
) and pull in any dependencies (our dependencies are simple, so it’s likely they are already installed, but let’s be sure):
cd ~/pubsub_py_ws/
rosdep install -i --from-path src --rosdistro humble -y
And then…
colcon build --packages-select py_pubsub
Which will output the usual messages letting us know what’s going on with the (very short) build process:
Starting >>> py_pubsub
Finished <<< py_pubsub [1.12s]
Summary: 1 package finished [1.48s]
Testing things out Link to heading
As before, we need to start up two terminals, source our overlay workspace with source install/setup.bash
in each, and then run our “talker” and “listener”. Respectively.
Here goes.
First, the talker…
source install/setup.bash
ros2 run py_pubsub talker
Which outputs:
[INFO] [1723720591.421781152] [minimal_publisher]: Publishing: "Hello World: 0"
[INFO] [1723720591.910507279] [minimal_publisher]: Publishing: "Hello World: 1"
[INFO] [1723720592.410504728] [minimal_publisher]: Publishing: "Hello World: 2"
[INFO] [1723720592.910505471] [minimal_publisher]: Publishing: "Hello World: 3"
[INFO] [1723720593.410500300] [minimal_publisher]: Publishing: "Hello World: 4"
[INFO] [1723720593.910504476] [minimal_publisher]: Publishing: "Hello World: 5"
[INFO] [1723720594.410517831] [minimal_publisher]: Publishing: "Hello World: 6"
[INFO] [1723720594.910596502] [minimal_publisher]: Publishing: "Hello World: 7"
[INFO] [1723720595.410561800] [minimal_publisher]: Publishing: "Hello World: 8"
[INFO] [1723720595.910863764] [minimal_publisher]: Publishing: "Hello World: 9"
[INFO] [1723720596.410524523] [minimal_publisher]: Publishing: "Hello World: 10"
[INFO] [1723720596.910598926] [minimal_publisher]: Publishing: "Hello World: 11"
[INFO] [1723720597.410591834] [minimal_publisher]: Publishing: "Hello World: 12"
[INFO] [1723720597.910608853] [minimal_publisher]: Publishing: "Hello World: 13"
[INFO] [1723720598.410591086] [minimal_publisher]: Publishing: "Hello World: 14"
[INFO] [1723720598.910569253] [minimal_publisher]: Publishing: "Hello World: 15"
[INFO] [1723720599.410811425] [minimal_publisher]: Publishing: "Hello World: 16"
[INFO] [1723720599.910854850] [minimal_publisher]: Publishing: "Hello World: 17"
[INFO] [1723720600.410799816] [minimal_publisher]: Publishing: "Hello World: 18"
And then the listener…
source install/setup.bash
ros2 run py_pubsub listener
Which outputs:
[INFO] [1723720596.922454042] [minimal_subscriber]: I heard: "Hello World: 11"
[INFO] [1723720597.410871075] [minimal_subscriber]: I heard: "Hello World: 12"
[INFO] [1723720597.910885299] [minimal_subscriber]: I heard: "Hello World: 13"
[INFO] [1723720598.410852076] [minimal_subscriber]: I heard: "Hello World: 14"
[INFO] [1723720598.910832905] [minimal_subscriber]: I heard: "Hello World: 15"
[INFO] [1723720599.411145130] [minimal_subscriber]: I heard: "Hello World: 16"
[INFO] [1723720599.911123100] [minimal_subscriber]: I heard: "Hello World: 17"
[INFO] [1723720600.411113397] [minimal_subscriber]: I heard: "Hello World: 18"
Naturally the listener does not start from message 0….because I started it a little while after the publisher was already running (previous messages are not stored, the listener will simply pick up on the first available message that is sent to the topic after it enters a running state).
Other stuff… Link to heading
An interesting thing to note is how much “noisier” the exit process is for the Python talker and listener, when compared to their C++ equivalents.
For instance, when hit Ctrl+C
to stop the Python “talker”, I see this message:
^CTraceback (most recent call last):
File "/home/ubuntu/pubsub_py_ws/install/py_pubsub/lib/py_pubsub/listener", line 33, in <module>
sys.exit(load_entry_point('py-pubsub==0.0.0', 'console_scripts', 'listener')())
File "/home/ubuntu/pubsub_py_ws/install/py_pubsub/lib/python3.10/site-packages/py_pubsub/subscriber_member_function.py", line 41, in main
rclpy.spin(minimal_subscriber)
File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/__init__.py", line 222, in spin
executor.spin_once()
File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py", line 739, in spin_once
self._spin_once_impl(timeout_sec)
File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py", line 728, in _spin_once_impl
handler, entity, node = self.wait_for_ready_callbacks(timeout_sec=timeout_sec)
File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py", line 711, in wait_for_ready_callbacks
return next(self._cb_iter)
File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py", line 608, in _wait_for_ready_callbacks
wait_set.wait(timeout_nsec)
KeyboardInterrupt
[ros2run]: Interrupt
This is not a problem, it’s just extra detail from Python about why the program quit…which might even be useful for me when I’m debugging things later on.
That’s it! See you next time.