迁移 C++ 包
目录
构建工具
在ROS 2中,不再使用 catkin_make
、catkin_make_isolated
或 catkin 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_DEPENDS
和DEPENDS
参数被传递给新的函数 ament_export_dependencies。CATKIN_GLOBAL_BIN_DESTINATION
:bin
CATKIN_GLOBAL_INCLUDE_DESTINATION
:include
CATKIN_GLOBAL_LIB_DESTINATION
:lib
CATKIN_GLOBAL_LIBEXEC_DESTINATION
:lib
CATKIN_GLOBAL_SHARE_DESTINATION
:share
CATKIN_PACKAGE_BIN_DESTINATION
:lib/${PROJECT_NAME}
CATKIN_PACKAGE_INCLUDE_DESTINATION
:include/${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 的依赖。
线程/互斥锁
在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_map
为std::unordered_map
示例:将现有的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
在另一个终端中,我们使用 rosrun
从 catkin
安装空间运行节点,再次首先获取设置文件(在这种情况下,必须是我们工作空间中的设置文件):
. ~/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