ROS通信机制之服务(Service)的应用

1、服务的概述

在上节我们讲过一个重要通信机制话题:ROS通信机制之话题(Topics)的发布与订阅以及自定义消息的实现,这里介绍另外一种节点之间传递数据的方法:服务(Service)
服务的本质是同步的跨进程函数调用,也就是说节点可以调用另一节点中的函数,其定义跟前面的消息很类似。
那么服务一般适合的场景是什么呢?对于那些只需要偶尔去做并且在有限时间里面完成的事情,比如说,分发到其他计算机上面去做通用计算,再比如打开传感器或者从摄像机获取一张高分辨率的图像,这些都可以考虑用到服务。分为两部分,服务端和客户端,或者说是请求和响应。
服务端:提供服务的节点,定义了一个回调函数,用来处理请求
客户端:服务请求的节点,通过本地代理去调用这个服务


2、服务的定义

跟自定义消息一样,先定义一个服务,区别在于定义的服务里面是有输入和输出,两者之间使用三个小短线"---"来隔开
我们这里看一个示例:统计字符串中单词的个数。这里我还是使用前面创建的test包,关于如何创建包,可以查阅:ROS新建工作区(workspace)与包(package)编译的实践(C++示例)

2.1、服务定义

cd ~/catkin_ws/src/test
mkdir srv
cd srv

gedit WordCount.srv

string words
---
uint32 count

为其添加可执行权限:chmod u+x WordCount.srv

可以看到,输入就是一串字符串,所以类型是string,短线隔开之后就是输出的定义,这里是统计单词个数,所以是一个无符号的整数类型uint32
定义好了服务之后,就需要运行catkin_make来创建我们与服务交互的时候真正会用到的代码和类定义,这些都将是自动生成。

2.2、修改package.xml

cd ~/catkin_ws/src/test
gedit package.xml

<buildtool_depend>catkin</buildtool_depend>
<build_depend>message_generation</build_depend>
<build_export_depend>rospy</build_export_depend>
<exec_depend>message_runtime</exec_depend>

2.3、修改CMakeLists.txt 

接着修改CMakeLists.txt文件,里面的find_package()调用包含message_generation,跟消息定义一样,进入编辑:gedit CMakeLists.txt

find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs message_generation)

在消息里面是告诉catkin添加的消息文件:add_message_files(),同样的,这里需要告知哪些服务定义的文件需要被编译:

add_service_files(FILES WordCount.srv)

最后同样的需要确保服务定义文件的依赖项已被声明,注释去掉:

generate_messages(DEPENDENCIES std_msgs)

2.4、编译

定义好了之后,跟自定义消息一样,回到工作区根目录进行编译 

cd ~/catkin_ws
catkin_make 

编译之后将自动生成三个类:WordCountRequestWordCountResponseWordCount,这些类将被用来跟服务进行交互,当然了,这些编译后的类一般是不需要去查看的,为了更清晰的了解这个过程,我们依然来看下:

cd ~/catkin_ws/devel/lib/python2.7/dist-packages/test/srv
cat _WordCount.py
# This Python file uses the following encoding: utf-8
"""autogenerated by genpy from test/WordCountRequest.msg. Do not edit."""
import codecs
import sys
python3 = True if sys.hexversion > 0x03000000 else False
import genpy
import struct


class WordCountRequest(genpy.Message):
  _md5sum = "6f897d3845272d18053a750c1cfb862a"
  _type = "test/WordCountRequest"
  _has_header = False  # flag to mark the presence of a Header object
  _full_text = """string words
"""
  __slots__ = ['words']
  _slot_types = ['string']

  def __init__(self, *args, **kwds):
    """
    Constructor. Any message fields that are implicitly/explicitly
    set to None will be assigned a default value. The recommend
    use is keyword arguments as this is more robust to future message
    changes.  You cannot mix in-order arguments and keyword arguments.

    The available fields are:
       words

    :param args: complete set of field values, in .msg order
    :param kwds: use keyword arguments corresponding to message field names
    to set specific fields.
    """
    if args or kwds:
      super(WordCountRequest, self).__init__(*args, **kwds)
      # message fields cannot be None, assign default values for those that are
      if self.words is None:
        self.words = ''
    else:
      self.words = ''

  def _get_types(self):
    """
    internal API method
    """
    return self._slot_types

  def serialize(self, buff):
    """
    serialize message into buffer
    :param buff: buffer, ``StringIO``
    """
    try:
      _x = self.words
      length = len(_x)
      if python3 or type(_x) == unicode:
        _x = _x.encode('utf-8')
        length = len(_x)
      buff.write(struct.Struct('<I%ss'%length).pack(length, _x))
    except struct.error as se: self._check_types(struct.error("%s: '%s' when writing '%s'" % (type(se), str(se), str(locals().get('_x', self)))))
    except TypeError as te: self._check_types(ValueError("%s: '%s' when writing '%s'" % (type(te), str(te), str(locals().get('_x', self)))))

  def deserialize(self, str):
    """
    unpack serialized message in str into this message instance
    :param str: byte array of serialized message, ``str``
    """
    if python3:
      codecs.lookup_error("rosmsg").msg_type = self._type
    try:
      end = 0
      start = end
      end += 4
      (length,) = _struct_I.unpack(str[start:end])
      start = end
      end += length
      if python3:
        self.words = str[start:end].decode('utf-8', 'rosmsg')
      else:
        self.words = str[start:end]
      return self
    except struct.error as e:
      raise genpy.DeserializationError(e)  # most likely buffer underfill


  def serialize_numpy(self, buff, numpy):
    """
    serialize message with numpy array types into buffer
    :param buff: buffer, ``StringIO``
    :param numpy: numpy python module
    """
    try:
      _x = self.words
      length = len(_x)
      if python3 or type(_x) == unicode:
        _x = _x.encode('utf-8')
        length = len(_x)
      buff.write(struct.Struct('<I%ss'%length).pack(length, _x))
    except struct.error as se: self._check_types(struct.error("%s: '%s' when writing '%s'" % (type(se), str(se), str(locals().get('_x', self)))))
    except TypeError as te: self._check_types(ValueError("%s: '%s' when writing '%s'" % (type(te), str(te), str(locals().get('_x', self)))))

  def deserialize_numpy(self, str, numpy):
    """
    unpack serialized message in str into this message instance using numpy for array types
    :param str: byte array of serialized message, ``str``
    :param numpy: numpy python module
    """
    if python3:
      codecs.lookup_error("rosmsg").msg_type = self._type
    try:
      end = 0
      start = end
      end += 4
      (length,) = _struct_I.unpack(str[start:end])
      start = end
      end += length
      if python3:
        self.words = str[start:end].decode('utf-8', 'rosmsg')
      else:
        self.words = str[start:end]
      return self
    except struct.error as e:
      raise genpy.DeserializationError(e)  # most likely buffer underfill

_struct_I = genpy.struct_I
def _get_struct_I():
    global _struct_I
    return _struct_I
# This Python file uses the following encoding: utf-8
"""autogenerated by genpy from test/WordCountResponse.msg. Do not edit."""
import codecs
import sys
python3 = True if sys.hexversion > 0x03000000 else False
import genpy
import struct


class WordCountResponse(genpy.Message):
  _md5sum = "ac8b22eb02c1f433e0a55ee9aac59a18"
  _type = "test/WordCountResponse"
  _has_header = False  # flag to mark the presence of a Header object
  _full_text = """uint32 count

"""
  __slots__ = ['count']
  _slot_types = ['uint32']

  def __init__(self, *args, **kwds):
    """
    Constructor. Any message fields that are implicitly/explicitly
    set to None will be assigned a default value. The recommend
    use is keyword arguments as this is more robust to future message
    changes.  You cannot mix in-order arguments and keyword arguments.

    The available fields are:
       count

    :param args: complete set of field values, in .msg order
    :param kwds: use keyword arguments corresponding to message field names
    to set specific fields.
    """
    if args or kwds:
      super(WordCountResponse, self).__init__(*args, **kwds)
      # message fields cannot be None, assign default values for those that are
      if self.count is None:
        self.count = 0
    else:
      self.count = 0

  def _get_types(self):
    """
    internal API method
    """
    return self._slot_types

  def serialize(self, buff):
    """
    serialize message into buffer
    :param buff: buffer, ``StringIO``
    """
    try:
      _x = self.count
      buff.write(_get_struct_I().pack(_x))
    except struct.error as se: self._check_types(struct.error("%s: '%s' when writing '%s'" % (type(se), str(se), str(locals().get('_x', self)))))
    except TypeError as te: self._check_types(ValueError("%s: '%s' when writing '%s'" % (type(te), str(te), str(locals().get('_x', self)))))

  def deserialize(self, str):
    """
    unpack serialized message in str into this message instance
    :param str: byte array of serialized message, ``str``
    """
    if python3:
      codecs.lookup_error("rosmsg").msg_type = self._type
    try:
      end = 0
      start = end
      end += 4
      (self.count,) = _get_struct_I().unpack(str[start:end])
      return self
    except struct.error as e:
      raise genpy.DeserializationError(e)  # most likely buffer underfill


  def serialize_numpy(self, buff, numpy):
    """
    serialize message with numpy array types into buffer
    :param buff: buffer, ``StringIO``
    :param numpy: numpy python module
    """
    try:
      _x = self.count
      buff.write(_get_struct_I().pack(_x))
    except struct.error as se: self._check_types(struct.error("%s: '%s' when writing '%s'" % (type(se), str(se), str(locals().get('_x', self)))))
    except TypeError as te: self._check_types(ValueError("%s: '%s' when writing '%s'" % (type(te), str(te), str(locals().get('_x', self)))))

  def deserialize_numpy(self, str, numpy):
    """
    unpack serialized message in str into this message instance using numpy for array types
    :param str: byte array of serialized message, ``str``
    :param numpy: numpy python module
    """
    if python3:
      codecs.lookup_error("rosmsg").msg_type = self._type
    try:
      end = 0
      start = end
      end += 4
      (self.count,) = _get_struct_I().unpack(str[start:end])
      return self
    except struct.error as e:
      raise genpy.DeserializationError(e)  # most likely buffer underfill

_struct_I = genpy.struct_I
def _get_struct_I():
    global _struct_I
    return _struct_I
class WordCount(object):
  _type          = 'test/WordCount'
  _md5sum = '58903d21a3264f3408d79ba79e9f7c7e'
  _request_class  = WordCountRequest
  _response_class = WordCountResponse

2.5、查看服务

服务文件定义好并编译之后,我们可以使用rossrv命令来查看服务定义的内容

rossrv show WordCount
'''
[test/WordCount]:
string words
---
uint32 count
'''

恩,没有问题,跟定义的是一样的。其他一些服务相关的命令如下:
查看所有可用的服务:rossrv list

查看所有提供了服务的包:rossrv packages

 

查看某个包提供的服务:rossrv package control_msgs

control_msgs/QueryCalibrationState
control_msgs/QueryTrajectoryState  

3、实现服务

我们已经定义并编译了服务,现在就可以开始实现这个服务了。跟前面介绍的话题一样,服务也是基于回调函数的机制。

3.1、服务端节点

对字符串计算单词的个数:

cd ~/catkin_ws/src/test/src
gedit service_server.py
#!/usr/bin/env python

import rospy
from test.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()

然后加个执行权限:chmod u+x service_server.py

这里导入类的时候,都是在与包名同名的带有.srv后缀的模块中,这里的包是test,所以就是test.srv里面
其中rospy.Service函数的参数分别是声明的名字word_count,类型WordCount,回调函数count_words,最后就是调用rospy.spin()将程序的执行交给ROS,只有当节点即将要退出的时候才返回,当然这里调用rospy.spin()之后,并没有真正地交出程序的控制(跟C++的API有点区别),因为回调函数是在它们自己的线程中运行的。如果你有其他的事情需要做,可以创建自己的循环,但是要记得检查何时需要结束,使用rospy.spin()是一种方便的方式来保证节点直到需要退出的时候才退出。

3.2、服务是否正常

先运行上面的服务节点:rosrun test service_server.py
当然在此之前需要运行节点管理器,这个也已在前面的文章有讲述:roscore
接下来就可以使用roservice来查看下运行了哪些服务节点:rosservice list

/rosout/get_loggers
/rosout/set_logger_level
/service_server/get_loggers
/service_server/set_logger_level
/word_count 

除了ROS提供的日志服务,我们自己定义的服务service_server也在里面,还可以使用rosservice info来获取更多的信息:rosservice info word_count

Node: /service_server
URI: rosrpc://YAB:45277
Type: test/WordCount
Args: words 

这个word_count就是前面服务声明的名字,显示出来的服务信息,可以看到有节点名称,URI,类型,还有一个参数words
其他一些命令也可以直接获取,获取类型:rosservice type word_count

test/WordCount

获取参数:rosservice args word_count

words

3.3、多返回值

前面介绍的是只有一个返回值的情况,也可以是多个返回值,就使用元组或者列表,比如:

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

len(request.words.split()) 可以修改成 [len(request.words.split())] 进行返回,一个与多个都可以。

还可以使用字典类型,其中键名是参数的名字:

def count_words(request):
    return {'count':len(request.words.split())}

这两种情况下,ROS服务调用的底层代码都会将这些返回值直接翻译成WordCountResponse对象,也就是说元组或列表或字典的写法,得到的结果跟回调函数是一样的:

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

另外需要注意的是,字典的键名如果不是WordCountResponse的属性,就会报错,比如我将count修改成count1

ERROR: service [/word_count] responded with an error: service cannot process request: handler returned invalid value: count1 is not an attribute of WordCountResponse

4、调用服务 

 调用服务最简单的方式,命令行使用rosservice直接call即可,如下:

rosservice call word_count "hello tony are you ok"
count: 5

从返回的结果也可以看到,返回的K:V值跟前面我们多返回值的两种形式相符。这种命令方式一般用来确认它是否正常工作,所以大多情况我们还是将其写在另一节点中来调用。

客户端节点service_client.py

cd ~/catkin_ws/src/test/src
gedit service_client.py
#!/usr/bin/env python

import rospy
from test.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)

加个可执行权限:chmod u+x service_client.py

可以看到使用了本文概述中介绍的本地代理rospy.ServiceProxy,里面的参数分别是服务名称和类型。
其中的sys.argv[1:]表示的是输入的内容,因为sys.argv返回的是列表,其中第一个内容是文件名,第二个开始就是输入的内容,比如1.py:

import sys
print(' '.join(sys.argv))
print(' '.join(sys.argv[1:]))

然后我们在命令行执行它

python 1.py hello           world a          b   c
'''
1.py hello world a b c
hello world a b c
'''

客户端节点定义好了之后,我们来测试下

rosrun test service_client.py hello tony are you ok haha
'''
('hello tony are you ok haha', '-->', 6)
'''

结果没有问题,单词和统计数都正确显示!

5、小结

在本节的例子,只有一个参数words,所以代理函数也是一个参数,同样,服务也只有一个返回参数,所以代理函数只返回一个值。如果在服务中定义:

string words
int min_word_len
---
uint32 count
uint32 ignored

输入和输出都有两个参数的情况,那么代理函数也需要两个参数:word_counter(words,3),参数按照定义中的顺序传递,也可以显式构造一个服务请求对象来进行服务调用:

request = WordCountRequest('hello tony are you ok haha',3)
count,ignored = word_counter(request)

如果使用这种方法,那么在客户端需要导入:from test.srv import WordCountRequest

另外一个好的习惯就是,任何的参数,我们都应该去显式赋值,如果省略一些服务调用所必须的参数,这些参数都将是未定义的值,可能在节点交互之间出现一些未知的bug。 

服务作为在ROS中的第二种主要通信机制,一般是对那些偶尔会做的事情,或者是当你需要同步响应的时候,就考虑使用服务。服务的回调函数中的计算时间应该较短,需要在有限时间内完成,如果它们耗时太长,或者说对时间的要求高,那就需要考虑另外一种通信机制:动作

猜你喜欢

转载自blog.csdn.net/weixin_41896770/article/details/132557308