通信接口的主要目的就是传输数据,为了高效的建立连接,并且准确包装和解析传输的数据内容,话题、服务等机制也就诞生了,他们传输的数据,都要符合通信接口的标准定义。这些接口看上去像是给我们加了一些约束,但却是ROS系统的精髓所在。
接口可以让程序之间的依赖降低,便于我们使用别人的代码,也方便别人使用我们的代码,这就是ROS的核心目标,减少重复造轮子。
ROS有三种常用的通信机制,分别是话题、服务、动作,通过每一种通信种定义的接口,各种节点才能有机的联系到一起。为了保证每一个节点可以使用不同语言编程,ROS将这些接口的设计做成了和语言无关的。
话题通信接口的定义使用的是.msg文件,由于是单向传输,只需要描述传输的每一帧数据是什么就行,比如在传输两个32位的整型数,x、y,我们可以用来传输二维坐标的数值。
服务通信接口的定义使用的是.srv文件,包含请求和应答两部分定义,通过中间的“—”区分,比如之前加法求和功能,请求数据是两个64位整型数a和b,“—”后的应答是求和的结果sum。
动作是另外一种通信机制,用来描述机器人的一个运动过程,使用.action文件定义,比如我们让小海龟转90度,一边转一边周期反馈当前的状态,此时接口的定义分成了三个部分,分别是动作的目标,比如是开始运动,运动的结果,最终旋转的90度是否完成,还有一个周期反馈,比如每隔1s反馈一下当前转到第10度、20度还是30度了,让我们知道运动的进度。
一个.srv的例子如下(消息类型名称必须以大写字母开头,即几个消息类型文件名的首字母都必须是大写字母):
bool get # 获取目标位置的指令
---
int32 x # 目标的X坐标
int32 y # 目标的Y坐标
编译时会根据不同语言扩展成对应语言的实现结构,所有内容会包括在一个大的结构体内,这里的“—”前后又会被编译为两个小的结构体(request和response),作为大结构体的组成部分。然后需要对该.srv文件所在文件夹进行c++功能包的编译(.msg和.action也一样要用cmake编译),得到c++功能包的配置文件“CMakeLists.txt”(即便python代码,这一步也需要用c++方式编译通信接口文件),打开并配置“CMakeLists.txt”文件中的rosidl_generate_interfaces,形如:
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/ObjectPosition.msg"
"srv/AddTwoInts.srv"
"srv/GetObjectPosition.srv"
"action/MoveCircle.action"
)
其中每行填的参数是需要编译的通信接口文件的地址,以“CMakeLists.txt”文件所在目录为根目录进行查找。
在执行colcon编译时,会将通信接口文件编译到install文件下对应位置。在以下地址会分别生成一份c和c++的库文件(get_object_position.h和get_object_position.hpp):
工作空间/install/learning_interface/include/learning_interface/learning_interface/srv
在以下地址则生成python的库文件(_get_object_position.py和_get_object_position_s.c):
工作空间/install/learning_interface/local/lib/python3.10/dist-packages/learning_interface/srv
在python程序内调用接口时,只需要如导入一般的工具包一样,按地址去导入该结构体(类)即可像使用普通结构体(类)一样去使用了。形如以下语句,“learning_interface.srv”即表示以项目根目录为当前目录,“./learning_interface/srv”文件夹内的“GetObjectPosition”类(实际上肯定是在install文件夹里面找到的,只是这样说更好判断目录层级结构):
from learning_interface.srv import GetObjectPosition # 自定义的服务接口
在server服务器端创建服务器对象的时候就可以直接指定消息类型为“GetObjectPosition”类了:
# 创建服务器对象(接口类型、服务名、服务器回调函数)
self.srv = self.create_service(GetObjectPosition,
'get_target_position',
self.object_position_callback)
收到客户端请求后调用回调函数时,接受到的是一整个完整的“GetObjectPosition”类,对于server类型的通信接口,需要拆成request和response来处理(即前文所说两个小结构体)。顾名思义,request部分就是客户端传递来的内容,而修改并返回response内容,就是设定返回给客户端的内容。一个server类的回调成员函数形如:
def object_position_callback(self, request, response): # 创建回调函数,执行收到请求后对数据的处理
if request.get == True:
response.x = self.objectX # 目标物体的XY坐标
response.y = self.objectY
self.get_logger().info('Object position\nx: %d y: %d' %
(response.x, response.y)) # 输出日志信息,提示已经反馈
else:
response.x = 0
response.y = 0
self.get_logger().info('Invalid command') # 输出日志信息,提示已经反馈
return response
在client客户端,也是类似的处理,导入类:
from learning_interface.srv import GetObjectPosition # 自定义的服务接口
并用于创建连接:
# 创建服务客户端对象(服务接口类型,服务名)
self.client = self.create_client(GetObjectPosition, 'get_target_position')
设置变量存放request:
self.request = GetObjectPosition.Request()
设置request的参数并通过建立的client连接发送给服务器端:
def send_request(self):
self.request.get = True
self.future = self.client.call_async(self.request)
最后通过检测self.future,适时获取response:
if node.future.done():
response = node.future.result()