【ROS1】06-ROS通信机制——话题通信
目录
一、话题通信的四大要素
二、工作流程
三、案例
3.1 C++实现
3.1.1 发布方
3.1.2 订阅方
3.1.3 计算图展示
3.2 Python实现
3.2.1 发布方
3.2.2 订阅方
3.2.3 计算图展示
3.3 C++节点与Python节点通信
ROS的基本通信机制主要有话题通信(发布订阅模式)、服务通信(请求响应模式)、参数服务器(参数共享模式),本篇文章主要介绍ROS的话题通信。
一、话题通信的四大要素
-
节点 (Node):执行具体任务的程序。它既可以是发布者,也可以是订阅者,或者两者都是。
-
话题 (Topic):数据传输的通道,有一个唯一的名字(例如 /cmd_vel)。它本身不存储数据,只是一个用于路由消息的“总线”或“标签”。
-
消息 (Message):在话题上传输的数据。它具有严格的、预定义的数据结构。例如,一个名为 geometry_msgs/Twist 的消息类型,专门用来描述速度,其内部包含 linear(线速度)和 angular(角速度)两个字段。
-
ROS Master (节点管理器):整个系统的“电话总机”。它不参与实际的数据传输,但负责帮助发布者和订阅者相互发现。
二、工作流程
-
发布者上线:
-
一个节点(我们称之为Node_A)启动,并决定要发布一个名为 /scan 的话题。
-
Node_A 向 ROS Master 发送一个注册请求:“你好Master,我是Node_A,我的网络地址是IP_A:Port_A,我要发布一个叫 /scan 的话题,消息类型是 sensor_msgs/LaserScan。”
-
ROS Master 记录下这些信息。
-
-
订阅者上线:
-
另一个节点(Node_B)启动,它需要激光雷达数据,所以它决定订阅 /scan 话题。
-
Node_B 向 ROS Master 发送一个请求:“你好Master,我是Node_B,我的网络地址是IP_B:Port_B,我想订阅 /scan 话题。”
-
ROS Master 检查它的记录,发现Node_A正在发布这个话题。
-
ROS Master 将Node_A的网络地址(IP_A:Port_A)回复给Node_B。
-
-
建立直接连接:
-
Node_B(订阅者)收到Node_A(发布者)的地址后,会主动向Node_A发起一个直接的点对点(Peer-to-Peer)TCP连接请求。
-
Node_A接受连接请求。
-
-
数据传输:
-
连接建立后,Node_A就可以通过这个TCP连接,将 sensor_msgs/LaserScan 类型的消息源源不断地直接发送给 Node_B。
-
关键点:这个数据传输过程完全不经过ROS Master。Master只在最初的“牵线搭桥”阶段起作用。这保证了通信的高效率。
-
如果之后又有Node_C也想订阅/scan话题,上述第2、3、4步会重复一遍,Node_A会与Node_C建立一个新的、独立的TCP连接。
三、案例
实现发布方持续发送文本,订阅方接收文本并输出
3.1 C++实现
3.1.1 发布方
进入到工作空间的src目录下,输入如下指令来创建一个名为“plumbing_pub_sub”的功能包
catkin_create_pkg plumbing_pub_sub roscpp rospy std_msgs
在该功能包中的“src”目录中创建.cpp,这里命名为“demo01_pub.cpp”,表示信息的发布方
添加如下代码:
#include "ros/ros.h"
#include "std_msgs/String.h"/*
发布方实现:1. 包含头文件2. 初始化ROS节点3. 创建节点句柄4. 创建发布者对象5. 编写发布逻辑并发布数据
*/int main(int argc, char *argv[])
{// 2.初 始化ROS节点ros::init(argc,argv,"pub"); //pub为节点名称//3. 创建节点句柄ros::NodeHandle nh;//4. 创建发布者对象ros::Publisher pub = nh.advertise<std_msgs::String>("Topic1",10); //ros::Publisher pub:创建一个发布者句柄; nh.advertise:使用节点句柄来广播; <std_msgs::String>:指定发布数据的类型; Topic1:话题名称; 10:发布队列的大小;//5. 编写发布逻辑并发布数据std_msgs::String msg; // 创建被发布的消息while (ros::ok()){msg.data = "hello";pub.publish(msg);}return 0;
}
打开该功能包下的“CMakeLists.txt” ,设置如下代码,通过 add_executable 命令将一个或多个.cpp 文件编译生成一个可执行文件,再通过target_link_libraries 命令将 demo01_pub 这个可执行文件与它所依赖的库链接起来。
Ctrl+Shift+B编译代码,然后打开一个窗口输入“roscore”启动ROS核心
在新终端中输入“rosrun plumbing_pub_sub demo01_pub”来启动该节点
再新建一个窗口,输入“rostopic echo Topic1”,来打印“Topic1”话题的信息
如果我们希望设置发布频率,并统计发布消息的数量,可以增加如下代码:
#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>/*
发布方实现:1. 包含头文件2. 初始化ROS节点3. 创建节点句柄4. 创建发布者对象5. 编写发布逻辑并发布数据
*/int main(int argc, char *argv[])
{setlocale(LC_ALL, ""); // 解决中文乱码问题// 2.初 始化ROS节点ros::init(argc,argv,"pub"); //pub为节点名称//3. 创建节点句柄ros::NodeHandle nh;//4. 创建发布者对象ros::Publisher pub = nh.advertise<std_msgs::String>("Topic1",10); //ros::Publisher pub:创建一个发布者句柄; nh.advertise:使用节点句柄来广播; <std_msgs::String>:指定发布数据的类型; Topic1:话题名称; 10:发布队列的大小;//5. 编写发布逻辑并发布数据std_msgs::String msg; // 创建被发布的消息ros::Rate rate(10); // 设置发布速率为1s10条int count = 0;while (ros::ok()){std::stringstream ss;ss<< "hello --->"<<count; // 拼接字符串msg.data = ss.str();pub.publish(msg);count++;ROS_INFO("发布的数据是:%s", ss.str().c_str());rate.sleep();ros::spinOnce(); // 官方建议添加}return 0;
}
代码添加后,Ctrl+Shift+B编译。
此时我们再通过“rosrun plumbing_pub_sub demo01_pub”来发布信息时,效果如下:
3.1.2 订阅方
新建一个.cpp文件,表示消息的订阅方,这里命名为“demo02_sub.cpp”
添加如下代码:
#include "ros/ros.h"
#include "std_msgs/String.h"/*
订阅方实现:1. 包含头文件2. 初始化ROS节点3. 创建节点句柄4. 创建订阅者对象5. 处理订阅到的数据
*///5. 处理订阅到的数据
void call_doMsg(const std_msgs::String::ConstPtr &msg){ROS_INFO("订阅到的数据:%s", msg->data.c_str());
}int main(int argc, char *argv[])
{setlocale(LC_ALL, ""); // 解决中文乱码问题// 2. 初始化ROS节点ros::init(argc, argv, "sub");// 3. 创建节点句柄ros::NodeHandle nh;// 4. 创建订阅者对象ros::Subscriber sub = nh.subscribe("Topic1",10,call_doMsg); //Topic1是话题名称; 10是消息队列的大小; call_doMsg是回调函数;ros::spin();return 0;
}
在功能包的“CMakeLists.txt”中添加如下部分
Ctrl+Shift+B编译一下
在两个终端中分别启动功能包下的两个节点
rosrun plumbing_pub_sub demo01_pub
rosrun plumbing_pub_sub demo02_sub
运行效果如下:
3.1.3 计算图展示
在新终端窗口中输入“rqt_graph”打开计算图
可以看到发布方“pub”通过话题“Topic1”给订阅方“sub”传递信息
3.2 Python实现
3.2.1 发布方
在功能包中新创建一个文件夹“scripts”,用于存放Python源码
在“scripts”添加Python文件,这里命名为“py_pub.py”
添加如下代码:
import rospy
from std_msgs.msg import String"""
发布方实现:1. 导包2. 初始化ROS节点3. 创建发布者对象4. 编写发布逻辑并发布数据
"""if __name__ == "__main__":# 2. 初始化ROS节点rospy.init_node("pub") # pub为节点名称# 3. 创建发布者对象pub = rospy.Publisher("Topic2", String, queue_size=10)# 4. 编写发布逻辑并发布数据msg = String() # 创建被发布的消息rate = rospy.Rate(1) # 设置发布速率为1s1条count = 0while not rospy.is_shutdown(): # 节点没关闭就一直循环msg.data = f"hello {count}"pub.publish(msg)rospy.loginfo("发布的数据是:%s", msg.data) # 日志信息count+=1rate.sleep()
给“scripts”中的所有python文件添加可执行权限(注意路径)
chmod +x *.py
修改一下功能包中的“CMakeLists.txt”文件,如下:
编译一下
在两个终端窗口中分别输入如下指令,一个用来启动发布者,一个用于打印话题信息
rosrun plumbing_pub_sub py_pub.pyrostopic echo Topic2
此时效果如下:
3.2.2 订阅方
在“scripts”目录下新建一个python文件,这里命名为“py_sub.py”
添加如下代码:
#! /usr/bin/env python
import rospy
from std_msgs.msg import String"""
订阅方实现:1. 导包2. 初始化ROS节点3. 创建订阅者对象4. 处理订阅到的数据5. spin()
"""# 4. 处理订阅到的数据
def call_doMsg(msg):rospy.loginfo("我订阅的数据:%s", msg.data)if __name__ == "__main__":# 2. 初始化ROS节点rospy.init_node("sub")# 3. 创建订阅者对象sub = rospy.Subscriber("Topic2", String, call_doMsg, queue_size=10)# 5. spin()rospy.spin()
打开“CMakeLists.txt”,添加如下部分
编译一下,在终端中分别输入如下指令来启动发布和订阅
rosrun plumbing_pub_sub py_pub.pyrosrun plumbing_pub_sub py_sub.py
启动后效果如下:
可以看到订阅到的数据少了1条,这是因为订阅方还没有来得及注册,我们可以在发布数据前加一点休眠
编译后再运行可以看到,订阅的数据就没有少了
3.2.3 计算图展示
在发布和订阅节点工作期间我们通过“rqt_graph”启动计算图
可以看到界面如下
3.3 C++节点与Python节点通信
通过话题通信机制,可以让不同编程语言实现的节点进行通信。
将Python中订阅者的话题改为“Topic1” ,修改后编译一下
由于C++发布者也是将数据发布到话题“Topic1”中,因此我们可以直接启动C++的发布方与Python的订阅方,如下
rosrun plumbing_pub_sub demo01_pubrosrun plumbing_pub_sub py_sub.py
可以看到还是能够正常工作