Creating a Python Node from scratch (for ament_python)

TL;DR Making an ament_python Node

  1. Modify package.xml with any additional dependencies.

  2. Create the Node.

  3. Modify the setup.py file.

Let us add an additional Node to our ament_python package that actually uses ROS2 functionality. These are the steps that must be taken, in general, to add a new Node.

Handling dependencies (package.xml)

The package.xml was automatically generated by ros2 pkg create and holds basic information about the package.

One important role of package.xml is to declare dependencies with other ROS2 packages. It is common for new Nodes to have additional dependencies, so we will cover that here. For any ROS2 package, we must modify the package.xml to add new dependencies.

In this toy example, let us add rclpy as a dependency because it is the Python implementation of the RCL. All Nodes that use anything related to ROS2 will directly or indirectly depend on that library.

By no coincidence, the package.xml has the .xml extension, meaning that it is written in XML.

Let us add the dependency between the <license> and <test_depend> tags. This is not a strict requirement but is where it commonly is for standard packages.

~/ros2_tutorial_workspace/src/python_package_with_a_node/package.xml

 1<?xml version="1.0"?>
 2<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
 3<package format="3">
 4  <name>python_package_with_a_node</name>
 5  <version>0.0.0</version>
 6  <description>TODO: Package description</description>
 7  <maintainer email="murilomarinho@ieee.org">murilo</maintainer>
 8  <license>TODO: License declaration</license>
 9
10  <depend>rclpy</depend>
11
12  <test_depend>ament_copyright</test_depend>
13  <test_depend>ament_flake8</test_depend>
14  <test_depend>ament_pep257</test_depend>
15  <test_depend>python3-pytest</test_depend>
16
17  <export>
18    <build_type>ament_python</build_type>
19  </export>
20</package>

After you modify the workspace, build it once

After you add a new dependency to package.xml, nothing really changes in the workspace unless a new build is performed.

In addition, when programming with new dependencies, unless you rebuild the workspace, PyCharm will not recognize the libraries, and autocomplete will not work.

So,

  1. close PyCharm.

  2. Run (in the terminal you used to run PyCharm before)
    cd ~/ros2_tutorial_workspace
    colcon build
    source install/setup.bash
    

    Note

    If you don’t remember why we’re building with these commands, see Always source after you build.

  3. Re-open pycharm
    pycharm_ros2
    

Creating the Node

In the directory src/python_package_with_a_node/python_package_with_a_node, create a new file called print_forever_node.py. Copy and paste the following contents into the file.

~/ros2_tutorial_workspace/src/python_package_with_a_node/python_package_with_a_node/print_forever_node.py

 1import rclpy
 2from rclpy.node import Node
 3
 4
 5class PrintForever(Node):
 6    """A ROS2 Node that prints to the console periodically."""
 7
 8    def __init__(self):
 9        super().__init__('print_forever')
10        timer_period: float = 0.5
11        self.timer = self.create_timer(timer_period, self.timer_callback)
12        self.print_count: int = 0
13
14    def timer_callback(self):
15        """Method that is periodically called by the timer."""
16        self.get_logger().info(f'Printed {self.print_count} times.')
17        self.print_count = self.print_count + 1
18
19
20def main(args=None):
21    """
22    The main function.
23    :param args: Not used directly by the user, but used by ROS2 to configure
24    certain aspects of the Node.
25    """
26    try:
27        rclpy.init(args=args)
28
29        print_forever_node = PrintForever()
30
31        rclpy.spin(print_forever_node)
32    except KeyboardInterrupt:
33        pass
34    except Exception as e:
35        print(e)
36
37
38if __name__ == '__main__':
39    main()

By now, this should be enough for you to be able to run the node in PyCharm. You can right-click it and choose Debug print_forever_node. This will output

[INFO] [1683009340.877110693] [print_forever]: Printed 0 times.
[INFO] [1683009341.336559942] [print_forever]: Printed 1 times.
[INFO] [1683009341.836334639] [print_forever]: Printed 2 times.
[INFO] [1683009342.336555088] [print_forever]: Printed 3 times.

To finish, press the Stop button or press CTRL+F2 on PyCharm. The node will exit gracefully with

Process finished with exit code 0

Making ros2 run work

Even though you can run the new node in PyCharm, we need an additional step to make it deployable in a place where ros2 run can find it.

To do so, we modify the console_scripts key in the entry_points dictionary defined in setup.py, to have our new node, as follows

Hint

console_scripts expects a list of str in a specific format. Hence, follow the format properly and don’t forget the commas to separate elements in the list.

~/ros2_tutorial_workspace/src/python_package_with_a_node/setup.py

 1from setuptools import setup
 2
 3package_name = 'python_package_with_a_node'
 4
 5setup(
 6    name=package_name,
 7    version='0.0.0',
 8    packages=[package_name],
 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='murilo',
17    maintainer_email='murilomarinho@ieee.org',
18    description='TODO: Package description',
19    license='TODO: License declaration',
20    tests_require=['pytest'],
21    entry_points={
22        'console_scripts': [
23            'sample_python_node = python_package_with_a_node.sample_python_node:main',
24            'print_forever_node = python_package_with_a_node.print_forever_node:main'
25        ],
26    },
27)

The format is straightforward, as follows

print_forever_node

The name of the node when calling it through ros2 run.

python_package_with_a_node

The name of the package.

print_forever_node

The name of the script, without the .py extension.

main

The function, within the script, that will be called. In general, main.

Once again, we have to refresh the workspace so we run

cd ~/ros2_tutorial_workspace
colcon build
source install/setup.bash

Note

If you don’t remember why we’re building with these commands, see Always source after you build.

And, with that, we can run

ros2 run python_package_with_a_node print_forever_node

which will output, as expected

[INFO] [1683010987.130432622] [print_forever]: Printed 0 times.
[INFO] [1683010987.622780292] [print_forever]: Printed 1 times.
[INFO] [1683010988.122731296] [print_forever]: Printed 2 times.
[INFO] [1683010988.622735422] [print_forever]: Printed 3 times.

To stop, press CTRL+C on the terminal and the Node will return gracefully.