Chapter 4. Services
Services are another way to pass data between nodes in ROS. Services are just synchronous remote procedure calls; they allow one node to call a function that executes in another node. We define the inputs and outputs of this function similarly to the way we define new message types. The server (which provides the service) specifies a callback to deal with the service request, and advertises the service. The client (which calls the service) then accesses this service through a local proxy.
Service calls are well suited to things that you only need to do occasionally and that take a bounded amount of time to complete. Common computations, which you might want to distribute to other computers, are a good example. Discrete actions that the robot might do, such as turning on a sensor or taking a high-resolution picture with a camera, are also good candidates for a service-call implementation.
Although there are several services already defined by packages in ROS, we’ll start by looking at how to define and implement our own service, since this gives some insight into the underlying mechanisms of service calls. As a concrete example in this chapter, we’re going to show how to create a service that counts the number of words in a string.
Defining a Service
The first step in creating a new service is to define the service call inputs and outputs. This is done in a service-definition file, which has a similar structure to the message-definition files we’ve already seen. However, since a service call has both inputs and outputs, it’s a bit more complicated than a message.
Our example service counts the number of words in a string. This
means that the input to the service call should be a string
and the
output should be an integer. Although we’re using messages from
std_msg
s here, you can use any ROS message, even ones that you’ve
defined yourself. Example 4-1 shows a service definition
for this.
Example 4-1. WordCount.srv
string words --- uint32 count
Note
Like message-definition files, service-definition files are just lists of message
types. These can be built in, such as those defined in the std_msgs
package, or they can be ones you have defined yourself.
The inputs to the service call come first. In this case, we’re just
going to use the ROS built-in string
type. Three dashes (---
)
mark the end of the inputs and the start of the output definition.
We’re going to use a 32-bit unsigned integer (uint32
) for our
output. The file holding this definition is called WordCount.srv
and is traditionally in a directory called srv in the main package
directory (although this is not strictly required).
Once we’ve got the definition file in the right place, we need to run
catkin_make
to create the code and class definitions that we will
actually use when interacting with the service, just like we did for
new messages. To get catkin_make
to generate this code, we need to
make sure that the find_package()
call in CMakeLists.txt contains
message_generation
, just like we did for new messages:
find_package(catkin REQUIRED COMPONENTS roscpp rospy message_generation # Add message_generation here, after the other packages )
We also have to make an addition to the package.xml file to reflect
the dependencies on both rospy
and the message system. This means
we need a build dependency on message_generation
and a runtime
dependency on message_runtime
:
<build_depend>
rospy</build_depend>
<run_depend>
rospy</run_depend>
<build_depend>
message_generation</build_depend>
<run_depend>
message_runtime</run_depend>
Then, we need to tell catkin
which service-definition files we want
compiled, using the add_service_files()
call in CMakeLists.txt:
add_service_files( FILES WordCount.srv )
Finally, we must make sure that the dependencies for the service-definition file are
declared (again in CMakeLists.txt), using the generate_messages()
call:
generate_messages( DEPENDENCIES std_msgs )
With all of this in place, running catkin_make
will generate three
classes: WordCount
, WordCountRequest
, and WordCountResponse
.
These classes will be used to interact with the service, as we will
see. Just like with messages, you will probably never have to
look at the details of the generated classes. However, just in case
you’re interested, (part of) the classes generated by the WordCount
example are shown in Example 4-2.
Example 4-2. The Python classes generated by catkin_make for the WordCount example (code in functions removed for clarity)
"""autogenerated by genpy from basics/WordCountRequest.msg. Do not edit."""
import
sys
python3
=
True
if
sys
.
hexversion
>
0x03000000
else
False
import
genpy
import
struct
class
WordCountRequest
(
genpy
.
Message
):
_md5sum
=
"6f897d3845272d18053a750c1cfb862a"
_type
=
"basics/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
):
...
"""
def serialize(self, buff):
...
def deserialize(self, str):
...
def serialize_numpy(self, buff, numpy):
...
def deserialize_numpy(self, str, numpy):
...
class WordCountResponse(genpy.Message):
...
class WordCount(genpy.Message):
...
The details of the definitions for WordCountResponse
and WordCount
are similar to those for WordCountRequest
. All of these are just
ROS messages.
We can verify that the service call definition is what we expect by
using the rossrv
command:
user@hostname$ rossrv show WordCount [basics/WordCount]: string words --- uint32 count
You can see all available services using rossrv list
, all packages
offering services with rossrv packages
, and all the services offered
by a particular package with rossrv package
.
Implementing a Service
Now that we have a definition of the inputs and outputs for the service call, we’re ready to write the code that implements the service. Like topics, services are a callback-based mechanism. The service provider specifies a callback that will be run when the service call is made, and then waits for requests to come in. Example 4-3 shows a simple server that implements our word-counting service call.
Example 4-3. service_server.py
#!/usr/bin/env python
import
rospy
from
basics.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
()
We first need to import the code generated by catkin
:
from
basics.srv
import
WordCount
,
WordCountResponse
Notice that we need to import both WordCount
and
WordCountResponse
. Both of these are generated in a Python module
with the same name as the package, with a .srv extension
(basics.srv, in our case).
The callback function takes a single argument of type WordCountRequest
and returns a single argument of type WordCountResponse
:
def
count_words
(
request
):
return
WordCountResponse
(
len
(
request
.
words
.
split
()))
The constructor for WordCountResponse
takes parameters that match
those in the service-definition file. For us, this means an unsigned
integer. By convention, services that fail, for whatever reason,
should return None
.
After initializing the node, we advertise the service, giving it a
name (word_count
) and a type (WordCount
), and specifying the callback
that will implement it:
service
=
rospy
.
Service
(
'word_count'
,
WordCount
,
count_words
)
Finally, we make a call to rospy.spin()
, which gives control of the
node over to ROS and exits when the node is ready to shut down. You
don’t actually have to hand control over by calling rospy.spin()
(unlike in the C++ API), since callbacks run in their own threads.
You could set up your own loop, remembering to check for node
termination, if you have something else you need to do. However,
using rospy.spin()
is a convenient way to keep the node alive until
it’s ready to shut down.
Checking That Everything Works as Expected
Now that we have the service defined and implemented, we can verify
that everything is working as expected with the rosservice
command.
Start up a roscore
and run the service node:
user@hostname$ rosrun basics service_server.py
First, let’s check that the service is there:
user@hostname$ rosservice list /rosout/get_loggers /rosout/set_logger_level /service_server/get_loggers /service_server/set_logger_level /word_count
In addition to the logging services provided by ROS, our service seems
to be there. We can get some more information about it with
rosservice info
:
user@hostname$ rosservice info word_count Node: /service_server URI: rosrpc://hostname:60085 Type: basics/WordCount Args: words
This tells us the node that provides the service, where it’s running,
the type that it uses, and the names of the arguments to the service
call. We can also get some of this information using rosservice type
word_count
and roservice args word_count
.
Other Ways of Returning Values from a Service
In the previous example, we explicitly created a WordCountResponse
object and returned it from the service callback. There are a number
of other ways to return values from a service callback that you can
use. In the case where there is a single return argument for the
service, you can simply return that value:
def
count_words
(
request
):
return
len
(
request
.
words
.
split
())
If there are multiple return arguments, you can return a tuple or a list. The values in the list will be assigned to the values in the service definition, in order. This works even if there’s only one return value:
def
count_words
(
request
):
return
[
len
(
request
.
words
.
split
())]
You can also return a dictionary, where the keys are the argument names (given as strings):
def
count_words
(
request
):
return
{
'count'
:
len
(
request
.
words
.
split
())}
In both of these cases, the underlying service call code in ROS will
translate these return types into a WordCountResponse
object and
return it to the calling node, just as in the initial example code.
Using a Service
The simplest way to use a service is to call it using the rosservice
command. For our word-counting service, the call looks like this:
user@hostname$ rosservice call word_count 'one two three' count: 3
The command takes the call
subcommand, the service name, and the
arguments. While this lets us call the service and make sure that
it’s working as expected, it’s not as useful as calling it from
another running node. Example 4-4 shows how to call our
service programmatically.
Example 4-4. service_client.py
#!/usr/bin/env python
import
rospy
from
basics.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
)
words
,
'->'
,
word_count
.
count
First, we wait for the service to be advertised by the server:
rospy
.
wait_for_service
(
'word_count'
)
If we try to use the service before it’s advertised, the call will fail with an exception. This is a major difference between topics and services. We can subscribe to topics that are not yet advertised, but we can only use advertised services. Once the service is advertised, we can set up a local proxy for it:
word_counter
=
rospy
.
ServiceProxy
(
'word_count'
,
WordCount
)
We need to specify the name of the service (word_count
) and the type
(WordCount
). This will allow us to use word_counter
like a local
function that, when called, will actually make the service call for
us:
word_count
=
word_counter
(
words
)
Checking That Everything Works as Expected
Now that we’ve defined the service, built the support code with
catkin
, and implemented both a server and a client, it’s time to see
if everything works. Check that your server is still running, and
run the client node (make sure that you’ve sourced
your workspace setup file in the shell in which you run the client
node, or it will not work):
user@hostname$ rosrun basics service_client.py these are some words these are some words -> 4
Now, stop the server and rerun the client node. It should stop, waiting for the service to be advertised. Starting the server node should result in the client completing normally, once the service is available. This highlights one of the limitations of ROS services: the service client can potentially wait forever if the service is not available for some reason. Perhaps the service server has died unexpectedly, or perhaps the service name is misspelled in the client call. In either case, the service client will get stuck.
Other Ways to Call Services
In our client node, we are calling the service through the proxy as if
it were a local function. The arguments to this function are used to
fill in the elements of the service request, in order. In our example,
we only have one argument (words
), so we are only allowed to give
the proxy function one argument. Similarly, since there is only one
output from the service call, the proxy function returns a single
value. If, on the other hand, our service definition were to look like this:
string words int min_word_length --- uint32 count uint32 ignored
then the proxy function would take two arguments, and return two values:
c
,
i
=
word_count
(
words
,
3
)
The arguments are passed in the order they are defined in the service definition. It is also possible to explicitly construct a service request object and use that to call the service:
request
=
WordCountRequest
(
'one two three'
,
3
)
count
,
ignored
=
word_counter
(
request
)
Note that, if you choose this mechanism, you will have to also import
the definition for WordCountRequest
in the client code, as follows:
from
basics.srv
import
WordCountRequest
Finally, if you only want to set some of the arguments, you can use keyword arguments to make the service call:
count
,
ignored
=
word_counter
(
words
=
'one two three'
)
While this mechanism can be useful, you should use it with care, since any arguments that you do not explicitly set will remain undefined. If you omit arguments that the service needs to run, you might get strange return values. You should probably steer clear of this calling style, unless you actually need to use it.
Summary
Now you know all about services, the second main communication mechanism in ROS. Services are really just synchronous remote procedure calls and allow explicit two-way communication between nodes. You should now be able to use services provided by other packages in ROS, and also to implement your own services.
Once again, we didn’t cover all of the details of services. To get more information on more sophisticated uses of services, you should look at the services API documentation.
You should use services for things that you only need to do occasionally, or when you need a synchronous reply. The computations in a service callback should take a short, bounded amount of time to complete. If they’re going to take a long time, or the time is going to be highly variable, you should think about using an action, which we describe in the next chapter.
Get Programming Robots with ROS now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.