Skip to content

编写简单的发布者和订阅者(C++)

目标: 使用 C++ 创建并运行发布者(publisher)和订阅者(subscriber)节点。

教程级别: 初级

预计时间: 20 分钟

节点是通过 ROS 图进行通信的可执行进程。在本教程中,节点之间将以字符串消息的形式通过话题传递信息。这里使用的示例是一个简单的“发布者/订阅者”系统:一个节点发布数据,另一个节点订阅该话题来接收数据。

本示例中使用的代码可以在这里找到。

在之前的教程中,你已经学习了如何创建工作空间和功能包。

打开一个新终端并 source ROS 2 安装环境,以便使用 ros2 命令。

导航到之前教程中创建的 ros2_ws 目录。

功能包应在 src 目录中创建,而非工作空间根目录。因此,进入 ros2_ws/src,然后运行创建功能包的命令:

Terminal window
ros2 pkg create --build-type ament_cmake --license Apache-2.0 cpp_pubsub

终端将返回一条消息,确认功能包 cpp_pubsub 及其所有必要的文件和文件夹已创建。

进入 ros2_ws/src/cpp_pubsub/src。在任何 CMake 功能包中,包含可执行文件的源文件都应放在此目录下。

输入以下命令下载示例发布者代码:

Linux/macOS:

Terminal window
wget -O publisher_lambda_function.cpp https://raw.githubusercontent.com/ros2/examples/{REPOS_FILE_BRANCH}/rclcpp/topics/minimal_publisher/lambda.cpp

Windows:

在 Windows 命令提示符中:

Terminal window
curl -sk https://raw.githubusercontent.com/ros2/examples/{REPOS_FILE_BRANCH}/rclcpp/topics/minimal_publisher/lambda.cpp -o publisher_lambda_function.cpp

或在 PowerShell 中:

Terminal window
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;
}

代码顶部包含了你要使用的标准 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::Node

public 构造函数将节点命名为 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;
}

返回上一级到 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。

确保保存文件。

现在打开 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++20
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 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 文件并运行它,但让我们先创建订阅者节点,这样就能看到整个系统的运行效果。

返回 ros2_ws/src/cpp_pubsub/src 创建下一个节点。在终端中输入以下命令:

Linux/macOS:

Terminal window
wget -O subscriber_lambda_function.cpp https://raw.githubusercontent.com/ros2/examples/{REPOS_FILE_BRANCH}/rclcpp/topics/minimal_subscriber/lambda.cpp

Windows:

在 Windows 命令提示符中:

Terminal window
curl -sk https://raw.githubusercontent.com/ros2/examples/{REPOS_FILE_BRANCH}/rclcpp/topics/minimal_subscriber/lambda.cpp -o subscriber_lambda_function.cpp

或在 PowerShell 中:

Terminal window
curl https://raw.githubusercontent.com/ros2/examples/{REPOS_FILE_BRANCH}/rclcpp/topics/minimal_subscriber/lambda.cpp -o subscriber_lambda_function.cpp

检查确认文件已存在:

Terminal window
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;
}

订阅者节点的代码与发布者的几乎完全相同。不过节点名称改为 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 中添加任何新内容。

重新打开 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})

保存文件后,你的发布/订阅系统就准备好了。

你的 ROS 2 系统中可能已经安装了 rclcpp 和 std_msgs 功能包。在构建之前,建议在工作空间(ros2_ws)的根目录下运行 rosdep 来检查缺失的依赖:

Linux:

Terminal window
rosdep install -i --from-path src --rosdistro {DISTRO} -y

macOS:

rosdep 仅在 Linux 上运行,可直接跳到下一步。

Windows:

rosdep 仅在 Linux 上运行,可直接跳到下一步。

仍在工作空间根目录 ros2_ws 下构建新功能包:

Linux/macOS:

Terminal window
colcon build --packages-select cpp_pubsub

Windows:

Terminal window
colcon build --merge-install --packages-select cpp_pubsub

打开一个新终端,导航到 ros2_ws,并 source 环境配置文件:

Linux/macOS:

Terminal window
. install/setup.bash

Windows:

Terminal window
call install/setup.bat

现在运行 talker 节点。终端应该每 0.5 秒发布一条 info 消息,如下所示:

Terminal window
$ 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 将开始向控制台打印消息,从发布者当时的消息计数开始:

Terminal window
$ 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 功能包。