目录
话题通信
话题通信
话题通信C++实现
自定义msg
服务通信
服务通信模型
自定义srv
参数服务器
理论模型
新增参数
获取参数
删除参数
话题通信是ROS中使用频率最高的一种通信模式,话题通信是基于发布订阅模式的,也即:一个节点发布消息,另一个节点订阅该消息。
以发布订阅的方式实现不同节点之间数据交互的通信模式。用于不断更新的、少逻辑处理的数据传输场景。
话题通信模型中涉及到三个角色:
0.Talker注册
Talker启动后,会通过RPC在 ROS Master 中注册自身信息,其中包含所发布消息的话题名称。ROS Master 会将节点的注册信息加入到注册表中。
1.Listener注册
Listener启动后,也会通过RPC在 ROS Master 中注册自身信息,包含需要订阅消息的话题名。ROS Master 会将节点的注册信息加入到注册表中。
2.ROS Master实现信息匹配
ROS Master 会根据注册表中的信息匹配Talker 和 Listener,并通过 RPC 向 Listener 发送 Talker 的 RPC 地址信息。
3.Listener向Talker发送请求
Listener 根据接收到的 RPC 地址,通过 RPC 向 Talker 发送连接请求,传输订阅的话题名称、消息类型以及通信协议(TCP/UDP)。
4.Talker确认请求
Talker 接收到 Listener 的请求后,也是通过 RPC 向 Listener 确认连接信息,并发送自身的 TCP 地址信息。
5.Listener与Talker件里连接
Listener 根据步骤4 返回的消息使用 TCP 与 Talker 建立网络连接。
6.Talker向Listener发送消息
连接建立后,Talker 开始向 Listener 发布消息。
注意1:上述实现流程中,前五步使用的 RPC协议,最后两步使用的是 TCP 协议
注意2: Talker 与 Listener 的启动无先后顺序要求
注意3: Talker 与 Listener 都可以有多个
注意4: Talker 与 Listener 连接建立后,不再需要 ROS Master。也即,即便关闭ROS Master,Talker 与 Listern 照常通信。
src右键Create Catkin Pakage 输入包名plumbing_pub_sub回车,输入依赖roscpp rospy std_msgs
1.发布方
#include "ros/ros.h"
#include "std_msgs/String.h" //普通文本类型的消息
#include
int main(int argc, char *argv[])
{
/*
发布方实现:
1.包含头文件
ROS中文本类型 --> std_msgs/String.h
2.初始化 ROS 节点;
3.创建节点句柄;
4.创建发布者对象;
5.编写发布逻辑并发布数据。
*/
//解决中文乱码
setlocale(LC_ALL,"");
//2.初始化 ROS 节点;erGouZi是节点名称
ros::init(argc,argv,"erGouZi");
//3.创建节点句柄;
ros::NodeHandle nh;
//4.创建发布者对象;话题一致 。队列中最大保存的消息数,超出此阀值时,先进的先销毁(时间早的先销毁)
ros::Publisher pub = nh.advertise("fang",10);
//5.编写发布逻辑并发布数据。要求以10hz的频率发布数据,并且文本后添加编号。
//先创建被发布的消息
std_msgs::String msg;
//发布频率,1s/次
ros::Rate rate(2);
//设置编号,消息计数器
int count = 0;
//延时3秒发布,避免订阅者收不到前几条消息。
ros::Duration(3).sleep();
//编写循环,循环中发布数据
while (ros::ok())
{
count++;
//msg.data = "hello";
//实现字符串拼接数字
std::stringstream ss;
ss << "hello -->" << count;
msg.data = ss.str();
pub.publish(msg);//发布消息
//添加日志:
ROS_INFO("发布的数据是:%s",ss.str().c_str());
rate.sleep();//根据前面制定的发送贫频率自动休眠 休眠时间 = 1/频率;
ros::spinOnce();//处理回调函数
}
return 0;
}
测试:配置CMake文件,编译,测试。
roscore
cd demo03_ws/
source ./devel/setup.bash
rosrun plumbing_pub_sub demo01_pub
rostopic echo fang
2.订阅方
#include "ros/ros.h"
#include "std_msgs/String.h" //普通文本类型的消息
//传入的消息是std_msgs,String类型的,常量指针引用
void doMsg(const std_msgs::String::ConstPtr &msg){
//通过msg获取并操作订阅到的数据
ROS_INFO("翠花订阅的数据:%s",msg->data.c_str());
}
int main(int argc, char *argv[])
{
/*
发布方实现:
1.包含头文件
ROS中文本类型 --> std_msgs/String.h
2.初始化 ROS 节点;
3.创建节点句柄;
4.创建订阅者对象;
5.处理订阅到的数据。
6.spin()函数
*/
//解决中文乱码
setlocale(LC_ALL,"");
//2.初始化 ROS 节点;cuiHua是节点名称
ros::init(argc,argv,"cuiHua");
//3.创建节点句柄;
ros::NodeHandle nh;
//4.创建订阅者对象; doMsg是回调函数
ros::Subscriber sub = nh.subscribe("fang",10,doMsg);
//5.处理订阅到的数据。
ros::spin();
return 0;
}
3.配置 CMakeLists.txt
编译运行
roscore
cd demo03_ws/
source ./devel/setup.bash
rosrun plumbing_pub_sub demo01_pub
cd demo03_ws/
source ./devel/setup.bash
rosrun plumbing_pub_sub demo02_sub
1.定义msgs文件。demo03/src/plumbing_pub_sub/msg/Person.msg
2.编写package.xml文件
3.编写CMake文件
Person.msg是文件名
4.编译后的中间文件
C++ 需要调用的中间文件(.../工作空间/devel/include/包名/Perso.h)
Python 需要调用的中间文件(.../工作空间/devel/lib/python3/dist-packages/包名/msg)
5.终端打开,pwd 复制
c_cpp文件,加入路径
6.编写发布者cpp文件
#include "ros/ros.h"
#include "plumbing_pub_sub/Person.h"
/*
发布方:发布人消息
1.包含头文件
ROS中文本类型 --> std_msgs/String.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.创建发布者对象;
ros::Publisher pub = nh.advertise("liaotian",10);
// 5.编写发布逻辑并发布数据。
// 5-1.创建被发布的数据。
plumbing_pub_sub::Person person;
person.name = "张三";
person.age = 18;
person.height = 1.73;
// 5-2.设置发布频率
ros::Rate rate(1);
//延时3秒发布,避免订阅者收不到前几条消息。
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 += 1;
//休眠
rate.sleep();
//
ros::spinOnce();
}
return 0;
}
7.编写订阅者cpp文件,注意回调函数。
#include "ros/ros.h"
#include "plumbing_pub_sub/Person.h"
/*
发布方:发布人消息
1.包含头文件
ROS中文本类型 --> std_msgs/String.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;
}
8.配置CMake文件
9.编译 ctrl+shift+B
roscore
cd demo03_ws/
source ./devel/setup.bash
rosrun plumbing_pub_sub demo03_pub_person
cd demo03_ws/
source ./devel/setup.bash
rosrun plumbing_pub_sub demo04_sub_person
计算图 rqt_graph
服务通信也是ROS中一种极其常用的通信模式,服务通信是基于请求响应模式的,是一种应答机制。也即: 一个节点A向另一个节点B发送请求,B接收处理请求并产生响应结果返回给A
以请求响应的方式实现不同节点之间数据交互的通信模式。用于偶然的、对时时性有要求、有一定逻辑处理需求的数据传输场景。
整个流程由以下步骤实现:
0.Server注册
Server 启动后,会通过RPC在 ROS Master 中注册自身信息,其中包含提供的服务的名称。ROS Master 会将节点的注册信息加入到注册表中。
1.Client注册
Client 启动后,也会通过RPC在 ROS Master 中注册自身信息,包含需要请求的服务的名称。ROS Master 会将节点的注册信息加入到注册表中。
2.ROS Master实现信息匹配
ROS Master 会根据注册表中的信息匹配Server和 Client,并通过 RPC 向 Client 发送 Server 的 TCP 地址信息。
3.Client发送请求
Client 根据步骤2 响应的信息,使用 TCP 与 Server 建立网络连接,并发送请求数据。
4.Server发送响应
Server 接收、解析请求的数据,并产生响应结果返回给 Client。
注意:
1.客户端请求被处理时,需要保证服务器已经启动;
2.服务端和客户端都可以存在多个。
服务通信中,数据分成两部分,请求与响应,在 srv 文件中请求和响应使用---
分割,
新建plumbing_server_client 功能包下新建 srv 目录,添加 AddInts.srv 文件
1.书写AddInts.srv文件
int32 num1
int32 num2
---
int32 sum
2.配置xml文件
配置CMake文件
3.需要像之前自定义 msg 实现一样配置c_cpp_properies.json 文件(加入路径)如果以前已经配置且没有变更工作空间,可以忽略,如果需要配置,配置方式与之前相同。
4.编写服务端cpp文件
#include "ros/ros.h"
#include "plumbing_server_client/AddInts.h"
/*
服务端实现:解析客户端所提交的数据,并运算再产生响应。
1.包含头文件
2.初始化ROS节点
3.创建节点句柄
4.创建一个服务对象
5.处理请求并产生响应
6.spin()
*/
bool doNums(plumbing_server_client::AddInts::Request& request,
plumbing_server_client::AddInts::Response& response){
// 1.处理请求;
int num1 = request.num1;
int num2 = request.num2;
ROS_INFO("收到的请求数据:num1 = %d,num2 = %d",num1,num2);
// 2.组织响应;
int sum = num1 + num2;
response.sum = sum;
ROS_INFO("求和结果:sum = %d",sum );
return true;
}
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
// 2.初始化 ROS 节点;
ros::init(argc,argv,"AddInts_Server");
// 3.创建节点句柄;
ros::NodeHandle nh;
// 4.创建一个服务对象
ros::ServiceServer server = nh.advertiseService("AddInts",doNums);
ROS_INFO("服务已经启动...");
// 5.处理请求并产生响应
// 6.spin()
ros::spin();
return 0;
}
5.
配置CMake文件
编译,测试
roscore
cd demo03
source ./devel/setup.bash
rosrun plumbing_server_client demo01_server
cd demo03
source ./devel/setup.bash
rosservice call AddInts (Tab补齐,更改数字,回车)
6.编写客户端cpp文件
#include "ros/ros.h"
#include "plumbing_server_client/AddInts.h"
/*
服务端实现:提交两个整数,并处理响应的结果。
1.包含头文件
2.初始化ROS节点
3.创建节点句柄
4.创建一个客户端对象
5.提交请求并处理响应
实现参数的动态提交:
1.格式:ros xxx xxx 12 34. 此时argc包含三个参数--文件名 12 34
2.节点执行时,需要获取命令中的参数,并组织进 request
问题:
如果先启动客户端,请求异常
需求:
如果先启动客户端,则先挂起,等服务器启动后,再正常请求。
解决:
在ros中有内置函数 ,这些函数可以让客户端启动后挂起,等待服务器启动
client.waitForExistence();
ros::service::waitForService("AddInts");
*/
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
if(argc != 3 )
{
ROS_INFO("提交的参数个数不对。");
return 1;
}
// 2.初始化ROS节点
ros::init(argc,argv,"AddInts_client");
// 3.创建节点句柄
ros::NodeHandle nh;
// 4.创建一个客户端对象
ros::ServiceClient client = nh.serviceClient("AddInts");
// 5.提交请求并处理响应
plumbing_server_client::AddInts ai;
// 5-1 组织请求,用atoi把argv解析出来的字符串型变为整形
ai.request.num1 = atoi(argv[1]);
ai.request.num2 = atoi(argv[2]);
// 5-2 处理响应。
// 调用判断服务器状态的函数
// client.waitForExistence();
ros::service::waitForService("AddInts");
bool flag = client.call(ai);
/*
1.client.call(ai) 相当于我客户端访问了服务器并且把这个ai对象提交了,ai对象里面有request,request里面有num1和num2 2.他响应回来的结果就是那个sum会继续封装在这个ai对象,在这ai对象中除了有request还有response他会把那个结果封装在response,接下来我们就可以通过ai的response来获取数据了。3.客户端访问服务器:client.call(ai),客户端访问服务器并且把ai对象提交了。服务器会返回结果
*/
if (flag)
{
ROS_INFO("响应成功");
// 获取结果
ROS_INFO("响应结果 = %d",ai.response.sum);
}
else
{
ROS_INFO("处理失败...");
}
return 0;
}
配置CMake文件
编译,运行
roscore
source ./devel/setup.bash
rosrun plumbing_server_client demo01_server
source ./devel/setup.bash
rosrun plumbing_server_client demo02_client
参数服务器在ROS中主要用于实现不同节点之间的数据共享。参数服务器相当于是独立于所有节点的一个公共容器,可以将数据存储在该容器中,被不同的节点调用,当然不同的节点也可以往其中存储数据。以共享的方式实现不同节点之间数据交互的通信模式。
整个流程由以下步骤实现:
1.Talker 设置参数
Talker 通过 RPC 向参数服务器发送参数(包括参数名与参数值),ROS Master 将参数保存到参数列表中。
2.Listener 获取参数
Listener 通过 RPC 向参数服务器发送参数查找请求,请求中包含要查找的参数名。
3.ROS Master 向 Listener 发送参数值
ROS Master 根据步骤2请求提供的参数名查找参数值,并将查询结果通过 RPC 发送给 Listener。
#include "ros/ros.h"
/*
需要实现参数的新增与修改
需求:设置机器人的共享参数,类型\半径(0.15m)
再修改半径(0.2m)
实现:
ros::NodeHandle
setParam()
ros::param
set()
修改:
继续调用setParam()或set(),键已经存在的话,值会被替换
*/
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
//初始化节点
ros::init(argc,argv,"set_param_c");
// 创建ROS节点句柄
ros::NodeHandle nh;
//参数增--
//方案一:nh
nh.setParam("type","xiaohuang");
nh.setParam("radius",0.15);
// 方案二:ros::param
ros::param::set("type_param","xiaobai");
ros::param::set("radius_param",0.15);
// 参数改
// 方案1:nh
nh.setParam("radius",0.2);//键重复,产生参数覆盖。
// 方案2:ros::param
ros::param::set("radius_param",0.25);
return 0;
}
配置,编译,运行
roscore
source ./devel/setup.bash
rosrun plumbing_param_server demo01_param_set
rosparam list
rosparam get /radius
#include "ros/ros.h"
/*
参数查询
ros::NodeHandle -------------
1.param(键,默认值)
存在,返回对应结果,否则返回默认值。
2.getParam(键,存储结果的变量)
存在,返回 true,且将值赋值给参数2
若果键不存在,那么返回值为 false,且不为参数2赋值
3.getParamCached键,存储结果的变量)--提高变量获取效率
存在,返回 true,且将值赋值给参数2
若果键不存在,那么返回值为 false,且不为参数2赋值
4.getParamNames(std::vector)
获取所有的键,并存储在参数 vector 中
5.hasParam(键)
是否包含某个键,存在返回 true,否则返回 false
6.searchParam(参数1,参数2)
搜索键,参数1是被搜索的键,参数2存储搜索结果的变量
ros::param ------------- 与上述类似
*/
int main(int argc, char *argv[])
{
// 中文
setlocale(LC_ALL,"");
// 初始化节点
ros::init(argc,argv,"get_param_c");
// 创建节点句柄
ros::NodeHandle nh;
// ros::NodeHandle ----------
// 1.param
double radius = nh.param("radius",0.5); //默认值是0.5
ROS_INFO("radius = %.2f",radius);
// 2.getParam
double radius2 = 0.0;
bool result = nh.getParam("radius",radius2);//把获取到的radius值存在radius2中。
// 3.getParamCached 与 getParam 类似,只是性能有所提升
if (result)
{
ROS_INFO("获取的半径是: %.2f",radius2);
}
else{
ROS_INFO("被查询的变量不存在。");
}
// 4.getParamNames
std::vector names;//表示可以改变大小的数组的序列容器。创建一个集合
nh.getParamNames(names);
for (auto &&name : names)/*auto的原理就是根据后面的值,来自己推测前面的类型是什么。
auto的作用就是为了简化变量初始化,如果这个变量有一个很长很长的初始化类型,就可以用auto代替。 &&右值引用,元素name,被遍历的是names*/
{
ROS_INFO("遍历的元素:%s",name.c_str());
}
// 5.hasParam
bool flag1 = nh.hasParam("radius");
bool flag2 = nh.hasParam("radiusxxx");
ROS_INFO("radius 存在吗?%d",flag1);
ROS_INFO("radiusxxx 存在吗?%d",flag2);
// 6.searchParam
std::string key;
nh.searchParam("radius",key);
ROS_INFO("搜索结果:%s",key.c_str());//%s字符串,c_str()函数返回一个指向正规C字符串的指针常量
// ros::param---------
double radius_param = ros::param::param("radius",100.5);
ROS_INFO("radius_param = %.2f",radius_param);
std::vector names_param;
ros::param::getParamNames(names_param);
for (auto &&name : names_param)
{
ROS_INFO("键:%s",name.c_str());
}
return 0;
}
#include "ros/ros.h"
/*
参数服务器操作之删除_C++实现:
ros::NodeHandle
deleteParam("键")
根据键删除参数,删除成功,返回 true,否则(参数不存在),返回 false
ros::param
del("键")
根据键删除参数,删除成功,返回 true,否则(参数不存在),返回 false
*/
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
ros::init(argc,argv,"param_del_c");
ros::NodeHandle nh;
// 删除 NodeHandle
bool flag1 = nh.deleteParam("radius");
if (flag1)
{
ROS_INFO("删除成功");
}
else{
ROS_INFO("删除失败");
}
// 删除 ros::param
bool flag2 = ros::param::del("radius_param");
if (flag2)
{
ROS_INFO("删除成功");
}
else{
ROS_INFO("删除失败");
}
return 0;
}