Basics before starting with Robotics — Part 7

Services in ROS

Talha Hanif Butt
9 min readJun 9, 2022

So, I have written about an initial Hello World program in ROS:

I followed it up with Topics in ROS:

Today, I will discuss Services in ROS.

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.

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.

WordCount.srv
string words
— -
uint32 count

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
std_msgs
message_generation
)

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>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
<build_depend>rospy</build_depend>
<build_export_depend>rospy</build_export_depend>
<exec_depend>rospy</exec_depend>

Then, we need to tell catkin which service-definition files we want compiled, using theadd_service_files() call in CMakeLists.txt:

add_service_files(
FILES
WordCount.srv
# Service1.srv
# Service2.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 # Or other packages containing 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.

We can verify that the service call definition is what we expect by using the rossrv command:

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.

A simple server that implements our word-counting service call:

service_server.py

#!/usr/bin/env python2
import rospy
from my_awesome_code.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 my_awesome_code.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 (my_awesome_code.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(), 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:

rosrun my_awesome_code service_server.py

First, let’s check that the service is there:

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:

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:

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:

You can also return a dictionary, where the keys are the argument names (given as strings):

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:

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.

Following shows how to call our service programmatically.

service_client.py

#!/usr/bin/env python2

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

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):

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:

then the proxy function would take two arguments, and return two values:

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:

Note that, if you choose this mechanism, you will have to also import the definition for WordCountRequest in the client code, as follows:

from my_awesome_code.srv import WordCountRequest

Finally, if you only want to set some of the arguments, you can use keyword arguments to make the service call:

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.

So, that’s it for now. See you later.

References

https://github.com/StevenShiChina/books/blob/master/Programming.Robots.with.ROS.A.Practical.Introduction.to.the.Robot.Operating.System.pdf

--

--

Talha Hanif Butt

PhD Student -- Signal and Systems Engineering, Halmstad University, Volvo Trucks http://pk.linkedin.com/in/talhahanifbutt