The Python Node, explained

Note

The way that a Python Node in ROS2 works, i.e. the explanation in this section, does not depend on the building with ament_python or ament_cmake.

In a strict sense, the print_forever_node.py is not a minimal Node, but it does showcase most good practices in a Node that actually does something.

The imports

import rclpy
from rclpy.node import Node

As in any Python code, we have to import the libraries that we will use and specific modules/classes within those libraries. With rclpy, there is no difference.

Making a subclass of Node

The current version of ROS2 behaves better when your custom Node is a subclass of rclpy.node.Node. That is achieved with

class PrintForever(Node):
    """A ROS2 Node that prints to the console periodically."""

    def __init__(self):
        super().__init__('print_forever')
        timer_period: float = 0.5

About inheritance in Python, you can check the official documentation on inheritance and on super().

In more advanced nodes, inheritance does not cut it, but that is an advanced topic to be covered some other time.

Use a Timer for periodic work (when using rclpy.spin())

Tips for the future you

If the code relies on rclpy.spin(), a Timer must be used for periodic work.

In its most basic usage, periodic tasks in ROS2 must be handled by a Timer.

To do so, have the node create it with the create_timer() method, as follows.

    def __init__(self):
        super().__init__('print_forever')
        timer_period: float = 0.5
        self.timer = self.create_timer(timer_period, self.timer_callback)
        self.print_count: int = 0

The method that is periodically called by the Timer is, in this case, as follows. We use self.get_logger().info() to print to the terminal periodically.

    def timer_callback(self):
        """Method that is periodically called by the timer."""
        self.get_logger().info(f'Printed {self.print_count} times.')

In ROS2, the logging methods, i.e. self.get_logger().info(), are methods of the Node itself. So, the capability to log (print to the terminal) using ROS2 Nodes is dependent on the scope in which that Node exists.

Where the ROS2 magic happens: rclpy.init() and rclpy.spin()

All the ROS2 magic happens in some sort of spin() method. It is called this way because the spin() method will constantly loop (or spin) through items of work, e.g. scheduled Timer callbacks. All the items of work will only be effectively executed when an executor runs through it. For simple Nodes, such as the one in this example, the global executor is implicitly used. You can read a bit more about that here.

Anyhow, the point is that nothing related to ROS2 will happen unless the two following methods are called. First, rclpy.init() is going to initialize a bunch of ROS2 elements behind the curtains, whereas rclpy.spin() will block the program and, well, spin through Timer callbacks forever. There are alternative ways to spin(), but we will not discuss them right now.

def main(args=None):
    """
    The main function.
    :param args: Not used directly by the user, but used by ROS2 to configure
    certain aspects of the Node.
    """
    try:
        rclpy.init(args=args)

        print_forever_node = PrintForever()

        rclpy.spin(print_forever_node)
    except KeyboardInterrupt:
        pass
    except Exception as e:
        print(e)

Have a try-catch block for KeyboardInterrupt

In the current version of the official ROS2 examples, for reasons beyond my comprehension, this step is not followed.

However, when running Nodes either in the terminal or in PyCharm, catching a KeyboardInterrupt is the only reliable way to finish the Nodes cleanly. A KeyboardInterrupt is emitted at a terminal by pressing CTRL+C, whereas it is emitted by PyCharm when pressing Stop.

That is particularly important when real robots need to be gracefully shut down (otherwise they might inadvertently start the evil robot uprising), but it also looks unprofessional when all your Nodes return with an ugly stack trace.

def main(args=None):
    """
    The main function.
    :param args: Not used directly by the user, but used by ROS2 to configure
    certain aspects of the Node.
    """
    try:
        rclpy.init(args=args)

        print_forever_node = PrintForever()

        rclpy.spin(print_forever_node)
    except KeyboardInterrupt:
        pass
    except Exception as e:
        print(e)

Document your code with Docstrings

As simple as a code might look for you right now, it needs to be documented for anyone you work with, including the future you. In a few weeks/months/years time, the BeStNoDeYouEvErWrote (TM) might be indistinguishable from Yautja Language.

Add as much description as possible to classes and methods, using the Docstring Convention.

Example of a class:

class PrintForever(Node):
    """A ROS2 Node that prints to the console periodically."""

Example of a method:

    def timer_callback(self):
        """Method that is periodically called by the timer."""