一、话题

话题是节点间传递数据的桥梁,实现了节点之间某一个方向上的数据传输,让机器人可以在多个功能之间产生千丝万缕的联系。

二、话题模型

1、发布/订阅模型

话题数据传输的特性是从一个节点到另外一个节点,发送数据的对象称之为发布者,接收数据的对象称之为订阅者,每一个话题都需要有一个名字,传输的数据也需要有固定的数据类型。

图1 话题的发布和订阅

图中的发布者和订阅者是节点,频道(redis服务)就相当于一个话题。

2、多对多模型

有以下三种形式:

  • 多个发布者对同一个话题

    • 允许多个节点同时向同一个话题发布消息。

    • 场景示例:多个传感器节点(如多个摄像头)都向/image_data话题发布图像。

  • 多个订阅者对同一个话题

    • 允许一个话题的数据被多个完全不同的节点同时接收和处理。

    • 场景示例/robot_pose(机器人位置)话题的数据可能同时被导航模块、可视化界面(Rviz)和日志记录节点订阅。

  • 发布者与订阅者数量不对等

    • 一个发布者对应零个、一个或多个订阅者。

    • 一个订阅者可以同时订阅零个、一个或多个发布者的话题。

为了实现这种灵活的多对多模型,ROS采用了以下机制:

  • 解耦:发布者不需要知道谁在订阅,甚至不知道有没有人在订阅;订阅者也不需要知道谁在发布。双方只通过话题名消息类型进行匹配。

  • 异步通信:发布者只管“发”(广播),不等待应答;订阅者通过回调机制“收”,不会阻塞发布者。

  • 动态连接:节点可以在系统运行时随时启动或关闭。当一个新节点加入时,ROS Master(或ROS2中的发现机制)会帮助建立发布者和订阅者之间的直接连接。

图2 两发布对两订阅

三、异步通信

1、什么是异步?

异步就是发布者发出数据后,并不知道订阅者什么时候可以收到,类似机器人大讲堂公众号发布一篇文章,你什么时候阅读的,机器人大讲堂根本不知道,报社发出一份报纸,你什么时候收到,报社也是不知道的。

2、话题适用在什么场景?

异步的特性也让话题更适合用于一些周期发布的数据,比如传感器的数据,运动控制的指令等等,如果某些逻辑性较强的指令,比如修改某一个参数,用话题传输就不太合适了。

四、消息接口

什么是消息呢?

在ROS中,话题通信数据的描述格式称之为消息,对应编程语言中数据结构的概念。比如一个图像数据,就会包含图像的长宽像素值、每个像素的RGB等等,在ROS中都有标准定义。

消息是ROS中的一种接口定义方式,与编程语言无关,我们也可以通过.msg后缀的文件自行定义,有了这样的接口,各种节点就像积木块一样,通过各种各样的接口进行拼接,组成复杂的机器人系统。

五、案例

图3 发布和订阅

本次案例也是打印字符串例程,创建一个发布者,发布话题“chatter”,周期发送“Aspire to inspire until I expire”这个字符串,消息类型是ROS中标准定义的String,再创建一个订阅者,订阅“chatter”这个话题,从而接收到“Aspire to inspire until I expire”这个字符串。

图4 功能包内部结构

topic_chatter_pub.cpp:

// 发布者
#include <chrono>
#include <functional>
#include <memory>
#include <string>

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

using namespace std::chrono_literals;

/* 创建一个发布者节点类 */
class PublisherNode : public rclcpp::Node
{
public:
    PublisherNode(std::string name) : Node(name)
    {
        // 创建发布者对象(消息类型、话题名、队列长度)
        publisher_ = this->create_publisher<std_msgs::msg::String>("chatter", 10);
        
        // 创建一个定时器(500ms周期,定时执行的回调函数)
        timer_ = this->create_wall_timer(
            500ms, std::bind(&PublisherNode::timer_callback, this));
    }

private:
    // 定时器周期执行的回调函数
    void timer_callback()
    {
        // 创建一个String类型的消息对象
        auto message = std_msgs::msg::String();
        
        // 填充消息对象中的消息数据
        message.data = "Aspire to inspire until I expire";
        
        // 发布话题消息
        publisher_->publish(message);
        
        // 输出日志信息,提示已经完成话题发布
        RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
    }
    
    // 成员变量声明
    rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
    rclcpp::TimerBase::SharedPtr timer_;
};

int main(int argc, char * argv[])
{
    // ROS2 C++接口库初始化
    rclcpp::init(argc, argv);
    
    // 创建节点对象并开始处理
    rclcpp::spin(std::make_shared<PublisherNode>("topic_chatter_pub"));
    
    // 清理资源
    rclcpp::shutdown();
    
    return 0;
}

topic_chatter_sub.cpp:

// 订阅者
#include <functional>
#include <memory>

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

/* 创建一个订阅者节点类 */
class SubscriberNode : public rclcpp::Node
{
public:
    SubscriberNode(std::string name) : Node(name)
    {
        // 创建订阅者对象(消息类型、话题名、订阅者回调函数、队列长度)
        subscription_ = this->create_subscription<std_msgs::msg::String>(
            "chatter", 10, std::bind(&SubscriberNode::listener_callback, this, std::placeholders::_1));
    }

private:
    // 回调函数,收到话题消息后对数据的处理
    void listener_callback(const std_msgs::msg::String::SharedPtr msg)
    {
        // 输出日志信息,提示订阅收到的话题消息
        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[])
{
    // ROS2 C++接口库初始化
    rclcpp::init(argc, argv);
    
    // 创建节点对象并开始处理
    rclcpp::spin(std::make_shared<SubscriberNode>("topic_chatter_sub"));
    
    // 清理资源
    rclcpp::shutdown();
    
    return 0;
}

CMakeLists.txt:

cmake_minimum_required(VERSION 3.8)
project(learn_c)

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)

# 添加可执行文件,可执行文件名node_luckygo
add_executable(node_luckygo src/node_LuckyGo.cpp)

# 编译发布者节点
add_executable(topic_chatter_pub src/topic_chatter_pub.cpp)
ament_target_dependencies(topic_chatter_pub rclcpp std_msgs)

# 编译订阅者节点
add_executable(topic_chatter_sub src/topic_chatter_sub.cpp)
ament_target_dependencies(topic_chatter_sub rclcpp std_msgs)

# 链接库
ament_target_dependencies(node_luckygo rclcpp)

# 安装可执行文件 - 使用 ${PROJECT_NAME} 而不是 ${learn_c}
install(TARGETS node_luckygo
  topic_chatter_pub
  topic_chatter_sub
  DESTINATION lib/${PROJECT_NAME})

if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  # the following line skips the linter which checks for copyrights
  # comment the line when a copyright and license is added to all source files
  set(ament_cmake_copyright_FOUND TRUE)
  # the following line skips cpplint (only works in a git repo)
  # comment the line when this package is in a git repo and when
  # a copyright and license is added to all source files
  set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()

ament_package()

package.xml:

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>learn_c</name>
  <version>0.0.0</version>
  <description>TODO: Package description</description>
  <maintainer email="sean@todo.todo">sean</maintainer>
  <license>TODO: License declaration</license>

  <buildtool_depend>ament_cmake</buildtool_depend>

  <depend>rclcpp</depend>
  <depend>std_msgs</depend>  <!-- 声明对标准消息的依赖 -->
  
  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>

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

编译功能包:

cd ~/study_ws  # 切换路径到工作空间根目录下

colcon build   # 编译创建的功能包

source install/local_setup.sh  #再次设置环境变量,只执行此条即可

运行结果:

启动的第一个终端

图5 发布者发送消息

启动的第二个终端

图6 订阅者接收消息

六、话题命令的常用操作

查看话题列表:ros2 topic list                           

查看话题信息:ros2 topic info <topic_name>  

查看话题发布频率:ros2 topic hz  <topic_name>    

查看话题传输带宽:ros2 topic bw <topic_name>    

查看话题数据:ros2 topic echo <topic_name>  

发布话题消息:ros2 topic pub <topic_name> <msg_type> <msg_data>

  

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐