Creating a Python Node from scratch (for ament_python)

Creating a Python Node from scratch (for ament_python)#

See also

The official API documentation: https://docs.ros.org/en/jazzy/p/rclpy/rclpy.html

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.

File structure#

In this section, we will be modifying or creating the following files.

python_package_with_a_node
|-- package.xml
|-- python_package_with_a_node
|   |-- __init__.py
|   |-- print_forever_node.py
|   `-- sample_python_node.py
|-- resource
|   `-- python_package_with_a_node
|-- setup.cfg
|-- setup.py
`-- test
    |-- test_copyright.py
    |-- test_flake8.py
    `-- test_pep257.py

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.

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>

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.

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()

Making ros2 run work#

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 find_packages, setup
 2
 3package_name = 'python_package_with_a_node'
 4
 5setup(
 6    name=package_name,
 7    version='0.0.0',
 8    packages=find_packages(exclude=['test']),
 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.

Build and source#

We can build and source the workspace with the following command.

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

Note

For additional explanation and troubleshooting tips, see Always source after you build.

Warning

colcon will not work properly if your terminal has an active venv.

Test#

And, with that, we can run

ros2 run python_package_with_a_node print_forever_node

which will output, as expected

[INFO] [1753518652.646459087] [print_forever]: Printed 0 times.
[INFO] [1753518653.131078795] [print_forever]: Printed 1 times.
[INFO] [1753518653.632436004] [print_forever]: Printed 2 times.
[INFO] [1753518654.132090212] [print_forever]: Printed 3 times.
[INFO] [1753518654.630924338] [print_forever]: Printed 4 times.

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