Migrating a C++ Package Example =============================== .. contents:: Table of Contents :depth: 2 :local: This example shows how to migrate an example C++ package from ROS 1 to ROS 2. Prerequisites ------------- You need a working ROS 2 installation, such as :doc:`ROS {DISTRO} <../../Installation>`. The ROS 1 code -------------- Say you have a ROS 1 package called ``talker`` that uses ``roscpp`` in one node, called ``talker``. This package is in a catkin workspace, located at ``~/ros1_talker``. Your ROS 1 workspace has the following directory layout: .. code-block:: bash $ cd ~/ros1_talker $ find . . ./src ./src/talker ./src/talker/package.xml ./src/talker/CMakeLists.txt ./src/talker/talker.cpp The files have the following content: ``src/talker/package.xml``: .. code-block:: xml talker 0.0.0 talker Brian Gerkey Apache-2.0 catkin roscpp std_msgs ``src/talker/CMakeLists.txt``: .. code-block:: cmake cmake_minimum_required(VERSION 2.8.3) project(talker) find_package(catkin REQUIRED COMPONENTS roscpp std_msgs) catkin_package() include_directories(${catkin_INCLUDE_DIRS}) add_executable(talker talker.cpp) target_link_libraries(talker ${catkin_LIBRARIES}) install(TARGETS talker RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}) ``src/talker/talker.cpp``: .. code-block:: cpp #include #include "ros/ros.h" #include "std_msgs/String.h" int main(int argc, char **argv) { ros::init(argc, argv, "talker"); ros::NodeHandle n; ros::Publisher chatter_pub = n.advertise("chatter", 1000); ros::Rate loop_rate(10); int count = 0; std_msgs::String msg; while (ros::ok()) { std::stringstream ss; ss << "hello world " << count++; msg.data = ss.str(); ROS_INFO("%s", msg.data.c_str()); chatter_pub.publish(msg); ros::spinOnce(); loop_rate.sleep(); } return 0; } Migrating to ROS 2 ------------------ Let's start by creating a new workspace in which to work: .. code-block:: bash mkdir ~/ros2_talker cd ~/ros2_talker We'll copy the source tree from our ROS 1 package into that workspace, where we can modify it: .. code-block:: bash mkdir src cp -a ~/ros1_talker/src/talker src Now we'll modify the C++ code in the node. The ROS 2 C++ library, called ``rclcpp``, provides a different API from that provided by ``roscpp``. The concepts are very similar between the two libraries, which makes the changes reasonably straightforward to make. Included headers ~~~~~~~~~~~~~~~~ In place of ``ros/ros.h``, which gave us access to the ``roscpp`` library API, we need to include ``rclcpp/rclcpp.hpp``, which gives us access to the ``rclcpp`` library API: .. code-block:: cpp //#include "ros/ros.h" #include "rclcpp/rclcpp.hpp" To get the ``std_msgs/String`` message definition, in place of ``std_msgs/String.h``, we need to include ``std_msgs/msg/string.hpp``: .. code-block:: cpp //#include "std_msgs/String.h" #include "std_msgs/msg/string.hpp" Changing C++ library calls ~~~~~~~~~~~~~~~~~~~~~~~~~~ Instead of passing the node's name to the library initialization call, we do the initialization, then pass the node name to the creation of the node object: .. code-block:: cpp // ros::init(argc, argv, "talker"); // ros::NodeHandle n; rclcpp::init(argc, argv); auto node = rclcpp::Node::make_shared("talker"); The creation of the publisher and rate objects looks pretty similar, with some changes to the names of namespace and methods. .. code-block:: cpp // ros::Publisher chatter_pub = n.advertise("chatter", 1000); // ros::Rate loop_rate(10); auto chatter_pub = node->create_publisher("chatter", 1000); rclcpp::Rate loop_rate(10); To further control how message delivery is handled, a quality of service (``QoS``) profile could be passed in. The default profile is ``rmw_qos_profile_default``. For more details, see the `design document `__ and :doc:`concept overview <../../Concepts/Intermediate/About-Quality-of-Service-Settings>`. The creation of the outgoing message is different in the namespace: .. code-block:: cpp // std_msgs::String msg; std_msgs::msg::String msg; In place of ``ros::ok()``, we call ``rclcpp::ok()``: .. code-block:: cpp // while (ros::ok()) while (rclcpp::ok()) Inside the publishing loop, we access the ``data`` field as before: .. code-block:: cpp msg.data = ss.str(); To print a console message, instead of using ``ROS_INFO()``, we use ``RCLCPP_INFO()`` and its various cousins. The key difference is that ``RCLCPP_INFO()`` takes a Logger object as the first argument. .. code-block:: cpp // ROS_INFO("%s", msg.data.c_str()); RCLCPP_INFO(node->get_logger(), "%s\n", msg.data.c_str()); Change the publish call to use the ``->`` operator instead of ``.``. .. code-block:: cpp // chatter_pub.publish(msg); chatter_pub->publish(msg); Spinning (i.e., letting the communications system process any pending incoming/outgoing messages) is different in that the call now takes the node as an argument: .. code-block:: cpp // ros::spinOnce(); rclcpp::spin_some(node); Sleeping using the rate object is unchanged. Putting it all together, the new ``talker.cpp`` looks like this: .. code-block:: cpp #include // #include "ros/ros.h" #include "rclcpp/rclcpp.hpp" // #include "std_msgs/String.h" #include "std_msgs/msg/string.hpp" int main(int argc, char **argv) { // ros::init(argc, argv, "talker"); // ros::NodeHandle n; rclcpp::init(argc, argv); auto node = rclcpp::Node::make_shared("talker"); // ros::Publisher chatter_pub = n.advertise("chatter", 1000); // ros::Rate loop_rate(10); auto chatter_pub = node->create_publisher("chatter", 1000); rclcpp::Rate loop_rate(10); int count = 0; // std_msgs::String msg; std_msgs::msg::String msg; // while (ros::ok()) while (rclcpp::ok()) { std::stringstream ss; ss << "hello world " << count++; msg.data = ss.str(); // ROS_INFO("%s", msg.data.c_str()); RCLCPP_INFO(node->get_logger(), "%s\n", msg.data.c_str()); // chatter_pub.publish(msg); chatter_pub->publish(msg); // ros::spinOnce(); rclcpp::spin_some(node); loop_rate.sleep(); } return 0; } Change the ``package.xml`` ~~~~~~~~~~~~~~~~~~~~~~~~~~ ROS 2 packages use CMake functions and macros from ``ament_cmake_ros`` instead of ``catkin``. Delete the dependency on ``catkin``: .. code-block:: catkin` Add a new dependency on ``ament_cmake_ros``: .. code-block:: xml ament_cmake_ros ROS 2 C++ libraries use `rclcpp `__ instead of `roscpp `__. Delete the dependency on ``roscpp``: .. code-block:: roscpp Add a dependency on ``rclcpp``: .. code-block:: xml rclcpp Add an ```` section to tell colcon the package is an ``ament_cmake`` package instead of a ``catkin`` package. .. code-block:: xml ament_cmake Your ``package.xml`` now looks like this: .. code-block:: xml talker 0.0.0 talker Brian Gerkey Apache-2.0 ament_cmake rclcpp std_msgs ament_cmake Changing the CMake code ~~~~~~~~~~~~~~~~~~~~~~~ Require a newer version of CMake so that ``ament_cmake`` functions work correctly. .. code-block:: cmake_minimum_required(VERSION 3.14.4) Use a newer C++ standard matching the version used by your target ROS distro in `REP 2000 `__. If you are using C++17, then set that version with the following snippet after the ``project(talker)`` call. Add extra compiler checks too because it is a good practice. .. code-block:: cmake if(NOT CMAKE_CXX_STANDARD) set(CMAKE_CXX_STANDARD 17) endif() if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic) endif() Replace the ``find_package(catkin ...)`` call with individual calls for each dependency. .. code-block:: cmake find_package(ament_cmake REQUIRED) find_package(rclcpp REQUIRED) find_package(std_msgs REQUIRED) Delete the call to ``catkin_package()``. Add a call to ``ament_package()`` at the bottom of the ``CMakeLists.txt``. .. code-block:: cmake ament_package() Make the ``target_link_libraries`` call modern CMake targets provided by ``rclcpp`` and ``std_msgs``. .. code-block:: cmake target_link_libraries(talker PUBLIC rclcpp::rclcpp ${std_msgs_TARGETS}) Delete the call to ``include_directories()``. Add a call to ``target_include_directories()`` below ``add_executable(talker talker.cpp)``. Don't pass variables like ``rclcpp_INCLUDE_DIRS`` into ``target_include_directories()``. The include directories are already handled by calling ``target_link_libraries()`` with modern CMake targets. .. code-block:: cmake target_include_directories(talker PUBLIC "$" "$") Change the call to ``install()`` so that the ``talker`` executable is installed into a project specific directory. .. code-block:: cmake install(TARGETS talker DESTINATION lib/${PROJECT_NAME}) The new ``CMakeLists.txt`` looks like this: .. code-block:: cmake cmake_minimum_required(VERSION 3.14.4) project(talker) if(NOT CMAKE_CXX_STANDARD) set(CMAKE_CXX_STANDARD 17) endif() if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic) endif() find_package(ament_cmake REQUIRED) find_package(rclcpp REQUIRED) find_package(std_msgs REQUIRED) add_executable(talker talker.cpp) target_include_directories(talker PUBLIC "$" "$") target_link_libraries(talker PUBLIC rclcpp::rclcpp ${std_msgs_TARGETS}) install(TARGETS talker DESTINATION lib/${PROJECT_NAME}) ament_package() Building the ROS 2 code ~~~~~~~~~~~~~~~~~~~~~~~ We source an environment setup file (in this case the one generated by following the ROS 2 installation tutorial, which builds in ``~/ros2_ws``, then we build our package using ``colcon build``: .. code-block:: bash . ~/ros2_ws/install/setup.bash cd ~/ros2_talker colcon build Running the ROS 2 node ~~~~~~~~~~~~~~~~~~~~~~ Because we installed the ``talker`` executable into the correct directory, after sourcing the setup file, from our install tree, we can invoke it by running: .. code-block:: bash . ~/ros2_ws/install/setup.bash ros2 run talker talker Conclusion ---------- You have learned how to migrate an example C++ ROS 1 package to ROS 2. Use the :doc:`Migrating C++ Packages reference page <./Migrating-CPP-Packages>` to help you migrate your own C++ packages from ROS 1 to ROS 2.