1.服务 service
参考:Writing a Simple Service and Client (Python)
官方定义: Request / reply is done via a Service, which is defined by a pair of messages: one for the request and one for the reply.
服务是同步的跨进程函数调用。使用 client/server 模型,能够让客户端节点调用运行在服务端节点中的函数。服务端声明一个服务,并定义了一个回调函数来处理服务请求。客户端通过一个本地的代理请求调用这个服务。
同步:客户端发送请求数据,服务端完成处理后返回应答数据。client 端发送请求后会阻塞,直到 server 端返回结果才会继续执行。
既然有了topic 这种通讯机制,为什么还要有服务呢?原因就在于订阅/发布话题是不同步的,发布者只管发布消息,不管有没有或有几个订阅者,也不管订阅者能不能跟得上自己的发布速度。订阅者则只管监听消息,不会告诉发布者听没听到。这种方式交换数据的效率高,但完全不具备应答功能。
什么时候要应答呢?比如,节点A对着广场疯狂喊:2+3 等于几,2+3 等于几,2+3 等于几…,没有人理他,正确的操作应该是这样:节点A对着会做加法运算(声明了一个加法运算的服务)的节点B说:2+3 等于几。节点B算了一下,然后告诉节点 A:等于5。
这就是需要应答的场合,所以有了服务这个概念。当服务端收到服务请求后,会对请求做出响应,将数据的处理结果返回给客户端。这种模式更适用于双向同步的信息传输。服务调用非常适合那些只需要偶尔去做,并且会在有限的时间里完成的事。
ROS服务相关指令有两个:rosservice
和rossrv
,前者是对ROS服务本身的管理,后者是对ROS 服务类型的管理,相当于话题的rostopic
和rosmsg
。
2. 创建 srv 文件
ros 已经定义了一些服务,但我们也可以定义自己的服务。服务自定义文件通常放在功能包的 srv 文件夹下,文件扩展名为 .srv
。服务包含请求(request)数据和应答(response)数据,中间用三个小短线(---)隔开。
例如,定义一个计算字符串中单词个数的服务文件 WordCount.srv
string words
---
uint32 count
定义好了 srv 文件,就需要运行catkin_make
命令来编译与服务交互的时候真正会用到的代码和类定义。和自定义 message 一样,编译之前首先要修改CMakeLists.txt
和package.xml
文件。
在CMakeLists.txt
文件添加:
find_package(catkin REQUIRED COMPONENTS
rospy
std_msgs
message_generation # add message_generation here
)
...
catkin_packages(
CATKIN_DEPENDS rospy std_msg message_runtime # this will not be the only thing here
...
add_service_files(FILES WordCount.srv)
generate_messages(DEPENDENCIES std_msgs)
在package.xml
添加编译依赖和执行依赖:
<depend>rospy</depend>
<depend>std_msg</depend>
<build_depend>message_generation</build_depend>
<run_depend>message_runtime</run_depend>
运行catkin_make
会生成三个类:WordCount,WordCountRequest,和 WordCountResponse。可以在 devel 文件夹下找到相应的生成文件,当然你可能永远都不需要去查看这些细节。
3. 编写一个 Service Node(python)
服务和话题都基于回调函数的机制。服务器定义一个函数,等待客户端请求服务调用时,调用该函数并返回执行结果。
例程:单词计数服务。service_server.py
#!/usr/bin/env python
# -*- coding: utf-8
import rospy
from ur_move_test.srv import WordCount,WordCountResponse
def count_words(request):
# 回调函数只接受WordCountRequest 类型的的参数,并返回一个WordCountResponse类型的值
return WordCountResponse(len(request.words.split()))
def server():
rospy.init_node('service_server') # 节点初始化
# 声明服务,服务名称 'word_count',srv类型 WordCount,回调函数 count_words.
service = rospy.Service('word_count', WordCount, count_words)
print("Ready to service.")
rospy.spin()
if __name__ == "__main__":
server()
增加运行权限:chmod +x service_server.py。运行节点 rosrun pkg_name service_server.py
。
rosservice call word_count "one two three"
命令可以直接调用服务。
注:前文创建WordCount服务时已经编译过功能包,又python是脚本语言,故此处无需编译即可执行。
从服务中返回一些值的其他方法
- 服务只有一个返回参数时可以直接 return,而无需显示创建一个WordCountResponse对象.
def count_words(request):
return len(request.words.split())
- 有多个返回参数时,可以返回一个元组或列表。列表中的值将会按顺序赋给服务中定义的返回参数。也可以返回字典,其中键名是参数的名字(以字符串的形式给出)。
def count_words(request):
return [len(request.words.split())]
#或返回字典
def count_words(request):
return {
'count':len(request.words.split())}
4. 编写一个 Client Node (python)
service_client.py
#!/usr/bin/env python
# -*- coding: utf-8
import rospy
import sys
from ur_move_test.srv import WordCount
rospy.init_node('service_client') # 节点初始化
print("wait for service.")
rospy.wait_for_service('word_count') # 等待服务端声明这个服务
print("Service has started")
# 声明服务的本地代理,需要指定服务的名称(‘word_count’)和类型(WordCount)
# 这允许我们像使用本地函数一样使用服务。
word_counter = rospy.ServiceProxy('word_count', WordCount)
words=''.join(sys.argv[1:])
word_count = word_counter(words)
print words, '->', word_count.count
rospy.spin()
如果没有rospy.wait_for_service('word_count')
,那么在服务声明之前调用这个服务,会抛出异常。这是话题和服务的一个主要区别。即使一个话题没有声明,我们也可以订阅它。
增加运行权限:chmod +x service_client.py。
运行 rosrun pkg_name service_client.py "these are some words"
会看到输出:these are some words -> 4
如果没有打开服务端,就运行客户端,客户端节点将会阻塞,等待服务被声明。此时启动服务端节点会使客户端节点继续工作。ros 的一大弊端是当服务端不可用时,客户端可能会一直等待下去。
也可以显示的构造一个服务请求对象来进行服务调用:
#!/usr/bin/env python
...
from pkg_name.srv import WordCount WordCountRequest
...
request = WordCountRequest('one two three')
word_count = word_counter(request)
...