编写简单的发布者和订阅者(C++)
目标: 使用 C++ 创建并运行发布者(publisher)和订阅者(subscriber)节点。
教程级别: 初级
预计时间: 20 分钟
节点是通过 ROS 图进行通信的可执行进程。在本教程中,节点之间将以字符串消息的形式通过话题传递信息。这里使用的示例是一个简单的“发布者/订阅者”系统:一个节点发布数据,另一个节点订阅该话题来接收数据。
本示例中使用的代码可以在这里找到。
在之前的教程中,你已经学习了如何创建工作空间和功能包。
1 创建功能包
Section titled “1 创建功能包”打开一个新终端并 source ROS 2 安装环境,以便使用 ros2 命令。
导航到之前教程中创建的 ros2_ws 目录。
功能包应在 src 目录中创建,而非工作空间根目录。因此,进入 ros2_ws/src,然后运行创建功能包的命令:
ros2 pkg create --build-type ament_cmake --license Apache-2.0 cpp_pubsub终端将返回一条消息,确认功能包 cpp_pubsub 及其所有必要的文件和文件夹已创建。
进入 ros2_ws/src/cpp_pubsub/src。在任何 CMake 功能包中,包含可执行文件的源文件都应放在此目录下。
2 编写发布者节点
Section titled “2 编写发布者节点”输入以下命令下载示例发布者代码:
Linux/macOS:
wget -O publisher_lambda_function.cpp https://raw.githubusercontent.com/ros2/examples/{REPOS_FILE_BRANCH}/rclcpp/topics/minimal_publisher/lambda.cppWindows:
在 Windows 命令提示符中:
curl -sk https://raw.githubusercontent.com/ros2/examples/{REPOS_FILE_BRANCH}/rclcpp/topics/minimal_publisher/lambda.cpp -o publisher_lambda_function.cpp或在 PowerShell 中:
curl https://raw.githubusercontent.com/ros2/examples/{REPOS_FILE_BRANCH}/rclcpp/topics/minimal_publisher/lambda.cpp -o publisher_lambda_function.cpp现在会有一个名为 publisher_lambda_function.cpp 的新文件。用你喜欢的文本编辑器打开它。
#include <chrono>#include <memory>#include <string>
#include "rclcpp/rclcpp.hpp"#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
/* This example creates a subclass of Node and uses a fancy C++11 lambda* function to shorten the callback syntax, at the expense of making the* code somewhat more difficult to understand at first glance. */
class MinimalPublisher : public rclcpp::Node{public: MinimalPublisher() : Node("minimal_publisher"), count_(0) { publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10); auto timer_callback = [this]() -> void { auto message = std_msgs::msg::String(); message.data = "Hello, world! " + std::to_string(this->count_++); RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str()); this->publisher_->publish(message); }; timer_ = this->create_wall_timer(500ms, timer_callback); }
private: rclcpp::TimerBase::SharedPtr timer_; rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_; size_t count_;};
int main(int argc, char * argv[]){ rclcpp::init(argc, argv); rclcpp::spin(std::make_shared<MinimalPublisher>()); rclcpp::shutdown(); return 0;}2.1 代码解析
Section titled “2.1 代码解析”代码顶部包含了你要使用的标准 C++ 头文件。标准 C++ 头文件之后引入了 rclcpp/rclcpp.hpp,它提供了 ROS 2 系统中最常用的功能。最后是 std_msgs/msg/string.hpp,它包含了你将用于发布数据的内置消息类型。
#include <chrono>#include <memory>#include <string>
#include "rclcpp/rclcpp.hpp"#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;这些头文件代表了节点的依赖。注意,依赖必须同时添加到 package.xml 和 CMakeLists.txt 中,下一节将完成这一操作。
下一行通过继承 rclcpp::Node 创建了节点类 MinimalPublisher。代码中的每个 this 都指向该节点。
class MinimalPublisher : public rclcpp::Nodepublic 构造函数将节点命名为 minimal_publisher,并将 count_ 初始化为 0。在构造函数内部,发布者使用 String 消息类型、话题名称 topic 以及所需的队列大小完成初始化,以便在消息积压时限制排队数量。接下来,声明了一个名为 timer_callback 的 lambda 函数,它按引用捕获当前对象 this,不接受输入参数,返回类型为 void。timer_callback 函数创建一条新的 String 类型消息,用所需的字符串设置其数据并发布。RCLCPP_INFO 宏确保每条发布的消息都被打印到控制台。最后,timer_ 完成初始化,使得 timer_callback 函数每秒执行两次。
public: MinimalPublisher() : Node("minimal_publisher"), count_(0) { publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10); auto timer_callback = [this]() -> void { auto message = std_msgs::msg::String(); message.data = "Hello, world! " + std::to_string(this->count_++); RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str()); this->publisher_->publish(message); }; timer_ = this->create_wall_timer(500ms, timer_callback); }类的底部是 timer、publisher 和 counter 字段的声明。
private: rclcpp::TimerBase::SharedPtr timer_; rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_; size_t count_;在 MinimalPublisher 类之后是 main 函数,节点在此实际执行。rclcpp::init 初始化 ROS 2,rclcpp::spin 开始处理来自节点的数据,包括来自定时器的回调。
int main(int argc, char * argv[]){ rclcpp::init(argc, argv); rclcpp::spin(std::make_shared<MinimalPublisher>()); rclcpp::shutdown(); return 0;}2.2 添加依赖
Section titled “2.2 添加依赖”返回上一级到 ros2_ws/src/cpp_pubsub 目录,那里已经为你创建了 CMakeLists.txt 和 package.xml 文件。
用文本编辑器打开 package.xml。
如之前教程中所述,确保填写 <description>、<maintainer> 和 <license> 标签:
<description>Examples of minimal publisher/subscriber using rclcpp</description> <maintainer email="you@email.com">Your Name</maintainer> <license>Apache-2.0</license>在 ament_cmake buildtool 依赖之后添加一行,并粘贴与节点 include 语句相对应的以下依赖:
<depend>rclcpp</depend><depend>std_msgs</depend>这声明了该功能包在构建和运行代码时需要 rclcpp 和 std_msgs。
确保保存文件。
2.3 CMakeLists.txt
Section titled “2.3 CMakeLists.txt”现在打开 CMakeLists.txt 文件。在已有的 find_package(ament_cmake REQUIRED) 依赖下方,添加以下内容:
find_package(rclcpp REQUIRED)find_package(std_msgs REQUIRED)然后,添加可执行文件并将其命名为 talker,这样就可以使用 ros2 run 来运行节点:
add_executable(talker src/publisher_lambda_function.cpp)target_link_libraries(talker PUBLIC rclcpp::rclcpp std_msgs::std_msgs)最后,添加 install(TARGETS...) 部分,以便 ros2 run 能找到你的可执行文件:
install(TARGETS talker DESTINATION lib/${PROJECT_NAME})你可以删除一些不必要的部分和注释来精简 CMakeLists.txt,使其看起来像这样:
cmake_minimum_required(VERSION 3.20)project(cpp_pubsub)
# Default to C++20if(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 src/publisher_lambda_function.cpp)target_link_libraries(talker PUBLIC rclcpp::rclcpp std_msgs::std_msgs)
install(TARGETS talker DESTINATION lib/${PROJECT_NAME})
ament_package()你现在可以构建功能包、source 本地 setup 文件并运行它,但让我们先创建订阅者节点,这样就能看到整个系统的运行效果。
3 编写订阅者节点
Section titled “3 编写订阅者节点”返回 ros2_ws/src/cpp_pubsub/src 创建下一个节点。在终端中输入以下命令:
Linux/macOS:
wget -O subscriber_lambda_function.cpp https://raw.githubusercontent.com/ros2/examples/{REPOS_FILE_BRANCH}/rclcpp/topics/minimal_subscriber/lambda.cppWindows:
在 Windows 命令提示符中:
curl -sk https://raw.githubusercontent.com/ros2/examples/{REPOS_FILE_BRANCH}/rclcpp/topics/minimal_subscriber/lambda.cpp -o subscriber_lambda_function.cpp或在 PowerShell 中:
curl https://raw.githubusercontent.com/ros2/examples/{REPOS_FILE_BRANCH}/rclcpp/topics/minimal_subscriber/lambda.cpp -o subscriber_lambda_function.cpp检查确认文件已存在:
publisher_lambda_function.cpp subscriber_lambda_function.cpp用文本编辑器打开 subscriber_lambda_function.cpp。
#include <memory>
#include "rclcpp/rclcpp.hpp"#include "std_msgs/msg/string.hpp"
class MinimalSubscriber : public rclcpp::Node{public: MinimalSubscriber() : Node("minimal_subscriber") { auto topic_callback = [this](std_msgs::msg::String::UniquePtr msg) -> void { RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str()); }; subscription_ = this->create_subscription<std_msgs::msg::String>("topic", 10, topic_callback); }
private: rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;};
int main(int argc, char * argv[]){ rclcpp::init(argc, argv); rclcpp::spin(std::make_shared<MinimalSubscriber>()); rclcpp::shutdown(); return 0;}3.1 代码解析
Section titled “3.1 代码解析”订阅者节点的代码与发布者的几乎完全相同。不过节点名称改为 minimal_subscriber,构造函数使用节点的 create_subscription 函数来执行回调。
这里没有 timer,因为订阅者只需在有数据发布到 topic 话题时做出响应。
topic_callback 函数接收通过话题发布的字符串消息数据,并使用 RCLCPP_INFO 宏将其输出到控制台。
注意,发布者和订阅者使用的话题名称和消息类型必须匹配,双方才能进行通信。
public: MinimalSubscriber() : Node("minimal_subscriber") { auto topic_callback = [this](std_msgs::msg::String::UniquePtr msg) -> void { RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str()); }; subscription_ = this->create_subscription<std_msgs::msg::String>("topic", 10, topic_callback); }这个类中唯一的字段声明就是 subscription。
private: rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;main 函数完全相同,只是现在 spin 的是 MinimalSubscriber 节点。对于发布者节点,spin 意味着启动 timer;但对于订阅者来说,它只是意味着准备好随时接收消息。
由于该节点与发布者节点具有相同的依赖,因此不需要在 package.xml 中添加任何新内容。
3.2 CMakeLists.txt
Section titled “3.2 CMakeLists.txt”重新打开 CMakeLists.txt,在发布者条目下方为订阅者节点添加可执行文件和目标。
add_executable(listener src/subscriber_lambda_function.cpp)target_link_libraries(listener PUBLIC rclcpp::rclcpp std_msgs::std_msgs)
install(TARGETS talker listener DESTINATION lib/${PROJECT_NAME})保存文件后,你的发布/订阅系统就准备好了。
4 构建与运行
Section titled “4 构建与运行”你的 ROS 2 系统中可能已经安装了 rclcpp 和 std_msgs 功能包。在构建之前,建议在工作空间(ros2_ws)的根目录下运行 rosdep 来检查缺失的依赖:
Linux:
rosdep install -i --from-path src --rosdistro {DISTRO} -ymacOS:
rosdep 仅在 Linux 上运行,可直接跳到下一步。
Windows:
rosdep 仅在 Linux 上运行,可直接跳到下一步。
仍在工作空间根目录 ros2_ws 下构建新功能包:
Linux/macOS:
colcon build --packages-select cpp_pubsubWindows:
colcon build --merge-install --packages-select cpp_pubsub打开一个新终端,导航到 ros2_ws,并 source 环境配置文件:
Linux/macOS:
. install/setup.bashWindows:
call install/setup.bat现在运行 talker 节点。终端应该每 0.5 秒发布一条 info 消息,如下所示:
$ ros2 run cpp_pubsub talker [INFO] [minimal_publisher]: Publishing: "Hello World: 0" [INFO] [minimal_publisher]: Publishing: "Hello World: 1" [INFO] [minimal_publisher]: Publishing: "Hello World: 2" [INFO] [minimal_publisher]: Publishing: "Hello World: 3" [INFO] [minimal_publisher]: Publishing: "Hello World: 4"打开另一个终端,再次从 ros2_ws 中 source 环境配置文件,然后启动 listener 节点。listener 将开始向控制台打印消息,从发布者当时的消息计数开始:
$ ros2 run cpp_pubsub listener [INFO] [minimal_subscriber]: I heard: "Hello World: 10" [INFO] [minimal_subscriber]: I heard: "Hello World: 11" [INFO] [minimal_subscriber]: I heard: "Hello World: 12" [INFO] [minimal_subscriber]: I heard: "Hello World: 13" [INFO] [minimal_subscriber]: I heard: "Hello World: 14"在每个终端中输入 Ctrl+C 停止节点的运行。
你创建了两个节点,通过话题发布和订阅数据。在编译和运行它们之前,你将它们的依赖和可执行文件添加到了功能包配置文件中。
接下来,你将使用服务/客户端模型创建另一个简单的 ROS 2 功能包。同样,你可以选择用 C++ 或 Python 来编写。
用 C++ 编写发布者和订阅者有多种方式;请查看 ros2/examples 仓库中的 minimal_publisher 和 minimal_subscriber 功能包。