实现发布者和订阅者(publisher & subscriber)(C++)

目标: 用 C++ 创建并运行 publisher & subscriber 节点.

教程等级: 初级

预计时长: 20 分钟

背景

节点 是可在 ROS 中通信的可执行进程. 在这个教程中,节点将通过 topic 以字符串消息的形式相互传递信息. 这里的例子是一个简单的 “talker” 和 “listener” 系统; 一个节点发布数据,另一个订阅主题以接收数据.

这些例子中使用的代码可以在 这里 找到.

前提条件

在之前的教程中,你学会了如何 创建工作空间创建包.

任务

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 编写发布者节点

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

wget -O publisher_member_function.cpp https://raw.githubusercontent.com/ros2/examples/humble/rclcpp/topics/minimal_publisher/member_function.cpp

现在会有一个新文件 publisher_member_function.cpp. 用你喜欢的文本编辑器打开这个文件.

#include <chrono>
#include <functional>
#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 std::bind() to register a
* member function as a callback from the timer. */

class MinimalPublisher : public rclcpp::Node
{
  public:
    MinimalPublisher()
    : Node("minimal_publisher"), count_(0)
    {
      publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
      timer_ = this->create_wall_timer(
      500ms, std::bind(&MinimalPublisher::timer_callback, this));
    }

  private:
    void timer_callback()
    {
      auto message = std_msgs::msg::String();
      message.data = "Hello, world! " + std::to_string(count_++);
      RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
      publisher_->publish(message);
    }
    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 检查代码

这个代码的最前面包含了你需要使用的 C++ 标准库头文件. 接下来 include rclcpp/rclcpp.hpp ,这样就能使用 ROS 2 系统最基本且最常用的部分. 最后 include std_msgs/msg/string.hpp ,这是你之后用来发布数据的内置消息类型.

#include <chrono>
#include <functional>
#include <memory>
#include <string>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

using namespace std::chrono_literals;

这几行代码声明了发布者节点的依赖. 记住,依赖必须在 package.xmlCMakeLists.txt 中添加,这是下一节你要做的事.

接下来的一行通过继承 rclcpp::Node 类创建了节点类 MinimalPublisher. 这里的每个 this 都指向节点.

class MinimalPublisher : public rclcpp::Node

这个公共构造函数将节点命名为 minimal_publisher 并将 count_ 初始化为 0. 在构造函数中,发布者使用 create_publisher 函数初始化,它的消息类型是 std_msgs::msg::String , topic 名是 topic ,队列大小是 10,用于限制备份时的消息数量. 接下来,初始化 timer_ ,让 timer_callback 函数每秒执行两次.

public:
  MinimalPublisher()
  : Node("minimal_publisher"), count_(0)
  {
    publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
    timer_ = this->create_wall_timer(
    500ms, std::bind(&MinimalPublisher::timer_callback, this));
  }

timer_callback 函数是设置消息数据并实际发布消息的函数. RCLCPP_INFO 宏确保每个发布的消息都打印到控制台.

private:
  void timer_callback()
  {
    auto message = std_msgs::msg::String();
    message.data = "Hello, world! " + std::to_string(count_++);
    RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
    publisher_->publish(message);
  }

最后声明定时器、发布者和计数变量.

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 添加依赖

返回到 ros2_ws/src/cpp_pubsub 目录,这里已经创建好了 CMakeLists.txtpackage.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 License 2.0</license>

ament_cmake 构建工具依赖后添加一行新行,粘贴以下依赖,这些依赖和节点的 include 语句对应:

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

这样声明了包在构建和执行时需要 rclcppstd_msgs.

记得保存文件。

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_member_function.cpp)
ament_target_dependencies(talker rclcpp std_msgs)

最后,添加 install(TARGETS...) 部分,这样 ros2 run 就能找到构建生成的可执行文件:

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

当然你可以清理一下 CMakeLists.txt ,删除一些不必要的部分和注释,让它看起来像这样:

cmake_minimum_required(VERSION 3.5)
project(cpp_pubsub)

# Default to C++14
if(NOT CMAKE_CXX_STANDARD)
  set(CMAKE_CXX_STANDARD 14)
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_member_function.cpp)
ament_target_dependencies(talker rclcpp std_msgs)

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

ament_package()

现在你可以构建包了,source 配置文件,然后运行节点。不过运行之前我们先创建订阅者节点,这样两部分配合起来就能看到整个系统是如何工作的.

3 编写订阅者节点

回到 ros2_ws/src/cpp_pubsub/src 目录,创建订阅者节点的代码. 在终端输入以下命令:

wget -O subscriber_member_function.cpp https://raw.githubusercontent.com/ros2/examples/humble/rclcpp/topics/minimal_subscriber/member_function.cpp

确定这两个文件是存在的:

publisher_member_function.cpp  subscriber_member_function.cpp

用你喜欢的文本编辑器打开 subscriber_member_function.cpp 文件.

#include <memory>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using std::placeholders::_1;

class MinimalSubscriber : public rclcpp::Node
{
  public:
    MinimalSubscriber()
    : Node("minimal_subscriber")
    {
      subscription_ = this->create_subscription<std_msgs::msg::String>(
      "topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
    }

  private:
    void topic_callback(const std_msgs::msg::String & msg) const
    {
      RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg.data.c_str());
    }
    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 检查代码

订阅者节点的代码几乎和发布者的一样. 现在节点叫 minimal_subscriber ,构造函数使用节点的 create_subscription 类来执行回调.

这里没有定时器,因为订阅者只需要在 topic 上有数据时作出响应.

public:
  MinimalSubscriber()
  : Node("minimal_subscriber")
  {
    subscription_ = this->create_subscription<std_msgs::msg::String>(
    "topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
  }

回想在 topic 教程 中,发布者和订阅者的 topic 名和消息类型必须匹配才能通信.

topic_callback 函数接收发布的字符串消息数据,并使用 RCLCPP_INFO 宏将其写入控制台.

唯一的变量声明是就是订阅本身.

private:
  void topic_callback(const std_msgs::msg::String & msg) const
  {
    RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg.data.c_str());
  }
  rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;

main 函数和发布者的一样,只是现在它让 MinimalSubscriber 节点运行. 对于发布者节点,spin 意味着开始计时器,但对于订阅者节点,它只是准备好接收消息.

由于这个节点和发布者节点有相同的依赖,所以 package.xml 中不需要添加新的内容.

3.2 CMakeLists.txt

再打开 CMakeLists.txt ,在发布者的条目下面添加订阅者节点的可执行文件和目标.

add_executable(listener src/subscriber_member_function.cpp)
ament_target_dependencies(listener rclcpp std_msgs)

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

保存文件之后,发布者和订阅者节点就都准备好了.

4 构建和运行

很大概率你已经安装了 rclcppstd_msgs 包,因为它们是 ROS 2 系统的一部分. 但是,最好在构建之前在工作空间的根目录( ros2_ws )下运行 rosdep 检查是否有缺少的依赖:

rosdep install -i --from-path src --rosdistro humble -y

现在在工作空间的根目录( ros2_ws )下构建新包:

colcon build --packages-select cpp_pubsub

构建完成后,打开新终端,进入 ros2_ws , source 配置文件:

. install/setup.bash

运行发布者节点:

ros2 run cpp_pubsub talker

终端开始每 0.5 秒发布一条消息,如下所示:

[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"

打开另一个终端,再次 source ros2_ws 中的配置文件,然后运行订阅者节点:

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 可以停止节点.

总结

你创建了两个节点,通过 topic 发布和订阅数据. 在编译和运行之前,添加了它们的依赖和可执行文件到包配置文件中.

下一步

接下来你需要创建另一个简单的 ROS 2 包,使用服务/客户端模型. 你可以选择用 C++ 或者 Python 来写.

相关内容

有很多种方法可以在 C++ 中实现发布者和订阅者; 查看 ros2/examples 中的 minimal_publisherminimal_subscriber 包.