7.2. rclpy, the Python client library#

In this section you will work through the basics of using the Python ROS client library, rclpy. Later in Section 7.4 you will do similar things with the C++ client library.

Tip

Is your Python a bit rusty?

You can go over the free online Python for Engineers teachbook by TU Delft, which goes over the core concepts, and some frequently used libraries such as numpy.

Note

In this section you will build simple ROS nodes in Python without creating a ROS package.

In Section 8.3 you will learn how to properly setup a Python ROS package and use colcon to build your ROS projects.

7.2.1. Publishing and Subscribing to Topics in Python#

Creating a Python program that sets up a ROS node to publish data only requires a few lines of code. Here is a complete “talker” Python program that publishes std_msgs/msg/String messages at a regular interval:

import rclpy
from std_msgs.msg import String

rclpy.init()
node = rclpy.create_node('my_publisher')
pub = node.create_publisher(String, 'chatter', 10)

msg = String()
def cb_timer():
    now = node.get_clock().now()
    now_in_seconds = now.nanoseconds / 1e9
    msg.data = f'Hello World, the time is: {now_in_seconds:.2f}'
    
    print(f'Publishing: "{msg.data}"')
    pub.publish(msg)

node.create_timer(0.5, cb_timer) # trigger callback every 0.5 seconds
rclpy.spin(node)

Note

Don’t worry if most of these functions looks unfamiliar, we will go over the code in more detail in the subsection below. But before we do that, let’s verify that the code actually works.

To try it out yourself, copy the code block above into a file, call it talker.py, then feed it to your Python3 interpreter:

$ python3 talker.py

You should see output similar to this, but with different time stamps:

Publishing: "Hello World, the time is: 1726781990.58"
Publishing: "Hello World, the time is: 1726781991.08"
Publishing: "Hello World, the time is: 1726781991.58"

Tip

If you get Python error that the rclpy module cannot be found, then probably your ROS environment is not setup.

So, also for these programming exercises make sure that you have sourced the ROS setup file in every shell. E.g., source /opt/ros/humble/setup.bash.

It prints to console, but is the data going anywhere? We can check our work using the ros2 topic tool that was introduced earlier. In another shell (leave your talker running), run:

$ ros2 topic echo chatter

You should see the following, though the numbers will vary depending on timing between the two commands:

data: 'Hello World, the time is: 1726782155.84'
---
data: 'Hello World, the time is: 1726782156.34'
---
data: 'Hello World, the time is: 1726782156.84'

So we have a working talker!

Let’s now create our own “listener” to use in place of ros2 topic echo. Here is a complete Python program that subscribes to std_msgs/msg/String messages and prints them to console:

import rclpy
from std_msgs.msg import String

def cb_subscriber(msg):
    print(f'I heard: "{msg.data}"')

rclpy.init()
node = rclpy.create_node('my_subscriber')
sub = node.create_subscription(String, 'chatter', cb_subscriber, 10)
rclpy.spin(node)

Try it out yourself. Copy the code block above into another file and call it listener.py. With your talker still running in one shell, start up your listener in another shell:

$ python3 listener.py

You should see (again, numbers will vary depending on timing):

I heard: "Hello World, the time is: 1726782245.54"
I heard: "Hello World, the time is: 1726782246.04"
I heard: "Hello World, the time is: 1726782246.54"

Exercise 7.1

Each of the two Python programs you wrote creates a ROS node each time you start it. This means you can inspect the nodes and the graph using the CLI tools you are already familiar with, such as ros2 node list, ros2 node info, and rqt_graph.

Can you use these tools to figure out what the names of the nodes are? Look at the Python code, where were these names configured there?

7.2.2. Digging into the Python Code#

Now that we know these programs work, we can dig into their code. Both programs start with the same preamble:

import rclpy
from std_msgs.msg import String

We need to import the rclpy client library, which gives us much of what we need to write ROS applications in Python. But we also need to specifically import the ROS message type(s) that we will use. In this case we are using the simple std_msgs/msg/String ROS message.

Note

Each ROS message type is made available as a Python class in a Python package following the same namespace convention as that of the ROS message. As you can see, we simply “replace” the / in the full ROS message type name by a . to find the package and class name in Python.

If we want to use a sensor_msgs/msg/Image ROS message to generate image data, we can use:

import sensor_msgs.msg
msg = sensor_msgs.msg.Image()

or simply directly import the class from its namespace:

from sensor_msgs.msg import Image
msg = Image()

7.2.2.1. Get parameter values & configure#

After the imports, both programs perform common ROS initialization:

rclpy.init()
node = rclpy.create_node('my_node_name')

This first line initializes the rclpy library. This second line then uses the initialized library to let it create a Node object, giving it a name. Subsequently we will operate on that Node object.

At this stage, we could use the Node object to read a start up ROS parameter value, for example node.get_parameter('my_setting').value, and use that to configure our node’s behavior. In these simple examples we don’t do that.

7.2.2.2. Set up ROS interfaces#

In the talker we then use the Node object to create a Publisher object:

pub = node.create_publisher(String, 'chatter', 10)

This line declares the type of data the node will publish (std_msgs/msg/String), the name of the topic on which it will publish (chatter), and the maximum number of outbound messages to locally queue up (10). That last argument comes into play when we are publishing faster than subscribers are consuming the data.

The equivalent step in the listener is to create a Subscription object:

sub = node.create_subscription(String, 'chatter', cb_subscriber, 10)

The type (String) and topic name (chatter) arguments have the same meaning as in the create_publisher() call and the final argument (10) is setting an analogous maximum queue size for inbound messages.

The key difference between create_publisher and create_subscription is the that the latter has an extra argument, cb_subscriber, which is the name of this callback function that we also defined in the listener.

def cb_subscriber(msg):
    print(f'I heard: "{msg.data}"')

So, this function will be called whenever the listener receives a message, and the received message will be passed to the callback as its argument, here called msg. In this case we simply print the received msg’s content to console.

The talker also has a callback function, cb_timer, but its callback is given to the Node object to create a new “timer” using create_timer(). ROS will regularly execute the timer’s callback when ROS spins the main loop.

node.create_timer(0.5, cb_timer) # trigger callback every 0.5 seconds

The first argument of this function is the timer’s target interval time (in seconds), which tells the timer how much time should pass between subsequent calls. In contrast to the subscriber’s callback, the timer callback does not have any argument because there is no incoming message to process:

def cb_timer():
    ...

We will discuss the content of the talker’s callback in more detail in a moment.

7.2.2.3. Spin the main loop#

After the ROS interfaces have been created and their callbacks defined, the rest of the talker and listener is just one line:

rclpy.spin(node)

This call hands control over to rclpy to spin a main loop. During this main loop, ROS will wait for new messages to arrive on subscribed topics (in case of the listener), and/or for a timer to fire (in case of the talker), and invoke the corresponding callbacks.

So while this is the final line, the program will probably spend most of its execution time in this main loop.

7.2.2.4. Understanding the talker callback#

Now let’s dig a bit deeper into the callback of the talker, which is repeated here:

msg = String()
def cb_timer():
    now = node.get_clock().now()
    now_in_seconds = now.nanoseconds / 1e9
    msg.data = f'Hello World, the time is: {now_in_seconds:.2f}'
    
    print(f'Publishing: "{msg.data}"')
    pub.publish(msg)

Here we first created a message object msg of the String class, which represents the std_msgs/msg/String ROS message type.

Tip

We could also have created the msg object inside the callback, but generally it is better to avoid as much unnecessary computation inside a callback as possible. Creating and deleting Python objects will sometimes trigger Python to perform memory management operations (such as its “garbage collection”), which could introduce irregular lag. This would not be much of a problem with such a simple example, but it is good practice to move whatever we safely can out of the callback.

Each time the callback is called, it will do a few things:

  1. Lookup the current time via the Node object, and convert it to a Unix timestamp in seconds.

  2. Update the message object’s content.

  3. Publish the message object via the publisher pub.

In step 1 we check the clock of the Node and use it to determine the “current time” as a UNIX time in seconds:

    now = node.get_clock().now()
    now_in_seconds = now.nanoseconds / 1e9

The object that node.get_clock().now() returns contains the UNIX time in nanoseconds, so we have to divide it by \(1 \times 10^9\) such that our talker can report the time in seconds.

Important

In regular non-ROS python code, you would typically get the current time via one of Python’s builtin libraries, such asdatetime.now(). That would give you the current wall clock (a.k.a. real) time as your computer’s regular clock tracks the date and time of day.

However, in ROS you sometimes want your code to follow a different clock, as we covered in the section on ROS Time vs Wall Clock time. The ROS node knows which clock to follow by automatically checking for the use_sim_time parameter during its creation with rclpy.create_node(). Therefore, we should always ask the Node object for the current time, and not Python’s standard libraries.

Tip

Another common operation is to use the node’s clock to set the stamp field of message’s header to the current time. The reported time can be converted to the Python class representing ROS’s builtin_interfaces/Time message type using to_msg(), as shown in this example:

from sensor_msgs.msg import Image
msg = Image()
msg.header.stamp = node.get_clock().now().to_msg()

In step 2 we set the String message’s only field, data, with a regular Python string.

    msg.data = f'Hello World, the time is: {now_in_seconds:.2f}'

In this example we used Python’s F-string syntax to define a string containing the value of floating point value of now_in_seconds up to two decimal places.

By the way, we can know that the msg object of Python class std_msgs.msg.String has a data field by studying the definition of the std_msgs/msg/String ROS message type. This interface definition only contains a data field of basic data type string, which means we can directly assign a regular Python string object to it. Other ROS message types would of course define other fields of different types, so always check a type’s definition when you are trying to define a message object’s content.

Tip

Remember from Section 5.7 how we can lookup a message’s definition to see which fields it has:

$ ros2 interface show std_msgs/msg/String
# This was originally provided as an example message.
# It is deprecated as of Foxy
# It is recommended to create your own semantically meaningful message.
# However if you would like to continue using this please use the equivalent in example_msgs.

string data

Finally step 3 actually publishes the message, and thereby completing the callback:

    pub.publish(msg)

7.2.3. Writing your own main loop (an “anti-pattern”)#

The talker example could easily have been implemented without a callback by implementing a main loop “manually” and therefore also avoid the use of rclpy.spin(), like this:

from time import sleep
import rclpy
from std_msgs.msg import String

rclpy.init()
node = rclpy.create_node('my_publisher')
pub = node.create_publisher(String, 'chatter', 10)
msg = String()

while rclpy.ok():
    now = node.get_clock().now()
    now_in_seconds = now.nanoseconds / 1e9
    msg.data = f'Hello World, the time is: {now_in_seconds:.2f}'

    print(f'Publishing: "{msg.data}"')
    pub.publish(msg)
    sleep(0.5) # wait for 0.5 seconds before the next iteration

This main loop is implemented using a regular Python while loop. The while condition checks rclpy.ok() which only returns False if the ROS system would tell the node to stop its execution and shutdown. This is pretty similar to how rclpy.spin() would control its main loop.

In this simple main loop we set the message data and invoke the Publisher to publish the message, just as we did in our callback implementation.

To set a rough interval, we briefly pause execution for half a second at the end of the iteration which can be done via the standard Python function sleep(). This prevents our main loop from “spinning” too quickly, spamming ROS messages and requiring a lot of unnecessary CPU time.

A node without rclpy.spin() will not respond to any callbacks, which is mostly ok for this simple talker example because it does not explicitly subscribe to any topics. However, the lack of rclpy.spin() also prevents our node from processing messages on topics that it might need to listen to implicitly! For example, recall from the ROS Time explanation that we can set a node’s use_sim_time ROS parameter to make the node automatically listen to /clock. If we implement our own main loop without rclpy.spin(), our node can still subscribe to /clock but it would not be able to respond to clock messages to adjust its internal ROS Time tracking.

Warning

Writing your own loop with while rclpy.ok(): ... is a ROS “anti-pattern”. It might work, and there could be some corner cases where it might be useful, but in most practical applications it is better to let ROS handle the main loop via rclpy.spin()! As we have seen in our original talker example, setting up a timer with a callback is pretty trivial in any case, and it makes our ROS node more reliable.

Exercise 7.2

Make sure you save both talker implementations as two separate Python scripts, e.g., the original talker using rclpy.spin() as talker.py, and the talker using while rclpy.ok(): as talker_whileok.py.

In a first terminal replay any rosbag with its --clock parameter set, as was explained in the ROS Time section. Now try to start both talkers using use_sim_time to make them follow the ROS Time from the rosbag. So, in a second terminal use:

python3 talker.py --ros-args -p use_sim_time:=true

And in a third terminal use:

python3 talker_whileok.py --ros-args -p use_sim_time:=true

What do you see in the reported time stamps in the terminal output of each talker? If you convert the UNIX Time stamps, what time and date do the clocks of these nodes represent?

7.2.4. Where to go from here#

That was a very brief introduction and we only covered topics, not services, actions, parameters, or the many other facets of ROS.

As we stated in the beginning of this section, we have here presented the simplest, shortest example ROS programs that we could come up with. Such programs are easy to understand and learn from, as they do not have unnecessary structure or decoration. But in exchange such programs are not easily extensible, composable, or maintainable.

The techniques that we used in the example code in this section are useful for prototyping and experimentation (an important aspect of any good robotics project!), but we do not recommend them for serious work. The best way to learn about ROS concepts, patterns, and conventions is to start reading and writing ROS code.

Tip

You have reached the end of the content for Lab Assignment 2 of Robot Software Practicals! If you worked through the whole manual, you should be ready to work on the hand-in assignment for Lab 2 with your lab partner.

Later in Lab Assignment 3 of RSP we will dive deeper into developing ROS code, and cover more advanced topics such as:

  • the C++ ROS client library

  • organizing your code into packages

  • organizing your packages into a workspace

  • managing dependencies among packages

  • using the colcon tool to build code in multiple packages in dependency order

  • using the client libraries’ console-logging routines for output to screen and elsewhere

These techniques will serve you well when you start building your own ROS applications, especially when you want to share your code with others, whether on your team or out in the world.