8.3. Build your own Python ROS 2 package#
In previous tutorials, you learned how to
create a workspace and to
create a package from some template code generated by ros2 pkg create
.
In this section, you will create a ROS package containing a publisher and subscriber node written in Python, and make some adaptations to template’s configuration files for Python’s setuptools. The example used here is similar to the Python API intro, namely a simple “talker” and “listener” system: one node publishes data and the other subscribes to the topic so it can receive that data.
8.3.1. Create a new Python ROS package#
Open a new terminal and
source your ROS 2 installation so that ros2
commands will work.
Navigate into the ros2_ws
directory created in a
previous tutorial.
Recall that packages should be created in the src
directory, not the
root of the workspace. So, navigate into ros2_ws/src
, and run the
package creation command:
$ ros2 pkg create --build-type ament_python --license Apache-2.0 py_pubsub
Your terminal will return a message verifying the creation of your
package py_pubsub
and all its necessary files and folders.
8.3.2. Write the publisher node#
Navigate into ros2_ws/src/py_pubsub/py_pubsub
. Recall that this
directory is a Python
package with
the same name as the ROS 2 package it’s nested in.
Download the example talker code by entering the following command:
$ wget https://raw.githubusercontent.com/ros2/examples/humble/rclpy/topics/minimal_publisher/examples_rclpy_minimal_publisher/publisher_member_function.py
Now there will be a new file named publisher_member_function.py
adjacent to __init__.py
.
Open the file using your preferred text editor.
import rclpy
from rclpy.executors import ExternalShutdownException
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):
try:
with rclpy.init(args=args):
minimal_publisher = MinimalPublisher()
rclpy.spin(minimal_publisher)
except (KeyboardInterrupt, ExternalShutdownException):
pass
if __name__ == '__main__':
main()
Important
Note that this example differs somewhat with the talker example in the Python ROS API introduction.
We here wrap the node creation and
rclpy.spin()
main loop into an explicit function, here calledmain()
. This will be necessary to configure the package in a later step when we have to specify which Python function must be called to run our talker program.While not strictly necessary for creating a package, it is good practice to define your node as a new class that subclasses the
Node
class, rather that creating a “generic” node withrclpy.create_node()
. The code above defines aMinimalPublisher
subclass ofNode
, and therefore just the lineminimal_publisher = MinimalPublisher()
will create the ROS node with all the required behavior. One benefit is that having a class makes it easier to develop and understand more complex nodes as all logic of the node should be contained within the class. Another benefit is that in more advanced use cases (outside the scope of this manual) you could create multiple objects of the class and execute these together in a single process.
Let’s quickly analyse the code and identify dependencies_ which should have been installed on the computer before attempting to use the node. We can then define such dependencies in the package meta-data, as we’ll do in the next section.
Most of the Python client code should be somewhat familiar now.
The first lines of code after the comments import rclpy
so its Node
class can be used.
import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
The next statement imports the built-in string message type that the node uses to structure the data that it passes on the topic.
from std_msgs.msg import String
Important
Statement that import
packages from the ROS Python client library
(rclpy
) or from other ROS packages (such as std_msgs
)
represent ROS dependencies for using this node.
Recall that such dependencies
will have to be added to package.xml
, which you’ll do in the next section.
You do not have to define dependencies for standard Python packages
which are always installed with Python, such as import time
.
Next, the MinimalPublisher
class is created, which inherits from (or
is a subclass of) Node
.
class MinimalPublisher(Node):
Following is the definition of the class’s constructor.
super().__init__
calls the Node
class’s constructor and gives it
your node name, in this case "minimal_publisher"
.
create_publisher
declares that the node publishes messages of type
String
(imported from the std_msgs.msg
module), over a topic named
topic
, and that the “queue size” is 10. Queue size is a required QoS
(quality of service) setting that limits the amount of queued messages
if a subscriber is not receiving them fast enough.
Next, a timer is created with a callback to execute every 0.5 seconds.
self.i
is a counter used in the callback.
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
timer_callback
creates a message with the counter value appended, and
publishes it to the console with get_logger().info
.
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
Lastly, the main function is defined.
def main(args=None):
try:
with rclpy.init(args=args):
minimal_publisher = MinimalPublisher()
rclpy.spin(minimal_publisher)
except (KeyboardInterrupt, ExternalShutdownException):
pass
First the rclpy
library is initialized, then the node is created, and
then it “spins” the node so its callbacks are called.
Add dependencies to package.xml
Navigate one level back to the ros2_ws/src/py_pubsub
directory, where
the setup.py
, setup.cfg
, and package.xml
files have been created
for you.
Open package.xml
with your text editor.
As mentioned in the
previous tutorial, make sure to fill in the <description>
, <maintainer>
and <license>
tags:
<description>Examples of minimal publisher/subscriber using rclpy</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache-2.0</license>
After the lines above, add the identified dependencies as follows:
<exec_depend>rclpy</exec_depend>
<exec_depend>std_msgs</exec_depend>
This declares the package needs rclpy
and std_msgs
when its code is
executed.
Make sure to save the file.
Add an entry point for publisher node
Next we have to update setup.py
which is used by Python’s setuptools
.
First, we match the maintainer
,
maintainer_email
, description
and license
fields to your
package.xml
:
maintainer='YourName',
maintainer_email='you@email.com',
description='Examples of minimal publisher/subscriber using rclpy',
license='Apache-2.0',
This properly defines the meta-data of your package,
which could be inspected using the regular Python package management tools
(as opposed to the meta-data in package.xml
which is used for ROS package management tool).
Now we have to tell Python’s setuptools
what function or functions from the provided Python code should be
exposed as runnable scripts that can be started from the terminal (a.k.a. console).
In this example, we are creating a Python module py_pubsub
,
which will have a submodule publisher_member_function
(in Python, files in module directories will be submodules).
Recall that we have a main()
function in this submodule to start the node and run its main loop
that we want to expose as a talker
program by this ROS package.
To express this, add the following line within the console_scripts
brackets of the entry_points
field:
entry_points={
'console_scripts': [
'talker = py_pubsub.publisher_member_function:main',
],
},
Don’t forget to save.
Tip
The console_scripts
“entry point” tells Python’s setuptools that, after installation,
this package should provide an executable Python script talker
,
and when it is invoked it should run the main()
function
in the Python package py_pubsub.publisher_member_function
.
Check setup.cfg
The contents of the setup.cfg
file should be correctly populated
automatically, like so:
[develop]
script_dir=$base/lib/py_pubsub
[install]
install_scripts=$base/lib/py_pubsub
This is simply telling setuptools to put your executables in a lib
subdirectory of your workspace’s install space,
because ros2 run
will look for them there.
You could build your package now, source the local setup files, and run it, but let’s create the subscriber node first so you can see the full system at work.
Write the subscriber node
Return to ros2_ws/src/py_pubsub/py_pubsub
to create the next node.
Enter the following code in your terminal:
$ wget https://raw.githubusercontent.com/ros2/examples/humble/rclpy/topics/minimal_subscriber/examples_rclpy_minimal_subscriber/subscriber_member_function.py
Now the directory should have these files:
__init__.py publisher_member_function.py subscriber_member_function.py
Open the subscriber_member_function.py
with your text editor,
and you should see this:
import rclpy
from rclpy.executors import ExternalShutdownException
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):
try:
with rclpy.init(args=args):
minimal_subscriber = MinimalSubscriber()
rclpy.spin(minimal_subscriber)
except (KeyboardInterrupt, ExternalShutdownException):
pass
if __name__ == '__main__':
main()
The subscriber node’s code is nearly identical to the publisher’s.
Tip
Since this node has the same import dependencies as the publisher, there’s
nothing new to add to package.xml
. The setup.cfg
file can also
remain untouched.
The constructor creates a subscriber with the same arguments as the publisher. Recall from the topics tutorial that the topic name and message type used by the publisher and subscriber must match to allow them to communicate.
self.subscription = self.create_subscription(
String,
'topic',
self.listener_callback,
10)
The subscriber’s constructor and callback don’t include any timer definition, because it doesn’t need one. Its callback gets called as soon as it receives a message.
The callback definition simply prints an info message to the console,
along with the data it received. Recall that the publisher defines
msg.data = 'Hello World: %d' % self.i
def listener_callback(self, msg):
self.get_logger().info('I heard: "%s"' % msg.data)
The main
definition is almost exactly the same, replacing the creation
and spinning of the publisher with the subscriber.
minimal_subscriber = MinimalSubscriber()
rclpy.spin(minimal_subscriber)
8.3.3. Add another entry point for subscriber node#
Reopen setup.py
and add the entry point for the subscriber node’s main()
function
below the publisher’s entry point.
The entry_points
field should now look like this:
entry_points={
'console_scripts': [
'talker = py_pubsub.publisher_member_function:main',
'listener = py_pubsub.subscriber_member_function:main',
],
},
Make sure to save the file, and then your pub/sub system should be ready.
8.3.4. Build your new package#
You likely already have the rclpy
and std_msgs
packages installed as part of your ROS 2 system.
It’s good practice to run rosdep
in the
root of your workspace (ros2_ws
) to check for missing dependencies
before building:
$ rosdep install -i --from-path src --rosdistro humble -y
Still in the root of your workspace, ros2_ws
, build your new package:
$ colcon build --packages-select py_pubsub
Open a new terminal, navigate to ros2_ws
, and source the setup files:
$ source install/setup.bash
8.3.5. Run the nodes from your new package#
Earlier in the manual, in Section 7.2.2, you also created a talker and listener nodes, which you could start by simply starting the script from the terminal.
Now that we have defined, built, and installed a proper Python ROS 2 package,
we can finally start the nodes as any other ROS package, e.g. via ros2 run
.
First, run the talker node:
$ ros2 run py_pubsub talker
The terminal should start publishing info messages every 0.5 seconds, like so:
[INFO] [minimal_publisher]: Publishing: "Hello World: 0"
[INFO] [minimal_publisher]: Publishing: "Hello World: 1"
[INFO] [minimal_publisher]: Publishing: "Hello World: 2"
[INFO] [minimal_publisher]: Publishing: "Hello World: 3"
[INFO] [minimal_publisher]: Publishing: "Hello World: 4"
...
Open another terminal, source the setup files from inside ros2_ws
again, and then start the listener node:
$ ros2 run py_pubsub listener
The listener will start printing messages to the console, starting at whatever message count the publisher is on at that time, like so:
[INFO] [minimal_subscriber]: I heard: "Hello World: 10"
[INFO] [minimal_subscriber]: I heard: "Hello World: 11"
[INFO] [minimal_subscriber]: I heard: "Hello World: 12"
[INFO] [minimal_subscriber]: I heard: "Hello World: 13"
[INFO] [minimal_subscriber]: I heard: "Hello World: 14"
Enter Ctrl-C in each terminal to stop the nodes from spinning.
8.3.6. Summary#
You created two nodes to publish and subscribe to data over a topic. Before running them, you added their dependencies and entry points to the package configuration files.