Migrating a Python Package Example ================================== This guide shows how to migrate an example Python package from ROS 1 to ROS 2. .. contents:: Table of Contents :depth: 2 :local: Prerequisites ------------- You need a working ROS 2 installation, such as :doc:`ROS {DISTRO} <../../Installation>`. The ROS 1 code -------------- You won't be using `catkin `__ in this guide, so you don't need a working ROS 1 installation. You are going to use ROS 2's build tool `Colcon `__ instead. This section gives you the code for a ROS 1 Python package. The package is called ``talker_py``, and it has one node called ``talker_py_node``. To make it easier to run Colcon later, these instructions make you create the package inside a `Colcon workspace `__, First, create a folder at ``~/ros2_talker_py`` to be the root of the Colcon workspace. .. tabs:: .. group-tab:: Linux .. code-block:: bash mkdir -p ~/ros2_talker_py/src .. group-tab:: macOS .. code-block:: bash mkdir -p ~/ros2_talker_py/src .. group-tab:: Windows .. code-block:: bash md \ros2_talker_py\src Next, create the files for the ROS 1 package. .. tabs:: .. group-tab:: Linux .. code-block:: bash cd ~/ros2_talker_py mkdir -p src/talker_py/src/talker_py mkdir -p src/talker_py/scripts touch src/talker_py/package.xml touch src/talker_py/CMakeLists.txt touch src/talker_py/src/talker_py/__init__.py touch src/talker_py/scripts/talker_py_node touch src/talker_py/setup.py .. group-tab:: macOS .. code-block:: bash cd ~/ros2_talker_py mkdir -p src/talker_py/src/talker_py mkdir -p src/talker_py/scripts touch src/talker_py/package.xml touch src/talker_py/CMakeLists.txt touch src/talker_py/src/talker_py/__init__.py touch src/talker_py/scripts/talker_py_node touch src/talker_py/setup.py .. group-tab:: Windows .. code-block:: bash cd \ros2_talker_py md src\talker_py\src\talker_py md src\talker_py\scripts type nul > src\talker_py\package.xml type nul > src\talker_py\CMakeLists.txt type nul > src\talker_py\src\talker_py\__init__.py type nul > src\talker_py\scripts/talker_py_node type nul > src\talker_py\setup.py Put the following content into each file. ``src/talker_py/package.xml``: .. code-block:: xml talker_py 1.0.0 The talker_py package Brian Gerkey BSD catkin rospy std_msgs ``src/talker_py/CMakeLists.txt``: .. code-block:: cmake cmake_minimum_required(VERSION 3.0.2) project(talker_py) find_package(catkin REQUIRED) catkin_python_setup() catkin_package() catkin_install_python(PROGRAMS scripts/talker_py_node DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} ) ``src/talker/src/talker_py/__init__.py``: .. code-block:: Python import rospy from std_msgs.msg import String def main(): rospy.init_node('talker') pub = rospy.Publisher('chatter', String, queue_size=10) rate = rospy.Rate(10) # 10hz while not rospy.is_shutdown(): hello_str = "hello world %s" % rospy.get_time() rospy.loginfo(hello_str) pub.publish(hello_str) rate.sleep() ``src/talker_py/scripts/talker_py_node``: .. code-block:: Python #!/usr/bin/env python import talker_py if __name__ == '__main__': talker_py.main() ``src/talker_py/setup.py``: .. code-block:: Python from setuptools import setup from catkin_pkg.python_setup import generate_distutils_setup setup_args = generate_distutils_setup( packages=['talker_py'], package_dir={'': 'src'} ) setup(**setup_args) This is the complete ROS 1 Python package. Migrate the ``package.xml`` --------------------------- When migrating packages to ROS 2, migrate the build system files first so that you can check your work by building and running code as you go. Always start by migrating your ``package.xml``. First, ROS 2 does not use ``catkin``. Delete the ```` on it. .. code-block:: catkin Next, ROS 2 uses ``rclpy`` instead of ``rospy``. Delete the dependency on ``rospy``. .. code-block:: rospy Replace it with a new dependency on ``rclpy``. .. code-block:: xml rclpy Add an ```` section to tell ROS 2's build tool `Colcon `__ that this is an ``ament_python`` package instead of a ``catkin`` package. .. code-block:: xml ament_python Your ``package.xml`` is fully migrated. It should now look like this: .. code-block:: xml talker_py 1.0.0 The talker_py package Brian Gerkey BSD rclpy std_msgs ament_python Delete the ``CMakeLists.txt`` ----------------------------- Python packages in ROS 2 do not use CMake, so delete the ``CMakeLists.txt``. Migrate the ``setup.py`` ------------------------ The arguments to ``setup()`` in the ``setup.py`` can no longer be automatically generated with ``catkin_pkg``. You must pass these arguments manually, which means there will be some duplication with your ``package.xml``. Start by deleting the import from ``catkin_pkg``. .. code-block:: # Delete this from catkin_pkg.python_setup import generate_distutils_setup Move all arguments given to ``generate_distutils_setup()`` to the call to ``setup()``, and then add the ``install_requires`` and ``zip_safe`` arguments. Your call to ``setup()`` should look like this: .. code-block:: Python setup( packages=['talker_py'], package_dir={'': 'src'}, install_requires=['setuptools'], zip_safe=True, ) Delete the call to ``generate_distutils_setup()``. .. code-block:: # Delete this setup_args = generate_distutils_setup( packages=['talker_py'], package_dir={'': 'src'} ) The call to ``setup()`` needs some `additional metadata `__ copied from the ``package.xml``: * package name via the ``name`` argument * package version via the ``version`` argument * maintainer via the ``maintainer`` and ``maintainer_email`` arguments * description via the ``description`` argument * license via the ``license`` argument The package name will be used multiple times. Create a variable called ``package_name`` above the call to ``setup()``. .. code-block:: Python package_name = 'talker_py' Copy all of the remaining information into the arguments of ``setup()`` in ``setup.py``. Your call to ``setup()`` should look like this: .. code-block:: Python setup( name=package_name, version='1.0.0', install_requires=['setuptools'], zip_safe=True, packages=['talker_py'], package_dir={'': 'src'}, maintainer='Brian Gerkey', maintainer_email='gerkey@example.com', description='The talker_py package', license='BSD', ) ROS 2 packages must install two data files: * a ``package.xml`` * a package marker file Your package already has a ``package.xml``. It describes your package's dependencies. A package marker file tells tools like ``ros2 run`` where to find your package. Create a directory next to the ``package.xml`` called ``resource``. Create an empty file in the ``resource`` directory with the same name as the package. .. tabs:: .. group-tab:: Linux .. code-block:: bash mkdir resource touch resource/talker_py .. group-tab:: macOS .. code-block:: bash mkdir resource touch resource/talker_py .. group-tab:: Windows .. code-block:: bash md resource type nul > resource\talker_py The ``setup()`` call in ``setup.py`` must tell ``setuptools`` how to install these files. Add the following ``data_files`` argument to the call to ``setup()``. .. code-block:: Python data_files=[ ('share/ament_index/resource_index/packages', ['resource/' + package_name]), ('share/' + package_name, ['package.xml']), ], Your ``setup.py`` is almost complete. Migrate Python scripts and create ``setup.cfg`` ----------------------------------------------- ROS 2 Python packages uses ``console_scripts`` `entry points `__ to install Python scripts as executables. The `configuration file `__ ``setup.cfg`` tells ``setuptools`` to install those executables in a package specific directory so that tools like ``ros2 run`` can find them. Create a ``setup.cfg`` file next to the ``package.xml``. .. tabs:: .. group-tab:: Linux .. code-block:: bash touch setup.cfg .. group-tab:: macOS .. code-block:: bash touch setup.cfg .. group-tab:: Windows .. code-block:: bash type nul > touch setup.cfg Put the following content into it: .. code-block:: [develop] script_dir=$base/lib/talker_py [install] install_scripts=$base/lib/talker_py You'll need to use the ``console_scripts`` entry point to define the executables to be installed. Each entry has the format ``executable_name = some.module:function``. The first part specifies the name of the executable to create. The second part specifies the function that should be run when the executable starts. This package needs to create an executable called ``talker_py_node``, and the executable needs to call the function ``main`` in the ``talker_py`` module. Add the following entry point specification as another argument to ``setup()`` in your ``setup.py``. .. code-block:: Python entry_points={ 'console_scripts': [ 'talker_py_node = talker_py:main', ], }, The ``talker_py_node`` file is no longer necessary. Delete the file ``talker_py_node`` and delete the ``scripts/`` directory. .. tabs:: .. group-tab:: Linux .. code-block:: bash rm scripts/talker_py_node rmdir scripts .. group-tab:: macOS .. code-block:: bash rm scripts/talker_py_node rmdir scripts .. group-tab:: Windows .. code-block:: bash del scripts/talker_py_node rd scripts The addition of ``console_scripts`` is the last change to your ``setup.py``. Your final ``setup.py`` should look like this: .. code-block:: Python from setuptools import setup package_name = 'talker_py' setup( name=package_name, version='1.0.0', packages=['talker_py'], package_dir={'': 'src'}, install_requires=['setuptools'], zip_safe=True, data_files=[ ('share/ament_index/resource_index/packages', ['resource/' + package_name]), ('share/' + package_name, ['package.xml']), ], maintainer='Brian Gerkey', maintainer_email='gerkey@example.com', description='The talker_py package', license='BSD', entry_points={ 'console_scripts': [ 'talker_py_node = talker_py:main', ], }, ) Migrate Python code in ``src/talker_py/__init__.py`` ---------------------------------------------------- ROS 2 changed a lot of the best practices for Python code. Start by migrating the code as-is. It will be easier to refactor code later after you have something working. Use ``rclpy`` instead of ``rospy`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ROS 2 packages use `rclpy `__ instead of ``rospy``. You must do two things to use ``rclpy``: 1. Import ``rclpy`` 2. Initialize ``rclpy`` Remove the statement that imports ``rospy``. .. code-block:: Python # Remove this import rospy Rplace it with a statement that imports ``rclpy``. .. code-block:: Python import rclpy Add a call to ``rclpy.init()`` as the very first statement in the ``main()`` function. .. code-block:: Python def main(): # Add this line rclpy.init() Execute callbacks in the background ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Both ROS 1 and ROS 2 use `callbacks `__. In ROS 1, callbacks are always executed in background threads, and users are free to block the main thread with calls like ``rate.sleep()``. In ROS 2, ``rclpy`` uses :doc:`Executors <../../Concepts/Intermediate/About-Executors>` to give users more control over where callbacks are called. When porting code that uses blocking calls like ``rate.sleep()``, you must make sure that those calls won't interfere with the executor. One way to do this is to create a dedicated thread for the executor. First, add these two import statements. .. code-block:: Python import threading from rclpy.executors import ExternalShutdownException Next, add top-level function called ``spin_in_background()``. This function asks the default executor to execute callbacks until something shuts it down. .. code-block:: Python def spin_in_background(): executor = rclpy.get_global_executor() try: executor.spin() except ExternalShutdownException: pass Add the following code in the ``main()`` function just after the call to ``rclpy.init()`` to start a thread that calls ``spin_in_background()``. .. code-block:: Python # In rospy callbacks are always called in background threads. # Spin the executor in another thread for similar behavior in ROS 2. t = threading.Thread(target=spin_in_background) t.start() Finally, join the thread when the program ends by putting this statement at the bottom of the ``main()`` function. .. code-block:: Python t.join() Create a node ~~~~~~~~~~~~~ In ROS 1, Python scripts can only create a single node per process, and the API ``init_node()`` creates it. In ROS 2, a single Python script may create multiple nodes, and the API to create a node is named ``create_node``. Remove the call to ``rospy.init_node()``: .. code-block:: rospy.init_node('talker') Add a new call to ``rclpy.create_node()`` and store the result in a variable named ``node``: .. code-block:: Python node = rclpy.create_node('talker') We must tell the executor about this node. Add the following line just below the creation of the node: .. code-block:: Python rclpy.get_global_executor().add_node(node) Create a publisher ~~~~~~~~~~~~~~~~~~ In ROS 1, users create publishers by instantiating the ``Publisher`` class. In ROS 2, users create publishers through a node's ``create_publisher()`` API. The ``create_publisher()`` API has an unfortunate difference with ROS 1: the topic name and topic type arguments are swapped. Remove the creation of the ``rospy.Publisher`` instance. .. code-block:: pub = rospy.Publisher('chatter', String, queue_size=10) Replace it with a call to ``node.create_publisher()``. .. code-block:: Python pub = node.create_publisher(String, 'chatter', 10) Create a rate ~~~~~~~~~~~~~ In ROS 1, users create ``Rate`` instances directly, while in ROS 2 users create them through a node's ``create_rate()`` API. Remove the creation of the ``rospy.Rate`` instance. .. code-block:: rate = rospy.Rate(10) # 10hz Replace it with a call to ``node.create_rate()``. .. code-block:: Python rate = node.create_rate(10) # 10hz Loop on ``rclpy.ok()`` ~~~~~~~~~~~~~~~~~~~~~~ In ROS 1, the ``rospy.is_shutdown()`` API indicates if the process has been asked to shutdown. In ROS 2, the ``rclpy.ok()`` API does this. Remove the statement ``not rospy.is_shutdown()`` .. code-block:: while not rospy.is_shutdown(): Replace it with a call to ``rclpy.ok()``. .. code-block:: Python while rclpy.ok(): Create a ``String`` message with the current time ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You must make a few changes to this line .. code-block:: hello_str = "hello world %s" % rospy.get_time() In ROS 2 you: * Must get the time from a ``Clock`` instance * Should format the ``str`` data using `f-strings `__ since `% is discouraged in active Python versions `__ * Must instantiate a ``std_msgs.msg.String`` instance Start with getting the time. ROS 2 nodes have a ``Clock`` instance. Replace the call to ``rospy.get_time()`` with ``node.get_clock().now()`` to get the current time from the node's clock. Next, replace the use of ``%`` with an f-string: ``f'hello world {node.get_clock().now()}'``. Finally, instantiate a ``std_msgs.msg.String()`` instance and assign the above to the ``data`` attribute of that instance. Your final code should look like this: .. code-block:: Python hello_str = String() hello_str.data = f'hello world {node.get_clock().now()}' Log an informational message ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In ROS 2, you must send log messages through a ``Logger`` instance, and the node has one. Remove the call to ``rospy.loginfo()``. .. code-block:: rospy.loginfo(hello_str) Replace it with a call to ``info()`` on the node's ``Logger`` instance. .. code-block:: Python node.get_logger().info(hello_str.data) This is the last change to ``src/talker_py/__init__.py``. Your file should look like the following: .. code-block:: Python import threading import rclpy from rclpy.executors import ExternalShutdownException from std_msgs.msg import String def spin_in_background(): executor = rclpy.get_global_executor() try: executor.spin() except ExternalShutdownException: pass def main(): rclpy.init() # In rospy callbacks are always called in background threads. # Spin the executor in another thread for similar behavior in ROS 2. t = threading.Thread(target=spin_in_background) t.start() node = rclpy.create_node('talker') rclpy.get_global_executor().add_node(node) pub = node.create_publisher(String, 'chatter', 10) rate = node.create_rate(10) # 10hz while rclpy.ok(): hello_str = String() hello_str.data = f'hello world {node.get_clock().now()}' node.get_logger().info(hello_str.data) pub.publish(hello_str) rate.sleep() t.join() Build and run ``talker_py_node`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Create three terminals: 1. One to build ``talker_py`` 2. One to run ``talker_py_node`` 3. One to echo the message published by ``talker_py_node`` Build the workspace in the first terminal. .. tabs:: .. group-tab:: Linux .. code-block:: bash cd ~/ros2_talker_py . /opt/ros/{DISTRO}/setup.bash colcon build .. group-tab:: macOS .. code-block:: bash cd ~/ros2_talker_py . /opt/ros/{DISTRO}/setup.bash colcon build .. group-tab:: Windows .. code-block:: bash cd \ros2_talker_py call C:\dev\ros2\local_setup.bat colcon build Source your workspace in the second terminal, and run the ``talker_py_node``. .. tabs:: .. group-tab:: Linux .. code-block:: bash cd ~/ros2_talker_py . install/setup.bash ros2 run talker_py talker_py_node .. group-tab:: macOS .. code-block:: bash cd ~/ros2_talker_py . install/setup.bash ros2 run talker_py talker_py_node .. group-tab:: Windows .. code-block:: bash cd \ros2_talker_py call install\setup.bat ros2 run talker_py talker_py_node Echo the message published by the node in the third terminal: .. tabs:: .. group-tab:: Linux .. code-block:: bash . /opt/ros/{DISTRO}/setup.bash ros2 topic echo /chatter .. group-tab:: macOS .. code-block:: bash . /opt/ros/{DISTRO}/setup.bash ros2 topic echo /chatter .. group-tab:: Windows .. code-block:: bash call C:\dev\ros2\local_setup.bat ros2 topic echo /chatter You should see messages with the current time being published in the second terminal, and those same messages received in the third. Refactor code to use ROS 2 convensions -------------------------------------- You have successfully migrated a ROS 1 Python package to ROS 2! Now that you have something working, consider refactoring it to align better with ROS 2's Python APIs. Follow these two principles. * Create a class that inherits from ``Node``. * Do all work in callbacks, and never block those callbacks. For example, create a ``Talker`` class that inherits from ``Node``. As for doing work in callbacks, use a ``Timer`` with a callback instead of ``rate.sleep()``. Make the timer callback publish the message and return. Make ``main()`` create a ``Talker`` instance rather than using ``rclpy.create_node()``, and give the executor the main thread to execute in. Your refactored code might look like this: .. code-block:: Python import rclpy from rclpy.node import Node from rclpy.executors import ExternalShutdownException from std_msgs.msg import String class Talker(Node): def __init__(self, **kwargs): super().__init__('talker', **kwargs) self._pub = self.create_publisher(String, 'chatter', 10) self._timer = self.create_timer(1 / 10, self.do_publish) def do_publish(self): hello_str = String() hello_str.data = f'hello world {self.get_clock().now()}' self.get_logger().info(hello_str.data) self._pub.publish(hello_str) def main(): rclpy.init() try: rclpy.spin(Talker()) except (ExternalShutdownException, KeyboardInterrupt): pass finally: rclpy.try_shutdown() Conclusion ---------- You have learned how to migrate an example Python ROS 1 package to ROS 2. From now on, refer to the :doc:`Migrating Python Packages reference page <./Migrating-Python-Packages>` as you migrate your own Python packages.