ROS2的核心概念B-话题
一、话题
话题是节点间传递数据的桥梁,实现了节点之间某一个方向上的数据传输,让机器人可以在多个功能之间产生千丝万缕的联系。
二、话题模型
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>
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)