迁移 C++ 包

构建工具

在ROS 2中,不再使用 catkin_makecatkin_make_isolatedcatkin build,而是使用命令行工具 colcon 来构建和安装一组包。请查看 初学者教程 以开始使用 colcon

构建系统

ROS 2 中的构建系统称为 ament。Ament 基于 CMake 构建:ament_cmake 提供了用于更轻松编写 CMakeLists.txt 文件的 CMake 函数。

更新 CMakeLists.txt 使用 ament_cmake

将以下更改应用于使用``ament_cmake``而不是``catkin``:

  • 在``package.xml``文件的导出部分中设置构建类型:

    <export>
      <build_type>ament_cmake</build_type>
    </export>
    
  • 将``find_package``调用替换为``catkin``,``COMPONENTS``改为:

    find_package(ament_cmake REQUIRED)
    find_package(component1 REQUIRED)
    # ...
    find_package(componentN REQUIRED)
    
  • 移动并更新``catkin_package``调用为:

    • 请改为调用``ament_package``,但是要在所有目标都注册之后调用。

    • ament_package``的唯一有效参数是``CONFIG_EXTRAS。所有其他参数都由单独的函数处理,这些函数都需要在``ament_package``之前调用:

      • 不要使用``CATKIN_DEPENDS ...``,而是在之前调用``ament_export_dependencies(...)``。

      • 不要使用``INCLUDE_DIRS ...``,而是在之前调用``ament_export_include_directories(...)``。

      • 在调用``LIBRARIES ...``之前,请使用``ament_export_libraries(...)``进行传递。

  • 将``add_message_files``、``add_service_files``和``generate_messages``的调用替换为`rosidl_generate_interfaces <https://github.com/ros2/rosidl/blob/humble/rosidl_cmake/cmake/rosidl_generate_interfaces.cmake>`__。

    • 第一个参数是``target_name``。如果你只构建一个库,那么它是``${PROJECT_NAME}``。

    • 接着是相对于软件包根目录的消息文件名列表。

      • 如果您将多次使用文件名列表,则建议为清晰起见编写一个消息文件列表,并将其传递给函数。

    • 最后一个多值关键字参数是``generate_messages``的``DEPENDENCIES``,需要依赖消息软件包的列表。

      rosidl_generate_interfaces(${PROJECT_NAME}
        ${msg_files}
        DEPENDENCIES std_msgs
      )
      
  • 删除所有*devel空间*的出现。相关的CMake变量,如``CATKIN_DEVEL_PREFIX``,已不再存在。

    • CATKIN_DEPENDSDEPENDS 参数被传递给新的函数 ament_export_dependencies

    • CATKIN_GLOBAL_BIN_DESTINATION: bin

    • CATKIN_GLOBAL_INCLUDE_DESTINATION: include

    • CATKIN_GLOBAL_LIB_DESTINATION: lib

    • CATKIN_GLOBAL_LIBEXEC_DESTINATIONlib

    • CATKIN_GLOBAL_SHARE_DESTINATIONshare

    • CATKIN_PACKAGE_BIN_DESTINATIONlib/${PROJECT_NAME}

    • CATKIN_PACKAGE_INCLUDE_DESTINATIONinclude/${PROJECT_NAME}

    • CATKIN_PACKAGE_LIB_DESTINATION: lib

    • CATKIN_PACKAGE_SHARE_DESTINATION: share/${PROJECT_NAME}

单元测试

如果您正在使用 gtest:

CATKIN_ENABLE_TESTING 替换为 BUILD_TESTING。将 catkin_add_gtest 替换为 ament_add_gtest

-   if (CATKIN_ENABLE_TESTING)
-     find_package(GTest REQUIRED)  # or rostest
-     include_directories(${GTEST_INCLUDE_DIRS})
-     catkin_add_gtest(${PROJECT_NAME}-some-test src/test/some_test.cpp)
-     target_link_libraries(${PROJECT_NAME}-some-test
-       ${PROJECT_NAME}_some_dependency
-       ${catkin_LIBRARIES}
-       ${GTEST_LIBRARIES})
-   endif()
+   if (BUILD_TESTING)
+     find_package(ament_cmake_gtest REQUIRED)
+     ament_add_gtest(${PROJECT_NAME}-some-test src/test/test_something.cpp)
+     ament_target_dependencies(${PROJECT_NAME)-some-test
+       "rclcpp"
+       "std_msgs")
+     target_link_libraries(${PROJECT_NAME}-some-test
+       ${PROJECT_NAME}_some_dependency)
+   endif()

在你的 package.xml 中添加 <test_depend>ament_cmake_gtest</test_depend>

-   <test_depend>rostest</test_depend>
+   <test_depend>ament_cmake_gtest</test_depend>

代码检查工具

在 ROS 2 中,我们努力保持干净的代码,并使用代码检查工具。不同语言的代码风格在我们的 开发者指南 中有定义。

如果您要从头开始创建项目,建议遵循代码风格指南,并通过在 if(BUILD_TESTING) 下方添加以下行来启用自动代码检查单元测试:

find_package(ament_lint_auto REQUIRED)
ament_lint_auto_find_test_dependencies()

您还需要将以下依赖项添加到您的 package.xml 文件中:

<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>

更新源代码

消息、服务和动作

ROS 2的消息、服务和动作的命名空间在包名后使用子命名空间(分别是``msg``、srv``或``action)。因此,一个包含语句的形式如下:#include <my_interfaces/msg/my_message.hpp>。C++类型的命名为:my_interfaces::msg::MyMessage

共享指针类型在消息结构体中作为typedef提供:my_interfaces::msg::MyMessage::SharedPtr``以及``my_interfaces::msg::MyMessage::ConstSharedPtr

更多详细信息请参阅有关`生成的C++接口 <https://design.ros2.org/articles/generated_interfaces_cpp.html>`__的文章。

迁移需要通过以下方式更改包含:

  • 在包名称和消息数据类型之间插入子文件夹``msg``

  • 将包含的文件名从驼峰式更改为下划线分隔

  • 将``*.h``更改为``*.hpp``

// ROS 1 style is in comments, ROS 2 follows, uncommented.
// # include <geometry_msgs/PointStamped.h>
#include <geometry_msgs/msg/point_stamped.hpp>

// geometry_msgs::PointStamped point_stamped;
geometry_msgs::msg::PointStamped point_stamped;

迁移需要在所有实例中插入``msg``命名空间的代码。

使用服务对象

ROS 2中的服务回调函数不具有布尔返回值。建议在失败时抛出异常,而不是返回false。

// ROS 1 style is in comments, ROS 2 follows, uncommented.
// #include "nav_msgs/GetMap.h"
#include "nav_msgs/srv/get_map.hpp"

// bool service_callback(
//   nav_msgs::GetMap::Request & request,
//   nav_msgs::GetMap::Response & response)
void service_callback(
  const std::shared_ptr<nav_msgs::srv::GetMap::Request> request,
  std::shared_ptr<nav_msgs::srv::GetMap::Response> response)
{
  // ...
  // return true;  // or false for failure
}

ros::Time的用法

关于``ros::Time``的用法:

  • 将所有``ros::Time``的实例替换为``rclcpp::Time``

  • 如果您的消息或代码使用了std_msgs::Time:

    • 将所有的std_msgs::Time实例转换为builtin_interfaces::msg::Time

    • 将所有的#include "std_msgs/time.h"转换为#include "builtin_interfaces/msg/time.hpp"

    • 将所有使用std_msgs::Time字段``nsec``的实例转换为builtin_interfaces::msg::Time字段``nanosec``

ros::Rate的用法

有一个等效的类型 rclcpp::Rate 对象,基本上可以替换 ros::Rate

Boost

之前由 Boost 提供的许多功能已经集成到 C++ 标准库中。因此,我们希望尽可能利用新的核心功能,并避免对 Boost 的依赖。

共享指针

将共享指针从 boost 转换为标准 C++,请替换以下实例:

  • #include <boost/shared_ptr.hpp> 替换为 #include <memory>

  • boost::shared_ptr 替换为 std::shared_ptr

还可能存在诸如 weak_ptr 的变体,您也希望进行转换。

此外,建议使用 using 而不是 typedefusing 在模板逻辑中能够更好地发挥作用。有关详细信息,请参阅 这里

线程/互斥锁

在ROS代码库中,boost::thread 中的互斥锁是另一个常见的boost部分。

  • 将``boost::mutex::scoped_lock``替换为``std::unique_lock<std::mutex>``

  • 将``boost::mutex``替换为``std::mutex``

  • 将``#include <boost/thread/mutex.hpp>``替换为``#include <mutex>``

无序映射

替换:

  • #include <boost/unordered_map.hpp>#include <unordered_map>

  • boost::unordered_mapstd::unordered_map

函数

替换:

  • #include <boost/function.hpp>#include <functional>

  • boost::functionstd::function

示例:将现有的ROS 1 包转换为 ROS 2

假设我们有一个名为``talker``的简单ROS 1包,在一个名为``talker``的节点中使用了``roscpp``。该包位于catkin工作空间中,位置为``~/ros1_talker``。

ROS 1代码

这是我们的catkin工作空间的目录布局:

$ cd ~/ros1_talker
$ find .
.
./src
./src/talker
./src/talker/package.xml
./src/talker/CMakeLists.txt
./src/talker/talker.cpp

以下是这三个文件的内容:

src/talker/package.xml

<package>
  <name>talker</name>
  <version>0.0.0</version>
  <description>talker</description>
  <maintainer email="gerkey@osrfoundation.org">Brian Gerkey</maintainer>
  <license>Apache 2.0</license>
  <buildtool_depend>catkin</buildtool_depend>
  <build_depend>roscpp</build_depend>
  <build_depend>std_msgs</build_depend>
  <run_depend>roscpp</run_depend>
  <run_depend>std_msgs</run_depend>
</package>

src/talker/CMakeLists.txt

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

#include <sstream>
#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<std_msgs::String>("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;
}

构建 ROS 1 代码

我们会源码一个环境设置文件(在此示例中是 Noetic 使用 bash 的情况),然后使用 catkin_make install 构建我们的包:

. /opt/ros/noetic/setup.bash
cd ~/ros1_talker
catkin_make install

运行 ROS 1 节点

如果还没有运行roscore,我们首先从我们的``catkin``安装目录中的设置文件开始运行``roscore``(系统设置文件位于``/opt/ros/noetic/setup.bash``也可以使用在这里):

. ~/ros1_talker/install/setup.bash
roscore

在另一个终端中,我们使用 rosruncatkin 安装空间运行节点,再次首先获取设置文件(在这种情况下,必须是我们工作空间中的设置文件):

. ~/ros1_talker/install/setup.bash
rosrun talker talker

迁移到ROS 2

我们首先创建一个新的工作空间来进行工作:

mkdir ~/ros2_talker
cd ~/ros2_talker

我们将从我们的ROS 1软件包中复制源代码树到该工作空间中,在那里我们可以进行修改:

mkdir src
cp -a ~/ros1_talker/src/talker src

现在我们将修改节点中的C++代码。ROS 2的C++库名为``rclcpp``,其提供的API与``roscpp``提供的API不同。这两个库之间的概念非常相似,这使得修改相对来说相当简单。

包含的头文件

我们需要包含``rclcpp/rclcpp.hpp``来替代``ros/ros.h``,后者提供了对``roscpp``库API的访问权限,而前者则提供了对``rclcpp``库API的访问权限:

//#include "ros/ros.h"
#include "rclcpp/rclcpp.hpp"

要获取``std_msgs/String``消息定义,我们需要包含``std_msgs/msg/string.hpp``,而不是``std_msgs/String.h``:

//#include "std_msgs/String.h"
#include "std_msgs/msg/string.hpp"

更改C++库调用

与将节点名称传递给库初始化调用不同,我们首先进行初始化,然后将节点名称传递给节点对象的创建:

//  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<std_msgs::String>("chatter", 1000);
//  ros::Rate loop_rate(10);
  auto chatter_pub = node->create_publisher<std_msgs::msg::String>("chatter",
    1000);
  rclcpp::Rate loop_rate(10);

为了进一步控制消息传递的处理方式,可以传递一个质量服务(QoS)配置文件。默认配置文件是``rmw_qos_profile_default``。更多详细信息,请参阅 设计文档概念概述

在命名空间中,对传出消息的创建方式有所不同:

//  std_msgs::String msg;
  std_msgs::msg::String msg;

我们用 rclcpp::ok() 替代了 ros::ok()

//  while (ros::ok())
  while (rclcpp::ok())

在发布循环中,我们仍然像之前一样访问 data 字段:

msg.data = ss.str();

要打印控制台消息,不再使用``ROS_INFO()``,而是使用``RCLCPP_INFO()``及其各种相关函数。关键区别在于``RCLCPP_INFO()``将Logger对象作为第一个参数。

//    ROS_INFO("%s", msg.data.c_str());
    RCLCPP_INFO(node->get_logger(), "%s\n", msg.data.c_str());

发布消息的方式与之前相同:

chatter_pub->publish(msg);

循环(即让通信系统处理任何待处理的传入/传出消息)的方式不同,调用现在将节点作为参数:

//    ros::spinOnce();
    rclcpp::spin_some(node);

使用rate对象进行休眠的方式保持不变。

将所有内容组合在一起,新的 talker.cpp 如下所示:

#include <sstream>
// #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<std_msgs::String>("chatter", 1000);
//  ros::Rate loop_rate(10);
  auto chatter_pub = node->create_publisher<std_msgs::msg::String>("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);
//    ros::spinOnce();
    rclcpp::spin_some(node);
    loop_rate.sleep();
  }
  return 0;
}

修改 package.xml

ROS 2 使用名为 ament_cmake 的较新版本 catkin,我们在 buildtool_depend 标签中进行了指定:

<!--  <buildtool_depend>catkin</buildtool_depend> -->
  <buildtool_depend>ament_cmake</buildtool_depend>

在我们的构建依赖项中,我们使用``rclcpp``代替了``roscpp``,它提供了我们使用的C++ API。

<!--  <build_depend>roscpp</build_depend> -->
  <build_depend>rclcpp</build_depend>

在运行时依赖项中,我们进行了相同的修改,并且还将``run_depend``标签更新为``exec_depend``标签(这是升级到软件包格式版本2的一部分):

<!--  <run_depend>roscpp</run_depend> -->
  <exec_depend>rclcpp</exec_depend>
<!--  <run_depend>std_msgs</run_depend> -->
  <exec_depend>std_msgs</exec_depend>

在ROS 1中,我们使用``<depend>``来简化指定编译时和运行时的依赖关系。在ROS 2中,我们可以做同样的事情:

<depend>rclcpp</depend>
<depend>std_msgs</depend>

我们还需要告诉构建工具我们是哪种类型的软件包,以便它知道如何构建我们。因为我们使用``ament``和CMake,所以我们添加以下行来声明我们的构建类型为``ament_cmake``:

<export>
  <build_type>ament_cmake</build_type>
</export>

将所有内容放在一起,我们的``package.xml``现在看起来像这样:

<!-- <package> -->
<package format="2">
  <name>talker</name>
  <version>0.0.0</version>
  <description>talker</description>
  <maintainer email="gerkey@osrfoundation.org">Brian Gerkey</maintainer>
  <license>Apache License 2.0</license>
<!--  <buildtool_depend>catkin</buildtool_depend> -->
  <buildtool_depend>ament_cmake</buildtool_depend>
<!--  <build_depend>roscpp</build_depend> -->
<!--  <run_depend>roscpp</run_depend> -->
<!--  <run_depend>std_msgs</run_depend> -->
  <depend>rclcpp</depend>
  <depend>std_msgs</depend>
  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

修改CMake代码

ROS 2依赖于更高版本的CMake:

#cmake_minimum_required(VERSION 2.8.3)
cmake_minimum_required(VERSION 3.5)

ROS 2 依赖于 C++17 标准。取决于您使用的编译器,可能默认情况下未启用对 C++17 的支持。通过在文件顶部附近添加以下行来显式启用对 C++17 的支持:

set(CMAKE_CXX_STANDARD 17)

在所有平台上工作的首选方式如下:

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

使用 catkin,我们通过将要构建的包作为 COMPONENTS 参数传递给初始查找 catkin 本身的命令来指定要构建的包。对于 ament_cmake,我们逐个查找每个包,从 ament_cmake 开始:

#find_package(catkin REQUIRED COMPONENTS roscpp std_msgs)
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)

系统依赖项与以前一样可以找到:

find_package(Boost REQUIRED COMPONENTS system filesystem thread)

我们使用 catkin_package() 来自动生成像 CMake 配置文件这样的东西,以供使用我们包的其他包使用。与构建目标之前的调用相反,现在我们在目标之后调用类似的 ament_package()

# catkin_package()
# At the bottom of the file:
ament_package()

唯一需要手动包含的目录是本地目录和不是 ament 包的依赖项:

#include_directories(${catkin_INCLUDE_DIRS})
include_directories(include ${Boost_INCLUDE_DIRS})

更好的选择是为每个目标单独指定包含目录,而不是为所有目标包含所有目录:

target_include_directories(target PUBLIC include ${Boost_INCLUDE_DIRS})

类似于我们单独找到每个依赖包的方式,我们需要将每个依赖包链接到构建目标上。对于 ament 包作为依赖包的链接,不使用 target_link_libraries(),而是使用 ament_target_dependencies() 是处理构建标志更简洁和更彻底的方式。它自动处理 _INCLUDE_DIRS 中定义的包含目录和 _LIBRARIES 中定义的链接库。

#target_link_libraries(talker ${catkin_LIBRARIES})
ament_target_dependencies(talker
  rclcpp
  std_msgs)

要与非ament软件包(例如系统依赖项,如“Boost”或在同一个“CMakeLists.txt”中构建的库)进行链接,请使用“target_link_libraries()”:

target_link_libraries(target ${Boost_LIBRARIES})

对于安装,catkin 定义了变量,如 CATKIN_PACKAGE_BIN_DESTINATION。使用 ament_cmake,我们只需给出相对于安装根目录的路径:

#install(TARGETS talker
#  RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION})
install(TARGETS talker
  DESTINATION lib/${PROJECT_NAME})

可选地,我们可以安装并导出包含的目录给下游软件包:

install(DIRECTORY include/
  DESTINATION include)
ament_export_include_directories(include)

可选地,我们可以为下游软件包导出依赖项:

ament_export_dependencies(std_msgs)

将所有内容放在一起,新的 CMakeLists.txt 如下所示:

#cmake_minimum_required(VERSION 2.8.3)
cmake_minimum_required(VERSION 3.5)
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(catkin REQUIRED COMPONENTS roscpp std_msgs)
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
#catkin_package()
#include_directories(${catkin_INCLUDE_DIRS})
include_directories(include)
add_executable(talker talker.cpp)
#target_link_libraries(talker ${catkin_LIBRARIES})
ament_target_dependencies(talker
  rclcpp
  std_msgs)
#install(TARGETS talker
#  RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION})
install(TARGETS talker
  DESTINATION lib/${PROJECT_NAME})
install(DIRECTORY include/
  DESTINATION include)
ament_export_include_directories(include)
ament_export_dependencies(std_msgs)
ament_package()

构建 ROS 2 代码

我们先引用一个环境设置文件(在这个例子中,我们按照 ROS 2 安装教程生成了一个位于 ~/ros2_ws 的文件),然后使用 colcon build 构建我们的软件包:

. ~/ros2_ws/install/setup.bash
cd ~/ros2_talker
colcon build

运行ROS 2节点

因为我们将``talker``可执行文件安装到了正确的目录中,所以在从我们的安装目录中的设置文件进行源码后,我们可以通过运行以下命令来调用它:

. ~/ros2_ws/install/setup.bash
ros2 run talker talker