文章目录
1. 应用场景
话题通信是ROS中使用频率最高的一种通信模式,话题通信是基于发布订阅模式,也即:一个节点发布消息,另一个节点订阅该消息。话题通信的应用场景也极其广泛,比如下面一个常见场景:
- 机器人在执行导航功能,使用的传感器是激光雷达,机器人会采集激光雷达感知到的信息并计算,然后生成运动控制信息驱动机器人底盘运动。
在上述场景中,就不止一次使用到了话题通信。
- 以激光雷达信息的采集处理为例,在 ROS 中有一个节点需要时时的发布当前雷达采集到的数据,导航模块中也有节点会订阅并解析雷达数据。
- 再以运动消息的发布为例,导航模块会根据传感器采集的数据时时的计算出运动控制信息并发布给底盘,底盘也可以有一个节点订阅运动信息并最终转换成控制电机的脉冲信号。
以此类推,像雷达、摄像头、GPS… 等等一些传感器数据的采集,也都是使用了话题通信,换言之,话题通信适用于不断更新的数据传输相关的应用场景。
- 作用:用于不断更新的、少逻辑处理的数据传输场景。
2. 话题通信模型
- 这里的/image_data是话题,message是传递的话题内容
话题通信实现模型是比较复杂的,该模型如下图所示,该模型中涉及到三个角色:
- ROS Master (管理者)
- Talker (发布者)
- Listener (订阅者)
ROS Master 负责保管 Talker 和 Listener 注册的信息,并匹配话题相同的 Talker 与 Listener,帮助 Talker 与 Listener 建立连接,连接建立后,Talker 可以发布消息,且发布的消息会被 Listener 订阅。
- bar —— 话题,{foo:1234} —— 地址
我们可以把这个通讯当做一次相亲过程,master可以根据话题建立发布者和订阅者之间的连接,起到了一个媒介的作用,也就是相亲中的媒婆。相对应假设Talker是男方,Listener是女方。
- step0:男方(Talker)会在媒婆(ROS Master)这里发布一个征婚广告(advertise)广告上面写着"在上海有一套房"(话题),还有联系电话(RPC地址)。
- step1:同时女方(Listener)给媒婆(ROS Master)发布一个“征婚标准”(subscribe),比如要找一个“在上海有一套房”的男人(话题)。
- step2:媒婆发现,男方满足女方要求,即有共同话题,那么媒婆(ROS Mster)把男方(Talker)的电话(RPC地址)发从给女方(Listener)。
- step3:女方(Listener)就打电话给男方(Talker):听说你有房,我们聊一聊
- step4:男方(Talker)回复:好啊,电话不方便,加我微信(TCP地址)吧!
- step5:女方(Listener)加男方(Talker)微信(TCP地址)
- step6:两者建立通信,女方就能订阅到男方信息
有几点注意:
- 注意1:上述实现流程中,前五步使用的 RPC协议,最后两步使用的是 TCP 协议
- 注意2: Talker 与 Listener 的启动无先后顺序要求
- 注意3: Talker 与 Listener 都可以有多个
- 注意4: Talker 与 Listener 连接建立后,不再需要 ROS Master。也即,即便关闭ROSMaster,Talker 与 Listern 照常通信。
整个流程由以下步骤实现:
-
Talker注册
Talker启动后,会通过RPC在 ROS Master 中注册自身信息,包含所发布消息的话题名称。ROS Master 会将节点的注册信息加入到注册表中。 -
Listener注册
Listener启动后,也会通过RPC在 ROS Master 中注册自身信息,包含需要订阅消息的话题名。ROS Master 会将节点的注册信息加入到注册表中。 -
ROS Master实现信息匹配
ROS Master 会根据注册表中的信息匹配Talker 和 Listener,并通过 RPC 向 Listener 发送 Talker 的 RPC 地址信息。 -
Listener向Talker发送请求
Listener 根据接收到的 RPC 地址,通过 RPC 向 Talker 发送连接请求,传输订阅的话题名称、消息类型以及通信协议(TCP/UDP)。 -
Talker确认请求
Talker 接收到 Listener 的请求后,也是通过 RPC 向 Listener 确认连接信息,并发送自身的 TCP 地址信息。 -
Listener与Talker件里连接
Listener 根据步骤4 返回的消息使用 TCP 与 Talker 建立网络连接。 -
Talker向Listener发送消息
连接建立后,Talker 开始向 Listener 发布消息。
3. 话题通信基本操作(C++) 功能包 plumbing_pub_sub
3.1 需求-分析-流程
- 需求:编写发布订阅实现,要求发布方以10HZ(每秒10次)的频率发布文本消息,订阅方订阅消息并将消息内容打印输出
- 分析:在模型实现中,ROS master 不需要实现,而连接的建立也已经被封装了,需要关注的关键点有三个 —— 发布方、接收方、数据(此处为普通文本)
- 流程:编写发布方实现;编写订阅方实现;编辑配置文件;编译并执行
3.2 发布方 demo01_pub.cpp
#include"ros/ros.h"
#include"std_msgs/String.h"
#include<sstream>
/*
发布方实现:
1.包含头文件
ROS中文本类型--->std_msgs/String.h
2.初始化ROS节点
3.创建节点句柄
4.创建发布者对象
5.编写发布逻辑并发布数据
*/
int main(int argc,char *argv[])
{
// 解决中文乱码问题
setlocale(LC_ALL,"");
// 2.初始化ROS节点
ros::init(argc,argv,"erGouZi");//ergouzi是节点名称,相当于是相亲对象,这里的ergouzi就是talker
// 3.创建节点句柄
ros::NodeHandle nh;
// 4.创建发布者对象,fang是话题
ros::Publisher pub = nh.advertise<std_msgs::String>("fang",10);
//缓存10条数据,话题名称是fang,队列长10,由于网络阻塞,没有发出去,则存储在这里,最大容量是10,超出阈值后先放进去的先销毁
// 5.编写发布逻辑并发布数据
// 5.1 创建被发布的消息
std_msgs::String msg;
// 5.2 要求以10HZ 的频率发布数据,并且文本后添加编号
// 5.2.1 发布频率
ros::Rate rate(10);//每秒发布10次
// 5.2.2 设置编号
int count = 0;
// 订阅时,第一条数据丢失;原因: 发送第一条数据时, publisher 还未在 roscore 注册完毕
ros::Duration(3).sleep();//延迟3秒
// 5.3 编写循环,循环要发布的消息
while(ros::ok())//如果节点还活着,循环成立
{
count++;
//msg.data = "hello";
//实现字符串的拼接
std::stringstream ss;
ss << "hello ---> " << count;
msg.data = ss.str();
pub.publish(msg);
//添加日志:
ROS_INFO("发布的数据是:%s",msg.data.c_str());
rate.sleep();// 发布一次,睡0.1秒
ros::spinOnce();//每次循环完都回头一次,spinOnce()只调用一次回调函数,之后会继续往下执行,官方建议,处理回调函数
}
return 0;
}
- 在终端里打开文件,发布数据
roscore
cd demo01_ws/
source ./devel/setup.bash
rosrun plumbing_pub_sub demo01_pub
订阅数据
rostopic echo fang
3.3 订阅方 demo02_sub.cpp
#include "ros/ros.h"
#include"std_msgs/String.h"
/*
订阅方实现:
1.包含头文件
ROS中文本类型--->std_msgs/String.h
2.初始化ROS节点
3.创建节点句柄
4.创建订阅者对象
5.处理订阅到的数据
6.spin()函数
*/
void doMsq(const std_msgs::String::ConstPtr &msg)//std_msgs String类型常量指针的引用,保证传入的实参是常量
{
//通过msg获取并且操作订阅到的数据
ROS_INFO("翠花订阅的数据:%s",msg->data.c_str());//指针的引用
// ROS_INFO("我听见:%s",(*msg_p).data.c_str());
}
int main(int argc, char *argv[])
{
//解决乱码问题
setlocale(LC_ALL,"");
// 2.初始化ROS节点
ros::init(argc,argv,"cuiHua");//节点名称具有唯一性
// 3.创建节点句柄
ros::NodeHandle nh;
// 4.创建订阅者对象
ros::Subscriber sub = nh.subscribe("fang",10,doMsq);//fang是两者的共同话题,doMsq是回调函数
// 5.处理订阅到的数据
ros::spin();//spin意思是回头,main函数从上往下一次执行,回头调用doMsq函数,doMsq需要被频繁执行
return 0;
}
3.4 配置 CMakeLists.txt(未自定义消息)
add_executable(demo01_pub src/demo01_pub.cpp)
add_executable(demo01_sub src/demo01_sub.cpp)
target_link_libraries(demo01_pub
${catkin_LIBRARIES}
)
target_link_libraries(demo01_sub
${catkin_LIBRARIES}
)
3.5 编译执行(打开终端)
- Ctrl + Shift +B
- 启动 roscore(窗口1)
roscore
- 启动发布节点(窗口2)
cd demo01_ws/
source ./devel/setup.bash
rosrun plumbing_pub_sub demo01_pub
- 启动订阅节点(窗口3)
cd demo01_ws/
source ./devel/setup.bash
rosrun plumbing_pub_sub demo02_sub
- 启动计算图模型rqt_graph(窗口4)
4. 话题通信基本操作(Python) 功能包 plumbing_pub_sub
4.1 需求-分析-流程
- 需求:编写发布订阅实现,要求发布方以10HZ(每秒10次)的频率发布文本消息,订阅方订阅消息并将消息内容打印输出
- 分析:在模型实现中,ROS master 不需要实现,而连接的建立也已经被封装了,需要关注的关键点有三个 —— 发布方、接收方、数据(此处为普通文本)
- 流程:编写发布方实现;编写订阅方实现;为python文件添加可执行权限;编辑配置文件;编译并执行
4.2 发布方 demo01_pub_p.py
#! /usr/bin/env python
import rospy
from std_msgs.msg import String # 发布的消息类型
"""
使用python实现消息发布:
1.导包;
2.初始化ros节点
3.创建发布者对象
4.编写发布逻辑并发布数据
"""
if __name__ == "__main__":
# 2.初始化ros节点
rospy.init_node("sanDai") # 传入节点名称
# 3.创建发布者对象
pub = rospy.Publisher("che",String, queue_size = 10) # che是话题名称,类型是String,消息队列大小为10
# 4.编写发布逻辑并发布数据
# 4.1 创建数据类型
msg = String()
# 4.2 指定发布频率
rate = rospy.Rate(1)
# 4.3 设置计数器
count = 0
# 休眠3s,完成在master下面的注册
rospy.sleep(3)
# 4.4 循环发布数据
while not rospy.is_shutdown(): # 判断当前节点是否已经关闭,没有关闭则发布数据
count += 1 # 如果要将count追加到hello后面,则需要将其变为字符串
msg.data = "hello" + str(count)
# 4.5 发布数据
pub.publish(msg)
# 4.6 添加日志输出
rospy.loginfo("发布的数据:%s",msg.data)
rate.sleep()
4.3 订阅方 demo02_sub_p.py
#! /usr/bin/env python
import rospy
from std_msgs.msg import String
"""
订阅实现流程:
1.导包
2.初始化ROS节点
3.创建订阅者对象
4.回调函数处理数据
5.spin()
"""
def doMsg(msg): #将订阅到的数据传进来
rospy.loginfo("订阅的数据:%s",msg.data) #data是数据
if __name__ == "__main__":
# 2.初始化ROS节点
rospy.init_node("huaHua")
# 3.创建订阅者对象
sub = rospy.Subscriber("che",String,doMsg,queue_size = 10)
# sub = rospy.Subscriber("fang",String,doMsg,queue_size = 10) 跨语言通信
# 4.回调函数处理数据
# 5.spin()
rospy.spin()
4.4 添加可执行权限
终端下进入 scripts 执行
chmod +x *.py
ll
4.5 配置 CMakeLists.txt
catkin_install_python(PROGRAMS
scripts/demo01_pub_p.py
scripts/demo02_sub_p.py
DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
4.6 编译执行
- 启动 roscore
roscore
- 启动发布节点
cd demo01_ws/
source ./devel/setup.bash
rosrun plumbing_pub_sub demo01_pub_p.py
- 启动订阅节点
cd demo01_ws/
source ./devel/setup.bash
rosrun plumbing_pub_sub demo02_sub_p.py
5 话题通信自定义msg(重点) — 功能包 plumbing_pub_sub
在 ROS 通信协议中,数据载体是一个较为重要组成部分,ROS 中通过 std_msgs 封装了一些原生的数据类型,比如:String、Int32、Int64、Char、Bool、Empty… 但是,这些数据一般只包含一个 data 字段,结构的单一意味着功能上的局限性,当传输一些复杂的数据,比如: 激光雷达的信息… std_msgs 由于描述性较差而显得力不从心,这种场景下可以使用自定义的消息类型。
msgs只是简单的文本文件,每行具有字段类型和字段名称,可以使用的字段类型有:
- int8, int16, int32, int64 (或者无符号类型: uint*)
- float32, float64
- string
- time, duration
- other msg files
- variable-length array[] and fixed-length array[C]
ROS中还有一种特殊类型:Header,标头包含时间戳和ROS中常用的坐标帧信息。会经常看到msg文件的第一行具有Header标头。
- 需求:创建自定义消息,该消息包含人的信息:姓名、身高、年龄等。
- 流程:1 按照固定格式创建 msg 文件;2 编辑配置文件;3 编译生成可以被 Python 或 C++ 调用的中间文件
5.1 定义msg文件
- 功能包下新建 msg 目录,添加文件 Person.msg
string name
int32 age
float32 height
5.2 编辑配置文件
- package.xml中添加编译依赖与执行依赖
<buildtool_depend>catkin</buildtool_depend>
<build_depend>roscpp</build_depend>
<build_depend>rospy</build_depend>
<build_depend>std_msgs</build_depend>
<build_depend>message_generation</build_depend>(新添加)
<build_export_depend>roscpp</build_export_depend>
<build_export_depend>rospy</build_export_depend>
<build_export_depend>std_msgs</build_export_depend>
<exec_depend>roscpp</exec_depend>
<exec_depend>rospy</exec_depend>
<exec_depend>std_msgs</exec_depend>
<exec_depend>message_runtime</exec_depend>(新添加)
- CMakeLists.txt编辑 msg 相关配置
find_package(catkin REQUIRED COMPONENTS # 编译依赖
roscpp
rospy
std_msgs
message_generation
)
# 需要加入 message_generation,必须有 std_msgs
## 配置 msg 源文件
add_message_files(
FILES
Person.msg
)
# 生成消息时依赖于 std_msgs
generate_messages(
DEPENDENCIES
std_msgs
)
# 执行依赖
catkin_package(
# INCLUDE_DIRS include
# LIBRARIES demo02_talker_listener
CATKIN_DEPENDS roscpp rospy std_msgs message_runtime
# DEPENDS system_lib
)
5.3 编译运行
- ctrl + shift + b
C++ 需要调用的中间文件(…/工作空间/devel/include/包名/xxx.h)
- Python 需要调用的中间文件(…/工作空间/devel/lib/python3/dist-packages/包名/msg)
6 话题通信自定义msg调用(C++)
6.1 vscode 配置(重要)
为了方便代码提示以及避免误抛异常,需要先配置 vscode,将前面生成的 head 文件路径配置进 c_cpp_properties.json 的 includepath属性。防止导包出现异常,可以给出正常提示
- 步骤1:右击文件devel下的include文件,选择在终端中打开
- 步骤2:输入pwd
- 步骤3:复制文字到json文件中
{
"configurations": [
{
"browse": {
"databaseFilename": "",
"limitSymbolsToIncludedHeaders": true
},
"includePath": [
"/opt/ros/melodic/include/**",
"/usr/include/**",
"/home/rosmelodic/demo01_ws/devel/include/**"(新添加)
],
"name": "ROS",
"intelliSenseMode": "gcc-x64",
"compilerPath": "/usr/bin/gcc",
"cStandard": "gnu11",
"cppStandard": "c++17"
}
],
"version": 4
}
6.2 发布方 demo03_pub_person.cpp
#include"ros/ros.h"
#include"plumbing_pub_sub/Person.h"
/*
发布方:发布人的消息
1.包含头文件
#include"plumbing_pub_sub/Person.h"
2.初始化ROS节点
3.创建节点句柄
4.创建发布者对象
5.编写发布逻辑,发布数据
*/
int main(int argc, char *argv[])
{
// 解决中文乱码问题
setlocale(LC_ALL,"");
// 添加日志
ROS_INFO("这是消息发布方");
// 2.初始化ROS节点
ros::init(argc, argv,"banZhuRen");
// 3.创建节点句柄
ros::NodeHandle nh;
// 4.创建发布者对象
// 消息类型是功能包plumbing_pub_sub下面的Person
ros::Publisher pub = nh.advertise<plumbing_pub_sub::Person>("liaoTian",10);
// 5.编写发布逻辑,发布数据
// 5.1 创建被发布的数据
plumbing_pub_sub::Person person;
person.name = "张三";
person.age = 1;
person.height = 1.73;
// 5.2 设置发布频率
ros::Rate rate(1);
// 5.3 循环发布数据
while(ros::ok())
{
// 修改数据
person.age += 1;
// 核心:数据发布
pub.publish(person);
// 打印消息
ROS_INFO("发布的消息:%s,%d,%.2f",person.name.c_str(),person.age,person.height);
// 休眠
rate.sleep();
// 建议:使用回调函数
ros::spinOnce();
}
return 0;
}
6.3 订阅方 demo04_sub_person.cpp
#include"ros/ros.h"
#include"plumbing_pub_sub/Person.h"
/*
订阅方:订阅的消息
1.包含头文件
#include"plumbing_pub_sub/Person.h"
2.初始化ROS节点
3.创建节点句柄
4.创建订阅者对象
5.处理订阅的数据
6.spin()
*/
void doPerson(const plumbing_pub_sub::Person::ConstPtr& person){
ROS_INFO("订阅人的信息:%s,%d,%.2f",person->name.c_str(),person->age,person->height);
}
int main(int argc, char *argv[])
{
// 中文乱码问题
setlocale(LC_ALL,"");
// 添加日志
ROS_INFO("订阅方实现");
// 2.初始化ROS节点
ros::init(argc,argv,"jiaZhang");
// 3.创建节点句柄
ros::NodeHandle nh;
// 4.创建订阅者对象
ros::Subscriber sub = nh.subscribe("liaoTian",10,doPerson);
// 5.处理订阅的数据
// 6.spin()
ros::spin();
return 0;
}
6.4 配置 CMakeLists.txt(自定义消息,添加add_dependencies)
需要添加 add_dependencies 用以设置所依赖的消息相关的中间文件
add_executable(demo03_pub_person src/demo03_pub_person.cpp)
add_executable(demo04_sub_person src/demo04_sub_person.cpp)
add_dependencies(demo03_pub_person ${PROJECT_NAME}_generate_messages_cpp)
add_dependencies(demo04_sub_person ${PROJECT_NAME}_generate_messages_cpp)
target_link_libraries(demo03_pub_person
${catkin_LIBRARIES}
)
target_link_libraries(demo04_sub_person
${catkin_LIBRARIES}
)
6.5 编译执行
- 启动 roscore(窗口1)
roscore
- 启动发布节点(窗口2)
cd demo01_ws/
source ./devel/setup.bash
rosrun plumbing_pub_sub demo03_pub_person
- 启动订阅节点(窗口3)
cd demo01_ws/
source ./devel/setup.bash
rosrun plumbing_pub_sub demo04_sub_person
- rqt_graph(窗口4)
7 话题通信自定义msg调用(python)
7.1 vscode 配置(重要)
为了方便代码提示以及误抛异常,需要先配置 vscode,将前面生成的 python 文件路径配置进 settings.json.
- 步骤1:右击文件devel下devel ——> lib ——> python3文件,选择在终端中打开
- 步骤2:输入pwd
- 步骤3:复制文字到json文件中
{
"python.autoComplete.extraPaths": [
"/opt/ros/noetic/lib/python3/dist-packages",
"/home/rosdemo/demo01_ws/devel/lib/python3/dist-packages"(新添加)
]
}
7.2 发布方 demo03_pub_person_p.py
#! /usr/bin/env python
"""
发布方:发布人的消息
1.导包
2.初始化ROS节点
3.创建发布者对象
4.组织发布逻辑并发布数据
"""
import rospy
from plumbing_pub_sub.msg import Person
if __name__ == "__main__":
# 2.初始化ROS节点
rospy.init_node("daMa")
# 3.创建发布者对象
pub = rospy.Publisher("jiaoSheTou",Person,queue_size=10)
# 4.组织发布逻辑并发布数据
# 4.1 创建Person
p = Person()
p.name = "奥特曼"
p.age = 8
p.height = 1.85
# 4.2 创建Rate对象
rate = rospy.Rate(1)
# 4.3 循环发布数据
while not rospy.is_shutdown():
pub.publish(p)
rospy.loginfo("发布的消息:%s,%d,%.2f",p.name,p.age,p.height)
rate.sleep()
7.3 订阅方 demo04_sub_person_p.py
#! /usr/bin/env python
"""
订阅方:订阅人的消息
1.导包
2.初始化ROS节点
3.创建订阅者对象
4.处理订阅的数据
5.spin()
"""
import rospy
from plumbing_pub_sub.msg import Person
def doPerson(p):
rospy.loginfo("小伙子的数据:%s,%d,%.2f",p.name,p.age,p.height)
if __name__ == "__main__":
# 2.初始化ROS节点
rospy.init_node("daYe")
# 3.创建订阅者对象
sub = rospy.Subscriber("jiaoSheTou",Person,doPerson,queue_size=10)
# 4.处理订阅的数据
# 5.spin()
rospy.spin()
7.4 添加可执行权限
scripts文件右击,在终端中打开
终端下进入 scripts 执行
chmod +x *.py
ll
7.5 配置 CMakeLists.txt
catkin_install_python(PROGRAMS
scripts/demo03_pub_p.py
scripts/demo04_sub_p.py
DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
7.6 编译执行
- ctrl + shift + B
- 启动 roscore(窗口1)
roscore
- 启动发布节点(窗口2)
cd demo01_ws/
source ./devel/setup.bash
rosrun plumbing_pub_sub demo03_pub_p.py
- 启动订阅节点(窗口3)
cd demo01_ws/
source ./devel/setup.bash
rosrun plumbing_pub_sub demo04_sub_p.py