内容参考自Programming Robots with ROS by Morgan Quiley, Brian Gerkey, and William D. Smart (O'Reily). Copyright 2015 Morgan Quiley, Brian Gerkey, and William D. Smart, 978-1-4493-2389-9

服务 (service) 是另一种在节点之间传递数据的方法。服务其实就是同步的跨进程函数调用;它们能够让一个节点调用运行在另一个节点中的函数。我们就像之前定义消息类型一样定义这个函数的输人/输出。服务端(提供服务的节点)定义了一个回调函数来处理服务请求,并声明这个服务。客户端(进行服务请求的节点)通过一个本地的代理调用这个服务。

服务调用非常适用于那些只需要偶尔去做并且会在有限时间里完成的事情。我们想要分发到其他计算机上去做的通用计算就是一个很好的例子。机器人可能偶尔发生的一些行为,比如打开传感器或从摄像机获取一张高分辨率的图像,也可以考虑用服务来实现。

定义服务

我们的例子使用了计算字符串中的单词的个数的。这意味着输入给服务调用的是一个stringg,输出的应该是一个整数。我们这里使用的是std_msgs包提供的消息类型,实际上也可以使用任何有效的ROS消息类型(即使是自己定义的也可以)。

WordCount.srv

1
2
3
string words
---
uint32 count

文件中首先是服务调用的输入。在这个例子中,我们直接使用ROS内建的string类型。三个小短线 (---) 表示输入的末尾和输出的开始。我们使用一个32位的无符号整数 (uint32) 作为输出。这个包含服务定义的文件叫作WordCount.srv并且通常保存在包目录的一个叫作rv的子目录中(虽然这并不是强制的)。

一旦写好了定义文件,我们就需要运行catkin_make来创建我们在与服务交互的时候真正会用到的代码和类定义,就像我们在创建新的消息类型时做的那样。为了让catkin_make产生这些代码,我们需要确保CMakeLists.txt文件中的find_package()调用包含message_generation,就像之前消息定义时那样:

1
2
3
4
5
find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
message_generation # Add message generation here, after the other packages
)

我们也需要在package.xml文件中添加一些东西来表示对rospy和消息生成系统的依赖。这意味着我们需要添加一条构建时依赖项message_generation和一条运行时依赖项message_runtime

1
2
3
4
5
<build_depend>rospy</build_depend>
<exec_depend>rospy</exec_depend>

<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>

然后,我们需要在CMakeLists.txt文件中使用add_service_files()告诉catkin那些服务定义文件需要编译:

1
2
3
4
add service files(
FILES
WordCount.srv
)

最后需要确保我们的服务定义文件的依赖项已经被声明了,使用CMakeLists.txt文件中的generate_messages()调用即可,如下代码所示:

1
2
3
4
generate_messages(
DEPENDENCIES
std_msgs
)

当所有这些都完成之后, 运行catkin_ make来生成三个类:WordCountWordCountRequestWordCountResponse

我们可以使用rossrv来检验服务的定义和我们想的是否一样:

1
rossrv show WordCount
1
2
3
4
[basic/WordCount]:
string words
---
uint32 count

实现服务

服务端程序

下面展示了一个简单的实现了单词计数服务的服务端程序。

service_server.py

1
2
3
4
5
6
7
8
9
10
#!/usr/bin/env python
import rospy
from basic.srv import WordCount, WordCountResponse

def count_words(request):
return WordCountResponse(len(request.words.split()))

rospy.init_node('service_server')
service = rospy.Service('word_count', WordCount, count_words)
rospy.spin()

检查一切是否工作正常

运行roscore并运行服务节点:

1
rosrun basic service_server.py

首先,我们检查服务是否已经可见:

1
rosservice list
1
2
3
4
5
/rosout/get_loggers
/rosout/set_logger_level
/service_server/get_loggers
/service_server/set_logger_level
/word_count

可以使用rosservice info获取更多信息:

1
rosservice info word_count
1
2
3
4
Node: /service_server
URI: rosrpc://ubuntu:42561
Type: basic/WordCount
Args: words

如果提示ERROR: Unable to load type [basic/WordCount].别忘了source ~/my_ws1/devel/setup.bash

使用服务

使用服务最简单的方式是使用rosservice命令直接调用它。对于我们的单词计数服务,调用方式如下:

1
rosservice call word_count 'one two three'
1
count: 3

在代码中调用服务

service_client.py

注意print函数,这里使用的是Python2语法。

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env python
import rospy
from basic.srv import WordCount
import sys

rospy.init_node('service_client')
rospy.wait_for_service('word_count')
word_counter = rospy.ServiceProxy('word_count', WordCount)
words = ' '.join(sys.argv[1:])
word_count = word_counter(words)
print words, '->', word_count.count

检查一切是否工作正常

1
rosrun basic service_client.py these are some words
1
these are some words -> 4

调用服务的其他方式

在客户端节点中,我们使用代理像调用本地函数一样调用服务。这个代理函数的参数用来作为服务调用的参数。在我们的例子中,只有一个参数 (words) ,所以我们只能给代理函数一个参数。同样,由于服务只有一个返回参数,所以代理函数只返回一个值如果在服务定义中我们这样写:

1
2
3
4
5
string words
int min_word_length
---
uint32 count
uint32 ignored

那么,代理函数將需要两个参数,并返回两个值:

1
c, i = word_count(words, 3)

参数按照定义文件中的顺序传递,也可以显式地构造一个服务请求对象来进行服务调用:

1
2
request = WordCountRequest('one two three', 3)
count, ignored = word_counter(request)

如果选择了这种方法,我们就需要在客户端节点中导入WordCountRequest的定义,如下代码所示:

1
from basic.srv import WordCountRequest

最后,如果只是想设置其中的某些参数,我们可以使用关键词参数:

1
count, ignored = word_counter(words='one two three')

尽管这种方式很有用,但是应谨慎使用,因为任何设有显式赋值的参数都将是未定义的值。如果我们省略了一些服务调用所必需的参数,那么我们可能会得到奇怪的值。应该尽量避免这种调用风格。