【Autolabor初级教程】ROS机器人入门 第 2 章 ROS通信机制 笔记
ROS 中的基本通信机制主要有如下三种实现策略:
- 话题通信(发布订阅模式)
- 服务通信(请求响应模式)
- 参数服务器(参数共享模式)
2.1 话题通信
概念:以发布订阅的方式实现不同节点之间数据交互的通信模式
作用:用于不断更新的、少逻辑处理的数据传输场景
适用场景:话题通信适用于不断更新的数据传输相关的应用场景
理论模型
参考链接:视频讲得很通透
038话题通信_理论模型_Chapter2-ROS通信机制_哔哩哔哩_bilibili
话题通信基本操作(C++)
-
发布方
// 1.包含头文件 #include "ros/ros.h" #include "std_msgs/String.h" // ros中文本类型 #include <sstream> int main(int argc, char *argv[]) { // 2.初始化ros节点 ros::init(argc,argv,"pub"); // 3.创建节点句柄 ros::NodeHandle nh; // 4.创建发布者对象 第一个参数为发布的话题 ros::Publisher pub = nh.advertise<std_msgs::String>("pos",10); // 5.编写发布逻辑并发布数据 // 要求以10hz的频率发布数据,并且文本后添加编号 // 先创建被发布消息 std_msgs::String msg; // 发布频率 ros::Rate rate(10); // 该参数为指定频率 // 设置编号 int count = 0; // 编写循环,循环中发布数据 while(ros::ok()) { // 输出中文 setlocale(LC_ALL,""); count++; // 实现字符串拼接数字 // msg.data = "here"; std::stringstream ss; ss << "hello ---> " << count; msg.data = ss.str(); pub.publish(msg); ROS_INFO("发布的数据是:%s",ss.str().c_str()); // 根据前面制定的发送频率自动休眠 休眠时间 = 1/频率; rate.sleep(); } return 0; }
如果代码中无输出,验证此代码时,可以在新的终端输入
rostopic echo pos
,屏幕中会显示数据 -
订阅方
// 1.包含头文件 #include "ros/ros.h" #include "std_msgs/String.h" // ros中文本类型 void domsg(const std_msgs::String::ConstPtr &msg) { // 通过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.创建订阅者对象 第一个参数为订阅的话题,domsg为所进行的操作 ros::Subscriber sub = nh.subscribe("pos",10,domsg); // 5.处理订阅到的数据 // 6.循环读取接收的数据,并调用回调函数处理 // 如果要用回调函数必须用spin ros::spin(); return 0; }
同时执行两代码得到结果
可以发现,订阅者接收的信息有丢失(在launch文件中先开启的subscriber)
原因是在publisher发送信息的时候,还未在roscore注册完毕,导致数据丢失
解决方案:在循环前加入以下代码,延迟第一条信息的发送
// 程序休眠,防止节点还未在roscore中注册时就发送消息 ros::Duration(3).sleep();
可以看到接收正常
程序运行时,可以新建终端输入指令rqt_graph
,就可以看到计算图,帮助理解节点间关系
话题通信自定义msg
作用:类似于C语言的结构体
在自定义msg时,可以适用的字段类型有
话题通信自定义msg实现(C++)
-
自定义msg实现
-
功能包下新建
msg
目录,添加文件xxx.msg
(注意后缀) -
内容每行定义字段类型和字段名即可,回车换行
string name uint16 age float64 height
-
修改
package.xml
,添加以下内容 -
修改
CMakeLists.txt
(没有的内容增加,被注释的解开) -
编译
编译后会在devel/include生成对应的头文件
-
-
后续代码使用自定义msg的准备工作
配置
.vscode/c_cpp_properties.json
将自定义msg的头文件所在路径添加到
"includePath"
即可 -
发布者
// 1.包含头文件 #include "demo_pub_sub/person.h" #include "ros/ros.h" #include "demo_pub_sub/person.h" int main(int argc, char *argv[]) { setlocale(LC_ALL,""); // 2.初始化ros节点 ros::init(argc,argv,"person_pub"); // 3.创建节点句柄 ros::NodeHandle nh; // 4.创建发布者对象 ros::Publisher pub = nh.advertise<demo_pub_sub::person>("person",100); // 5.编写发布逻辑并发布数据 // 5-1.创建被发布的数据 demo_pub_sub::person person; person.age = 1; person.name = "haha"; person.height = 1.83; // 5-2.设置发布频率 ros::Rate rate(1); ros::Duration(3).sleep(); // 5-3.循环发布数据 while(ros::ok()) { pub.publish(person); ROS_INFO("我叫:%s,今年%d岁,高%.2f米", person.name.c_str(), person.age, person.height); person.age++; rate.sleep(); ros::spinOnce(); } return 0; }
在修改配置文件过程中,其他步骤与之前相同,额外增加一步
测试时若
rostopic echo "person"
报错,则在终端重新执行source ./devel/setup.bash
-
订阅者
// 1.包含头文件 #include "demo_pub_sub/person.h" #include "ros/ros.h" #include "demo_pub_sub/person.h" void doperson(const demo_pub_sub::person::ConstPtr& p) { ROS_INFO("接收到的信息:姓名->%s,年龄->%d,身高->%.2f",p->name.c_str(),p->age,p->height); } int main(int argc, char *argv[]) { setlocale(LC_ALL,""); // 2.初始化ros节点 ros::init(argc,argv,"person_sub"); // 3.创建节点句柄 ros::NodeHandle nh; // 4.创建订阅者对象 ros::Subscriber sub = nh.subscribe<demo_pub_sub::person>("person",100,doperson); // 5.处理订阅数据 // 6.调用spin()函数 ros::spin(); return 0; }
-
测试结果
2.2 服务通信
概念:以请求响应的方式实现不同节点之间数据交互的通信模式
作用:用于偶然的、对时时性有要求、有一定逻辑处理需求的数据传输场景
适用场景:服务通信适用于对时时性有要求、具有一定逻辑处理的应用场景
理论模型
参考链接:063服务通信_理论模型_Chapter2-ROS通信机制_哔哩哔哩_bilibili
服务通信自定义srv(C++)
-
自定义srv实现
-
功能包下新建
srv
目录,添加文件xxx.srv
(注意后缀) -
数据分成两部分,请求与响应,在 srv 文件中请求和响应使用
---
分割(srv
文件内的可用数据类型与msg
文件一致),回车换行# 客户请求 int32 num1 int32 num2 --- # 服务器响应 int32 sum
-
修改
package.xml
,添加以下内容(与自定义msg一样) -
修改
CMakeLists.txt
(只有图2略有区别) -
编译
编译后会在devel/include生成对应的头文件
-
-
后续代码使用自定义srv的准备工作
需要像之前自定义
msg
实现一样配置c_cpp_properies.json
文件,如果以前已经配置且没有变更工作空间,可以忽略,如果需要配置,配置方式与之前相同 -
服务器
// 1.包含头文件 #include "demo_server_client/add_int.h" #include "ros/ros.h" #include "demo_server_client/add_int.h" bool donums(demo_server_client::add_int::Request &request, demo_server_client::add_int::Response &response) { // 1.处理请求 int num1 = request.num1; int num2 = request.num2; ROS_INFO("这里是饲养员,让我偷偷告诉你%d加上%d等于多少!",num1,num2); // 2.组织响应 int sum = num1 + num2; response.sum = sum; ROS_INFO("现在你知道了吗?"); return true; } int main(int argc, char *argv[]) { setlocale(LC_ALL,""); // 2.初始化ros节点 ros::init(argc,argv,"gtt"); // 3.创建节点句柄 ros::NodeHandle nh; // 4.创建一个服务对象 // 第一个参数为话题名字,第二个参数为回调函数 ros::ServiceServer server = nh.advertiseService("speak",donums); ROS_INFO("饲养员已就位!"); // 5.处理请求并产生响应 // 6.调用spin()函数 ros::spin(); return 0; }
修改配置文件多修改一步
测试过程有两个容易出错的地方:
-
编译后记得执行
source ./devel/setup.bash
,否则会出现以下错误 -
测试指令
a. 首先键入
rosservice call speak(自己定义的话题名称)
,然后打一个空格,连续按两次tab
,终端就会变成下图b. 用键盘的左右按键移到两个数字中进行编辑即可
c. 按下回车终端中就会返回结果
-
-
客户端
// 1.包含头文件 #include "demo_server_client/add_int.h" #include "ros/ros.h" #include "demo_server_client/add_int.h" int main(int argc, char *argv[]) { setlocale(LC_ALL,""); // 2.初始化ros节点 ros::init(argc,argv,"zxs"); // 3.创建节点句柄 ros::NodeHandle nh; // 4.创建一个客户端对象 ros::ServiceClient client = nh.serviceClient<demo_server_client::add_int>("speak"); ROS_INFO("张臭臭已就位!"); // 5.提交请求并处理响应 demo_server_client::add_int zcc; // 5-1.组织请求 zcc.request.num1 = 2; zcc.request.num2 = 3; ROS_INFO("你好饲养员,请问%d加上%d等于多少",zcc.request.num1,zcc.request.num2); // 5-2.处理响应 bool flag = client.call(zcc); if(flag) { ROS_INFO("臭臭知道了,%d加上%d等于%d,所以我要",zcc.request.num1,zcc.request.num2,zcc.response.sum); while(zcc.response.sum--) { ROS_INFO("哼"); } }else { ROS_INFO("臭臭太笨了,臭臭不会呜呜呜"); } return 0; }
-
测试结果
-
对于客户端的优化实现(通过命令行输入参数,增加交互)
// 1.包含头文件 #include "demo_server_client/add_int.h" #include "ros/ros.h" #include "demo_server_client/add_int.h" // 优化实现(执行时不用launch) // 实现参数的动态提交 // 1.格式 rosrun xxx xxx num1 num2 // 2.节点执行时,需要获取命令中的参数,并组织进request int main(int argc, char *argv[]) { setlocale(LC_ALL,""); // 优化实现 if(argc != 3){ ROS_INFO("臭臭说的好像不对!"); return 1; } // 2.初始化ros节点 ros::init(argc,argv,"zxs"); // 3.创建节点句柄 ros::NodeHandle nh; // 4.创建一个客户端对象 ros::ServiceClient client = nh.serviceClient<demo_server_client::add_int>("speak"); ROS_INFO("张臭臭已就位!"); // 5.提交请求并处理响应 demo_server_client::add_int zcc; // 5-1.组织请求 zcc.request.num1 = atoi(argv[1]); // atoi()转化为整数 zcc.request.num2 = atoi(argv[2]); ROS_INFO("你好饲养员,请问%d加上%d等于多少",zcc.request.num1,zcc.request.num2); // 5-2.处理响应 bool flag = client.call(zcc); if(flag) { ROS_INFO("臭臭知道了,%d加上%d等于%d,所以我要",zcc.request.num1,zcc.request.num2,zcc.response.sum); while(zcc.response.sum--) { ROS_INFO("哼"); } }else { ROS_INFO("臭臭太笨了,臭臭不会呜呜呜"); } return 0; }
运行结果:
成功:
失败:
Note:
先启动客户端,未启动服务端如下图
若要进行优化(当服务器未启动,客户端先启动时,将客户端挂起,而非进行错误处理,当服务端启动成功后才会继续执行)
执行以下代码中的一句
client.waitForExistence();
ros::service::waitForService("xxx(话题)");
2.3 参数服务器
概念:以共享的方式实现不同节点之间数据交互的通信模式
作用:存储一些多节点共享的数据,类似于全局变量
适用场景:一般适用于存在数据共享的一些应用场景
理论模型
参考链接:077参数服务器_理论模型_Chapter2-ROS通信机制_哔哩哔哩_bilibili
参数可使用的数据类型
- 32-bit integers
- booleans
- strings
- doubles
- iso8601 dates(时间的表示方法)
- lists
- base64-encoded binary data
- 字典
注意:参数服务器不是为高性能而设计的,因此最好用于存储静态的非二进制的简单数据
对于这句话的理解(来自gpt)可能这句话的意思是直接存放解析好的有意义的数据,比如说电机回传往往是0-65535之间的二进制数据,这里的意思是建议存储对应的解析到0-360的数据
参数操作
参数服务器新增(修改)参数
主要依赖以下两个函数:
nh.setParam("键",值);
ros::param::set("键",值);
初次设置键为新增,再次调用相同键为修改
#include "ros/ros.h"
/*
实现参数的新增与修改
首先设置机器人的共享参数,类型,名字,体重(86kg)
再修改体重(80kg)
实现:
ros::NodeHandle setParam()
ros::param set()
*/
int main(int argc, char *argv[])
{
// 初始化ros节点
ros::init(argc,argv,"set_param");
// 创建ros节点句柄
ros::NodeHandle nh;
// 参数增
// 方案1:nh
nh.setParam("type","pig");
nh.setParam("name","zxs");
nh.setParam("weight",86);
// 方案2:ros::param
ros::param::set("type_param","pig");
ros::param::set("name_param","zxs");
ros::param::set("weight_param",86);
//参数改
// 方案1:nh
nh.setParam("weight",80);
// 方案2:ros::param
ros::param::set("weight_param",80);
return 0;
}
运行结果:
参数服务器获取参数
常用函数:
ros::NodeHandle
param(键,默认值)
存在,返回对应结果,否则返回默认值
getParam(键,存储结果的变量)
存在,返回 true,且将值赋值给参数2
若果键不存在,那么返回值为 false,且不为参数2赋值
getParamCached(键,存储结果的变量)--提高变量获取效率
存在,返回 true,且将值赋值给参数2
若果键不存在,那么返回值为 false,且不为参数2赋值
getParamNames(std::vector<std::string>)
获取所有的键,并存储在参数 vector 中
hasParam(键)
是否包含某个键,存在返回 true,否则返回 false
searchParam(参数1,参数2)
搜索键,参数1是被搜索的键,参数2存储搜索结果的变量(实测参数2存储的是被搜索的键的名字)
运行代码:
#include "ros/ros.h"
/*
演示参数查询
实现:
ros::NodeHandle
ros::param
*/
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
// 初始化ros节点
ros::init(argc,argv,"set_param");
// 创建ros节点句柄
ros::NodeHandle nh;
// nodehandle
// 1.param
double weight = nh.param("weight",100);
ROS_INFO("weight = %.2f",weight);
// 2.getParam
double getParam_test = 0;
bool result = nh.getParam("weight",getParam_test);
if(result)
{
ROS_INFO("zxs的体重是%.2fkg",getParam_test);
}else{
ROS_INFO("被查询的变量不存在");
}
// 3.getParamNames
std::vector<std::string> names;
nh.getParamNames(names);
for(auto &&name : names)
{
ROS_INFO("遍历的元素名字:%s",name.c_str());
}
// 4.hasParam
bool flag1 = nh.hasParam("weight");
bool flag2 = nh.hasParam("pig");
ROS_INFO("weight存在吗?存在:1,不存在:0 回答:%d",flag1);
ROS_INFO("pig存在吗?存在:1,不存在:0 回答:%d",flag2);
// 5.searchParam
std::string key;
nh.searchParam("name",key);
ROS_INFO("pig的名字是%s",key.c_str());
return 0;
}
- param(键,默认值)
与demo_param_set一起运行时:
单独运行时:
- getParam(键,存储结果的变量)
与demo_param_set一起运行时:
单独运行时:
分析可以得知,虽然在前面定义了参数weight,但是查询结果仍然返回false,证明这两个weight并非同一个weight,仅仅是声明的话,并没有将其放入参数服务器
- getParamCached(键,存储结果的变量)–提高变量获取效率
效果与getParam相同,这里不做演示
提升效率的原理:getParam的工作原理是不管这个参数之前是否调用过,在查询时都会从服务器获取查询,getParamCached回首先在本地的缓冲区查看是否过,如果已经调用过参数,会直接从本地查询,提高效率
- getParamNames(std::vectorstd::string)
- hasParam(键)
- searchParam(参数1,参数2)
与demo_param_set一起运行时:
实测返回的是键名,而非键的值
单独运行时:
未返回结果
PS:ros::param
与ros::NodeHandle
对应的函数及用法是一致的,就不再展开
- param对应param
- get对应getParam
- getCached对应getParamCached
- getParamNames对应getParamNames
- has对应hasParam
- search对应searchParam
参数服务器删除参数
#include "ros/ros.h"
/*
演示参数的删除
实现:
ros::NodeHandle delParam()
ros::param del()
*/
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
// 初始化ros节点
ros::init(argc,argv,"set_param");
// 创建ros节点句柄
ros::NodeHandle nh;
// 删除
// 方案1:nh
bool flag1 = nh.deleteParam("type");
if(flag1){
ROS_INFO("方案1删除成功!");
}else{
ROS_INFO("方案1删除失败!");
}
// 方案2:ros::param
bool flag2 = ros::param::del("type_param");
if(flag1){
ROS_INFO("方案2删除成功!");
}else{
ROS_INFO("方案2删除失败!");
}
return 0;
}
初次运行删除成功,再次运行删除失败(已被删除)
2.4 通信机制比较
参数服务器是一种数据共享机制,可以在不同的节点之间共享数据
话题通信与服务通信是在不同的节点之间传递数据的
话题通信与服务通信比较
A.都涉及四个要素:
- 要素1: 消息的发布方/客户端(Publisher/Client)
- 要素2: 消息的订阅方/服务端(Subscriber/Server)
- 要素3: 话题名称(Topic/Service)
- 要素4: 数据载体(msg/srv)
B.对比
服务同步性指服务端和客户端基于请求响应的模式,按照顺序依次处理信息(服务端处理完,客户端才继续发送)。
话题异步性指发布方和订阅方在处理信息时没有严格顺序要求,并非只有订阅方处理完信息发布方才可继续发送。
2.5 常用命令
http://wiki.ros.org/ROS/CommandLineTools
文件操作命令是静态的,操作的是磁盘上的文件,而以下命令是动态的,在ROS程序启动后,可以动态的获取运行中的节点或参数的相关信息。
常用命令类型:
- rosnode : 操作节点
- rostopic : 操作话题
- rosservice : 操作服务
- rosmsg : 操作msg消息
- rossrv : 操作srv消息
- rosparam : 操作参数
ps:感觉他们的关系是
rostopic
和rosmsg
对应话题通信
rossrervice
和rossrv
对应服务通信
rostopic
和rossrervice
对应活动
rosmsg
和rossrv
对应消息
2.5.1 rosnode
rosnode ping 测试到节点的连接状态
rosnode list 列出活动节点
rosnode info 打印节点信息
rosnode machine 列出指定设备上节点
rosnode kill 杀死某个节点
rosnode cleanup 清除不可连接的节点
2.5.2 rostopic
rostopic bw 显示主题使用的带宽
rostopic delay 显示带有 header 的主题延迟
rostopic echo 打印消息到屏幕
rostopic find 根据类型查找主题
rostopic hz 显示主题的发布频率
rostopic info 显示主题相关信息
rostopic list 显示所有活动状态下的主题
rostopic pub 将数据发布到主题
rostopic type 打印主题类型
eg:rostopic pub
rostopic pub /主题名称 消息类型 消息内容
rostopic pub -r 频率 /主题名称 消息类型 消息内容
tip:可以rostopic pub /主题名称
然后敲两次回车,自动补全后面的信息
2.5.3 rosmsg
rosmsg show 显示消息描述
rosmsg info 显示消息信息
rosmsg list 列出所有消息
rosmsg md5 显示 md5 加密后的消息
rosmsg package 显示某个功能包下的所有消息
rosmsg packages 列出包含消息的功能包
2.5.4 rosservice
rosservice args 打印服务参数
rosservice call 使用提供的参数调用服务
rosservice find 按照服务类型查找服务(这个应该是给服务类型打印出话题名字)
rosservice info 打印有关服务的信息
rosservice list 列出所有活动的服务
rosservice type 打印服务类型(这个应该是给话题名字打印出服务类型)
rosservice uri 打印服务的 ROSRPC uri
eg:rosservice find msg-type
推测这个msg-type的意思应该是定义的xxx.srv的名字,即服务的类型,可能需要加上包名以便于查找(rosservice find add_int
无响应)
2.5.5 rossrv
rossrv show 显示服务消息详情
rossrv info 显示服务消息相关信息
rossrv list 列出所有服务信息
rossrv md5 显示 md5 加密后的服务消息
rossrv package 显示某个包下所有服务消息
rossrv packages 显示包含服务消息的所有包
2.5.6 rosparam
rosparam set 设置参数
rosparam get 获取参数
rosparam load 从外部文件加载参数
rosparam dump 将参数写出到外部文件
rosparam delete 删除参数
rosparam list 列出所有参数