掌握ROS机器人通信系统架构
掌握服务的通信原理及其与主题的不同
掌握自定义ROS服务的方法
掌握使用C++和Python实现ROS服务
本实验主要学习以下几方面的内容:
ROS通信架构
ROS服务的原理
创建一个自定义的ROS服务
实现自定义ROS服务通讯
1.所需仪器设备
(1)“卓越之星”设备-用于功能调试;
(2)个人PC-用于应用程序的开发与展示;
2.注意事项
(1)个人PC需要安装NoMachine或者VSCode支持远程编程开发;
(2)运动实验尽可能将设备平放在地面上;
(3)实验开始前确保设备电量为满电,实验完成后关闭设备电源;
ROS 中的服务机制可以让一个节点向另一个节点发送请求,并且等待并接收响应。服务机制由两个部分组成:服务服务器和服务客户端。服务服务器是提供服务的节点,服务客户端是请求服务的节点。
当服务客户端向服务服务器发送请求时,服务服务器会执行请求,并将结果返回给服务客户端。服务服务器可以同时服务多个服务客户端。
服务机制使用了 ROS 中的服务消息类型,服务消息类型定义了请求和响应的数据结构。服务客户端发送请求时,需要提供请求消息,服务服务器收到请求消息后,会根据请求消息执行相应的操作,并返回响应消息给服务客户端。
在 ROS 中,服务机制可以通过命令行工具rosservice进行测试和调试。rosservice list命令可以列出当前正在运行的服务,rosservice
call命令可以向指定的服务发送请求。
服务与主题的区别
服务和主题是 ROS 中常用的两种通信机制。
服务机制是一种双向通信机制,用于节点之间的请求和响应。服务客户端向服务服务器发送请求,服务服务器执行请求并返回响应。服务机制使用服务消息类型来定义请求和响应的数据结构,服务机制提供了一种方便的方式来实现节点间的函数调用,通常用于节点之间的交互和控制。
主题机制是一种发布/订阅模式的通信机制,用于节点之间的消息传递。主题发布者将消息发布到特定的主题上,主题订阅者可以订阅该主题并接收消息。主题机制使用主题消息类型来定义消息的数据结构,主题机制提供了一种方便的方式来实现节点之间的数据传递,通常用于节点之间的传感器数据和控制信号的传递。
因此,服务和主题的区别在于:服务是一种双向通信机制,用于节点之间的请求和响应,而主题是一种发布/订阅模式的通信机制,用于节点之间的消息传递。服务使用服务消息类型来定义请求和响应的数据结构,主题使用主题消息类型来定义消息的数据结构。
当服务的客户端向服务端发送一个服务请求的时候,客户端的程序进程是阻塞的,即必须要服务端接收到请求并给予回应,程序才可以继续正常运行,因此我们说服务是一种同步消息传递机制,而消息则无此约束,因此消息被称为异步的机制。
与消息一样,ROS 中有一些预定义的标准服务:
以下是 ROS 中预定义的标准服务类型:
但与消息不同的是,服务的需求一般非常定制化,因此我们直接先来介绍如何声明一个自定义的服务类型,自定义 ROS
服务消息可以让您根据自己的需求开发符合自己要求的服务。下面是一个简单的自定义服务消息类型的例子:
首先,在工作空间中创建一个名为class_3_pkg 的功能包,并在功能包根目录下创建一个名为srv的文件夹。
在 class_3_pkg 根目录的srv文件夹下,创建一个名为 MyServiceMsg.srv 的文件,文件名必须与服务消息类型的名称相同。
在 MyServiceMsg.srv 文件中定义服务消息类型:
int64 input #查询
---
int64 output #回复
上面的代码表示您的服务将接收一个 int64 类型的输入参数,并返回一个 int64 类型的输出参数。
在功能包的 package.xml 文件中添加以下内容:
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
这将会告诉 catkin 构建系统,您的包需要使用 ROS 消息生成库。
在 CMakeLists.txt 文件中添加以下内容:
find_package(catkin REQUIRED COMPONENTS
message_generation
)
add_service_files(
DIRECTORY srv
FILES MyServiceMsg.srv
)
generate_messages(
DEPENDENCIES
)
catkin_package(
CATKIN_DEPENDS message_runtime
)
这将会告诉 catkin 构建系统,您的包依赖于消息生成库,并且您的服务消息类型定义在 MyServiceMsg.srv 文件中。
在您的 C++代码中,您可以使用 class_3_pkg/MyServiceMsg.h
头文件来引用您自定义的消息类型,class_3_pkg代表包的名称,MyServiceMsg代表服务文件的文件名称(头文件可以在devel/include/class_3_pkg文件夹中找到)。
在您的 Python 代码中,您可以使用from class_3_pkg.srv import MyServiceMsg来引用您自定义的消息类型,class_3_pkg代表包的名称,srv
代表存放消息文件的文件夹名称,MyServiceMsg代表 srv 文件的文件名称(py
引用文件可以在devel/lib/python3/dist-packages/class_3_pkg/srv文件夹中找到)。
接下来我们实现一个 C++节点来创造一个 ROS 服务端,声明一个服务。
在功能包下创建一个 CPP 文件,输入以下示例代码:
#include "ros/ros.h"
#include "class_3_pkg/MyServiceMsgRequest.h"
#include "class_3_pkg/MyServiceMsgResponse.h"
#include "class_3_pkg/MyServiceMsg.h"
bool myServiceCallback(class_3_pkg::MyServiceMsgRequest &req, class_3_pkg::MyServiceMsgResponse &res) {
// 处理服务请求
res.output = req.input * 2;
ROS_INFO("Request: input = %d, output = %d", req.input, res.output);
return true;
}
int main(int argc, char **argv) {
// 初始化ROS节点
ros::init(argc, argv, "my_service_node");
// 创建节点句柄
ros::NodeHandle nh;
// 创建服务
ros::ServiceServer service = nh.advertiseService("my_service", myServiceCallback);
ROS_INFO("Ready to receive service requests.");
// 循环处理ROS回调函数
ros::spin();
return 0;
}
在这个示例中,我们首先包含了 ROS
节点所需的头文件,以及自定义的服务消息类型头文件然后,我们定义了一个服务回调函数myServiceCallback,用于处理服务请求并生成响应。在服务回调函数中,我们从请求消息中获取输入值,处理操作,并将输出结果存储在响应消息中,最后将响应返回给服务客户端。
接下来,在main函数中,我们初始化了 ROS
节点,并创建了一个节点句柄nh。然后,我们使用nh.advertiseService函数创建了一个名为my_service的服务,并将服务回调函数myServiceCallback传递给该服务。最后,我们使用ros::
spin函数循环处理 ROS 回调函数。
这样,我们就实现了一个 ROS 节点,通过服务机制提供了一个双倍数的计算服务,反馈一个查询数值双倍的数字,可以通过其他节点的服务客户端请求该服务,并获取计算结果。
接下来实现一个客户端请求这个服务:
#include "ros/ros.h"
#include "class_3_pkg/MyServiceMsgRequest.h"
#include "class_3_pkg/MyServiceMsgResponse.h"
#include "class_3_pkg/MyServiceMsg.h"
int main(int argc, char **argv) {
ros::init(argc, argv, "my_service_client");
ros::NodeHandle nh;
// 创建一个 ROS 服务客户端,请求 my_service 服务
ros::ServiceClient client = nh.serviceClient<class_3_pkg::MyServiceMsg>("my_service");
// 创建一个请求消息
class_3_pkg::MyServiceMsg srv;
srv.request.input = 42;
if (client.call(srv)) {
ROS_INFO("Service response: %ld", (long int) srv.response.output); // 输出响应结果
} else {
ROS_ERROR("Failed to call service my_service");
return 1;
}
return 0;
}
上面的代码中,我们创建了一个 ROS 服务客户端来请求 my_service 服务。然后,我们创建了一个自定义服务类型的请求消息,并设置了输入参数。接下来,我们使用
client.call(srv) 发送请求消息并等待响应。如果服务响应成功,我们将输出响应结果。
效果如下图所示:
首先是服务端:
#!/usr/bin/env python3
import rospy
from class_3_pkg.srv import MyServiceMsg
def handle_my_service(req):
rospy.loginfo("Service request: %d", req.input)
return req.input * 2 # 返回输出结果
rospy.init_node("my_service_server")
service = rospy.Service("my_service", MyServiceMsg, handle_my_service)
rospy.loginfo("Ready to handle requests")
rospy.spin()
上述代码中,我们使用 rospy.Service 函数创建了一个名为 my_service 的服务,并定义了一个回调函数 handle_my_service
来处理服务请求。当收到请求时,回调函数将记录请求参数,并返回一个 req.input*2的输出结果。最后,我们使用 rospy.spin() 函数来让
ROS 节点保持运行状态。
客户端:
#!/usr/bin/env python3
import rospy
from class_3_pkg.srv import MyServiceMsg
rospy.init_node("my_service_client")
# 等待 my_service 服务可用
rospy.wait_for_service("my_service")
# 创建 my_service 服务的客户端
client = rospy.ServiceProxy("my_service", MyServiceMsg)
# 发送请求消息并等待响应
try:
resp = client.call(2)
rospy.loginfo("Service response: %d", resp.output)
except rospy.ServiceException as e:
rospy.logerr("Service call failed: %s", e)
上述代码中,我们首先使用 rospy.wait_for_service 函数等待 my_service 服务可用。然后,我们使用 rospy.ServiceProxy 函数创建了一个
my_service 服务的客户端,并创建了一个请求消息 req。接下来,我们使用 client(req)
发送请求消息并等待响应。如果服务响应成功,我们将记录响应结果。如果服务调用失败,我们将记录错误信息。
运行效果:
需要注意的是,同一个服务类型,接口一致的情况下,C++和Python编写的服务节点也可以互相通信。
首先打开卓越之星的电源,AI处理器与PC连接到同一个局域网,使用Nomachine或Remote-SSH进入开发模式;
然后打开一个终端,输入roscore,启动ros的master;
在ROS中创建一个工作空间;
在这个工作空间中创建一个功能包;
在功能包内,自定义一个服务,在服务器中存储本班学生学号(整数类型)对应的姓名(字符串),客户端通过学号来请求姓名;
在这个功能包内,基于Python/C++实现自定义服务的服务端/客户端;