ROS有两种通信方式:话题通信、服务通信。
话题通信是基于发布订阅模式的,即:一个节点发布消息,另一个节点订阅该消息。发布方和订阅方不相互影响,也就是发布方只负责发消息,订阅方只负责订阅消息。
服务通信是基于请求响应模式的,是一种应答机制。也即:一个节点A向另一个节点B发送请求,B接收处理请求并产生响应结果返回给A。用于偶然的、对时时性有要求、有一定逻辑处理需求的数据传输场景。
用一个案例来学习ros服务通信:
一、服务通信理论模型
二、服务通信自定义srv
step1:在功能包中定义一个srv文件夹,在这个文件夹中定义xxx.srv文件。
step2:在xxx.srv中规定数据类型。---上部分是请求部分,下部分为响应数据。
step3:为xxx.srv添加编译规则
打开package.xml文件,添加以下两行代码:
打开CMakeList.txt文件, 在find_package中添加:
找到下方示例代码,将刚刚定义的xxx.srv添加进去,作用是让在srv文件夹中生成服务:
我们定义的xxx.srv是基于std_msgs依赖的,所以需要添加依赖项。
在catkin_package中添加message_runtime。(find_package是指当前的功能包所依赖的包,而catkin_package是指依赖包所依赖的其他功能包)
step4:编译,会生成中间文件。在工作空间中的devel文件夹中,会生成以下三种头文件。这三个文件是写c++代码使用的。
在lib中会有以下文件,这是编写python代码时使用的。
三、服务通信自定义srv调用c++实现
配置c_cpp_properies.json 文件。这样可以使用代码补齐等功能。这一步可以没有!!!
3.1 服务端C++实现
step1:在src目录下创建xxx.cpp文件
step2:添加头文件,第二个头文件是指自定义的srv文件,格式是:功能包名/xxx.h
step3:编写代码
/*需求: 编写两个节点实现服务通信,客户端节点需要提交两个整数到服务器服务器需要解析客户端提交的数据,相加后,将结果响应回客户端,客户端再解析服务器实现:1.包含头文件2.初始化 ROS 节点3.创建 ROS 句柄4.创建 服务 对象5.回调函数处理请求并产生响应6.由于请求有多个,需要调用 ros::spin()*/
#include "ros/ros.h"
#include "demo03_server_client/AddInts.h"// bool 返回值用于标志是否处理成功
bool doReq(demo03_server_client::AddInts::Request& req,demo03_server_client::AddInts::Response& resp){int num1 = req.num1;int num2 = req.num2;ROS_INFO("服务器接收到的请求数据为:num1 = %d, num2 = %d",num1, num2);//逻辑处理if (num1 < 0 || num2 < 0){ROS_ERROR("提交的数据异常:数据不可以为负数");return false;}//如果没有异常,那么相加并将结果赋值给 respresp.sum = num1 + num2;return true;
}int main(int argc, char *argv[])
{setlocale(LC_ALL,"");// 2.初始化 ROS 节点ros::init(argc,argv,"AddInts_Server");// 3.创建 ROS 句柄ros::NodeHandle nh;// 4.创建 服务 对象ros::ServiceServer server = nh.advertiseService("AddInts",doReq);ROS_INFO("服务已经启动....");// 5.回调函数处理请求并产生响应// 6.由于请求有多个,需要调用 ros::spin()ros::spin();return 0;
}
回调函数必须返回一个bool值,形参指的是自定义srv文件中的请求和响应部分的数据。格式为:功能包名::xxx::Requset,他里面包含的有num1,num2两个数据。
step4:添加编译规则。打开CMakeList.txt文件:
step5:编译
step6:测试。可以启动服务端节点,查看设置的打印语句是否正常输出。
step7:另外起一个端口,运行rosservice,这个命令可以直接调用ros中的service服务。后面加上call和话题名称,再输入发送的数据,服务端就能响应并返回结果。
3.2 客户端C++实现
step1:创建客户端节点
step2:编写代码
/*需求: 客户端节点需要提交两个整数到服务器,并对返回结果进行响应服务器实现:1.包含头文件2.初始化 ROS 节点3.创建 ROS 句柄4.创建 客户端 对象5.请求服务,接收响应*/
// 1.包含头文件
#include "ros/ros.h"
#include "demo03_server_client/AddInts.h"int main(int argc, char *argv[])
{setlocale(LC_ALL,"");// 2.初始化 ROS 节点ros::init(argc,argv,"AddInts_Client");// 3.创建 ROS 句柄ros::NodeHandle nh;// 4.创建 客户端 对象ros::ServiceClient client = nh.serviceClient<demo03_server_client::AddInts>("AddInts");// 5.组织请求数据demo03_server_client::AddInts ai;ai.request.num1 = 100;ai.request.num2 = 200;// 6.发送请求,返回 bool 值,标记是否成功bool flag = client.call(ai);// 7.处理响应if (flag){ROS_INFO("请求正常处理,响应结果:%d",ai.response.sum);}else{ROS_ERROR("请求处理失败....");return 1;}return 0;
}
step3: 添加编译规则。打开CMakeList.txt文件:
step4:编译+运行。
3.3 客户端动态提交数据
上述代码中将num1和num2写死了。利用main函数的argc、argv来实现客户端动态提交数据。
需求:
1、格式 :rosrun 功能包名 节点名 num1 num2
2、节点执行时,需要获取命令中的参数,并组织进request
step1:优化客户端代码
#include "ros/ros.h"
#include "demo03_server_client/AddInts.h"int main(int argc, char *argv[])
{setlocale(LC_ALL,"");// 调用时动态传值。通过argv传参,参数有两个,因此argc=3,argv的第二三个数值为num1和num2if (argc != 3)// if (argc != 5)//launch 传参(0-文件路径 1传入的参数 2传入的参数 3节点名称 4日志路径){ROS_ERROR("请提交两个整数");return 1;}// 2.初始化 ROS 节点ros::init(argc,argv,"AddInts_Client");// 3.创建 ROS 句柄ros::NodeHandle nh;// 4.创建 客户端 对象ros::ServiceClient client = nh.serviceClient<demo03_server_client::AddInts>("AddInts");//等待服务启动成功//方式1,参数1:被等待的服务话题名称ros::service::waitForService("AddInts");//方式2// client.waitForExistence();// 5.组织请求数据,argv[1]里的数值是string类型,需要将其转换化成intdemo03_server_client::AddInts ai;ai.request.num1 = atoi(argv[1]);ai.request.num2 = atoi(argv[2]);// 6.发送请求,返回 bool 值,标记是否成功bool flag = client.call(ai);// 7.处理响应if (flag){ROS_INFO("请求正常处理,响应结果:%d",ai.response.sum);}else{ROS_ERROR("请求处理失败....");return 1;}return 0;
}
step2:编译+运行
【注意】:
四、服务通信自定义srv调用python实现
配置settings.json 文件。这样可以使用代码补齐等功能。这一步可以没有!!!
4.1 服务端python实现
step1:创建节点
step2:编写代码
#! /usr/bin/env python
"""服务器端实现:1.导包2.初始化 ROS 节点3.创建服务对象4.回调函数处理请求并产生响应5.spin 函数"""
# 1.导包
import rospy
from demo03_server_client.srv import AddInts,AddIntsRequest,AddIntsResponse
# 或者写成以下形式
# from demo03_server_client.srv import *
# 回调函数的参数是请求对象,返回值是响应对象
def doReq(req):# 解析提交的数据sum = req.num1 + req.num2rospy.loginfo("提交的数据:num1 = %d, num2 = %d, sum = %d",req.num1, req.num2, sum)# 创建响应对象,赋值并返回# resp = AddIntsResponse()# resp.sum = sumresp = AddIntsResponse(sum)return respif __name__ == "__main__":# 2.初始化 ROS 节点rospy.init_node("addints_server_p")# 3.创建服务对象server = rospy.Service("AddInts",AddInts,doReq)# 4.回调函数处理请求并产生响应# 5.spin 函数rospy.spin()
step3:添加可执行权限
step4:添加编译规则
step5:编译 +运行(同上)
4.2 客户端python实现
step1:新建节点
step2:编写代码
#! /usr/bin/env python"""客户端实现:1.导包2.初始化 ROS 节点3.创建请求对象4.发送请求5.接收并处理响应优化:加入数据的动态获取"""
#1.导包
import rospy
from demo03_server_client.srv import *
import sysif __name__ == "__main__":#优化实现if len(sys.argv) != 3:rospy.logerr("请正确提交参数")sys.exit(1)# 2.初始化 ROS 节点rospy.init_node("AddInts_Client_p")# 3.创建请求对象client = rospy.ServiceProxy("AddInts",AddInts)# 请求前,等待服务已经就绪# 方式1:# rospy.wait_for_service("AddInts")# 方式2client.wait_for_service()# 4.发送请求,接收并处理响应# 方式1# resp = client(3,4)# 方式2# resp = client(AddIntsRequest(1,5))# 方式3req = AddIntsRequest()# req.num1 = 100# req.num2 = 200 #优化req.num1 = int(sys.argv[1])req.num2 = int(sys.argv[2]) resp = client.call(req)rospy.loginfo("响应结果:%d",resp.sum)
step3:添加可执行权限
step4:添加编译规则
step5:编译+运行