Skip to content

C++ 软件包迁移示例

本示例展示了如何将一个 C++ 示例软件包从 ROS 1 迁移到 ROS 2。

需要安装好可正常使用的 ROS 2 环境,例如 ROS DISTRO。

假设你有一个名为 talker 的 ROS 1 软件包,它在一个名为 talker 的节点中使用 roscpp。 该软件包位于 catkin 工作空间中,路径为 ~/ros1_talker。

你的 ROS 1 工作空间具有以下目录布局:

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

文件内容如下:

src/talker/package.xml:

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="2">
<name>talker</name>
<version>0.0.0</version>
<description>talker</description>
<maintainer email="gerkey@example.com">Brian Gerkey</maintainer>
<license>Apache-2.0</license>
<buildtool_depend>catkin</buildtool_depend>
<depend>roscpp</depend>
<depend>std_msgs</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;
}

首先创建一个新的工作空间:

Terminal window
mkdir ~/ros2_talker
cd ~/ros2_talker

将 ROS 1 软件包的源代码复制到该工作空间中,方便后续修改:

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

接下来修改节点中的 C++ 代码。 ROS 2 的 C++ 库(即 rclcpp)提供了与 roscpp 不同的 API,但两者的概念非常相似,因此迁移起来并不复杂。

用 rclcpp/rclcpp.hpp(用于访问 rclcpp 库 API)替换 ros/ros.h(用于访问 roscpp 库 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"

ROS 2 中不再将节点名称传给初始化调用,而是先执行初始化,再在创建节点对象时传入节点名称:

// ros::init(argc, argv, "talker");
// ros::NodeHandle n;
rclcpp::init(argc, argv);
auto node = rclcpp::Node::make_shared("talker");

创建 publisher 和 rate 对象的方式基本一致,只是命名空间和方法名有所不同。

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

将 publish 调用中的 . 运算符改为 ->。

// chatter_pub.publish(msg);
chatter_pub->publish(msg);

Spin(即让通信系统处理所有待处理的传入/传出消息,直到没有更多工作为止)也有所不同:该调用现在接受节点和超时作为参数:

// ros::spinOnce();
rclcpp::spin_all(node, 0s);

rate 对象的 sleep 调用保持不变。

修改完成后的 talker.cpp 如下所示:

#include <chrono>
#include <sstream>
// #include "ros/ros.h"
#include "rclcpp/rclcpp.hpp"
// #include "std_msgs/String.h"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
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);
chatter_pub->publish(msg);
// ros::spinOnce();
rclcpp::spin_all(node, 0s);
loop_rate.sleep();
}
return 0;
}

ROS 2 软件包使用来自 ament_cmake_ros(而非 catkin)的 CMake 函数和宏。 首先删除对 catkin 的依赖:

<!-- delete this -->
<buildtool_depend>catkin</buildtool_depend>`

添加对 ament_cmake_ros 的新依赖:

<buildtool_depend>ament_cmake_ros</buildtool_depend>

ROS 2 C++ 库使用 rclcpp 而非 roscpp,因此需要删除对 roscpp 的依赖:

<!-- delete this -->
<depend>roscpp</depend>

添加对 rclcpp 的依赖:

<depend>rclcpp</depend>

添加 <export> 部分,告知 colcon 这是一个 ament_cmake 软件包而非 catkin 软件包。

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

package.xml 修改后如下所示:

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="2">
<name>talker</name>
<version>0.0.0</version>
<description>talker</description>
<maintainer email="gerkey@example.com">Brian Gerkey</maintainer>
<license>Apache-2.0</license>
<buildtool_depend>ament_cmake</buildtool_depend>
<depend>rclcpp</depend>
<depend>std_msgs</depend>
<export>
<build_type>ament_cmake</build_type>
</export>
</package>

ROS 2 要求使用更新版本的 CMake,才能使 ament_cmake 函数正常工作。

cmake_minimum_required(VERSION 3.20)

根据 REP 2000 中目标 ROS 发行版所要求的版本,使用相应的 C++ 标准。 如果使用 C++20,则在 project(talker) 调用之后添加以下代码片段来设置标准。 同时建议加上编译器检查,这是一个好习惯。

if(NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 20)
endif()
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()

将 find_package(catkin ...) 调用替换为每个依赖项的单独调用。

find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)

删除 catkin_package() 调用,并在 CMakeLists.txt 末尾添加 ament_package() 调用。

ament_package()

修改 target_link_libraries 调用,使用由 rclcpp 和 std_msgs 提供的现代 CMake targets。

target_link_libraries(talker PUBLIC
rclcpp::rclcpp
std_msgs::std_msgs)

删除 include_directories() 调用,在 add_executable(talker talker.cpp) 下方添加 target_include_directories() 调用。 注意不要将 rclcpp_INCLUDE_DIRS 等变量传入 target_include_directories()。 通过使用现代 CMake targets 调用 target_link_libraries(),include 目录已经自动处理了。

target_include_directories(talker PUBLIC
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>"
"$<INSTALL_INTERFACE:include/${PROJECT_NAME}>")

修改 install() 调用,使 talker 可执行文件安装到项目特定的目录中。

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

修改后的 CMakeLists.txt 如下所示:

cmake_minimum_required(VERSION 3.20)
project(talker)
if(NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 20)
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
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>"
"$<INSTALL_INTERFACE:include/${PROJECT_NAME}>")
target_link_libraries(talker PUBLIC
rclcpp::rclcpp
std_msgs::std_msgs)
install(TARGETS talker
DESTINATION lib/${PROJECT_NAME})
ament_package()

先 source 环境设置文件(本例中使用的是按照 ROS 2 安装教程生成的那一个,构建在 ~/ros2_ws 中),然后使用 colcon build 构建软件包:

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

由于 talker 可执行文件已安装到正确的目录中,在 source 安装树的设置文件后,可以通过以下命令运行它:

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

至此,你已经学会了如何将一个 C++ ROS 1 示例软件包迁移到 ROS 2。 在迁移自己的 C++ 软件包时,可参考 C++ 软件包迁移参考页面。