Skip to content

Lab 5.2. ROS Services in Python

Estimated time to complete this lab: 1 hour

Objectives

At the end of this self-learning lab, you should be able to:

  • write a service server and client in Python

Preparations

We will continue to use the lab5 package used in the previous lab. If you have downloaded the package into your workspace, you can skip the preparation.

See 05 L1 :: Preparations for the preparations.

Communication between nodes via ROS Services: Basic concepts

  • In programming, a function has optionally input parameters, a function body and optionally a return value. A function can be "called" by other programs.
  • In ROS, sometimes we wish that a node provides "functions" that other nodes can "call".
    • For example, we may want to have one node that is responsible for performing the function "addition of two integers", and this function can be called by other nodes that are responsible for receive input.
  • A "function provided by a node that other nodes can call" in ROS is called a service.
  • Services in ROS are commonly used to:
    • Trigger some start of an event via the function body
    • Perform some calculation based on input parameters, and return the result
    • Avoid repeated code, especially for features that are used by many nodes
  • The node that hosts the service is called a server.
  • The node that is calling the service is called a client.
  • The input of a service (function parameters) is called a request.
  • The output of a service (return values) is called a response.
  • Similar to communication via topics, we also need to define the format and structure of the request and response of a service. The format (or service type) is called a srv.
    • Note that it is possible for the request and/or response to be blank and not contain any variables. For example, if both are empty, then it is similar to a void function without any parameters.
  • Services are distinguished by their names. For any one service, only one node can be the server of that service. In other words, exactly one node will receive the service request and is responsible for making the service response.
  • Multiple nodes can make requests (be the client) to the same service.
  • The same node can be the server of multiple services and the client of multiple (other) services simultaneously.

Example

Server-Client demo

  • On two separate terminals (remember to keep roscore running somewhere else!), run the server node (adder.py) first, afterwards run the client node (caller.py):

    • $ rosrun lab5 server.py
    • $ rosrun lab5 caller.py
    • You should see caller.py generating some integers and making some requests, adder.py then processing the request of adding the numbers together, and finally caller.py displaying the response.
rosrun lab5 adder.py [INFO] [1577085365.200784]: Ready to add two ints. [INFO] [1577085376.393572]: Received [5, 6], returning 11 [INFO] [1577085377.401102]: Received [1, 1], returning 2 [INFO] [1577085378.413771]: Received [4, 4], returning 8 ...(omitted)
rosrun lab5 caller.py [INFO] [1577085376.386713]: Generated [5, 6], sending addition request...
[INFO] [1577085376.394974]: Received response: 11 [INFO] [1577085377.388028]: Generated [1, 1], sending addition request...
[INFO] [1577085377.404676]: Received response: 2 [INFO] [1577085378.388859]: Generated [4, 4], sending addition request...
[INFO] [1577085378.420586]: Received response: 8 ...(omitted)

Explanations

The following will become apparent in the code, but we show the basic flow below:

  • The adder node provides a service named /calc. The addition logic is performed in adder.
  • The service format of calc is of the srv type AddTwoInts.
    • In the code below, note that the response and request part of the srv are of the class AddTwoIntsRequest and AddTwoIntsResponse respectively.
  • caller is a node that generates random numbers and makes requests to /calc. Note that requests are made to a service (the interface is /calc), not to a node (not adder).

TODO: Insert Diagram


Section 1. Server

  • To make a node host a service, code for a service server is needed.
  • The service server receives the service request data via a callback function, which is invoked with the request data as an argument.

The code

  • Look at the content of lab5/src/adder.py.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/env python3

import rospy
from lab5.srv import *

def callback(req):
    ans = req.first + req.second

    rospy.loginfo("Received [%s, %s], returning %s"%
                                (req.first, req.second, ans))

    resp = AddTwoIntsResponse()
    resp.sum = ans
    return resp

    # return ans

    # return AddTwoIntsResponse(sum = ans)

if __name__ == "__main__":
    rospy.init_node('adder')
    rospy.Service('calc', AddTwoInts, callback)

    rospy.loginfo("Ready to add two ints.")
    rospy.spin()
  • Look at the content of lab5/srv/AddTwoInts.srv.
int64 first
int64 second
---
int64 sum

Explanations

Most portions of the code should be familiar as they are used for ROS nodes in general, and they appeared in the publisher/subscriber codes shown in previous labs. The new parts highlighted are those which are related to servers.

from lab5.srv import *
lab5.srv import allows us to use the lab5/AddTwoInts service type. This import also includes the classes lab5/AddTwoIntsRequest and lab5/AddTwoIntsResponse.
def callback(req):

The callback function is called whenever a new service request is received. Similar to subscriber callback, the request contents are stored in req, and the fields can be accessed via the . (dot) operator.

  • The request portion of a srv refers to the top part (above ---).
  • The response portion of a srv refers to the bottom part (below ---).
resp = AddTwoIntsResponse()

Creates an AddTwoIntsResponse object in resp and assigns values to the variable fields inside AddTwoIntsResponse.

  • return resp: You can choose to return the resp object directly.
  • return ans: You can choose to return a tuple containing the values to be filled into the AddTwoIntsResponse fields in order.
  • return AddTwoIntsResponse(sum = ans): You can also choose to only initialize some of the fields and leave the rest with default values (0).
rospy.Service('calc', AddTwoInts, callback)
This declares the node provides a calc service which is of type AddTwoInts. When new service requests are received, callback is invoked with the service request (an AddTwoIntsRequest object) as the first argument.
rospy.spin()
Prevent the node from exiting until the node has been shutdown. Without this line, the node will terminate by running to the end of main , and callback will not be invoked even if there are service requests to calc.

Running the node

  • Let's run the adder service server node: $ rosrun lab5 adder.py
    • Nothing is happening now, as the calc service has not been called yet.
  • To call a ROS service from the command line, use $ rosservice call:
    • Leave adder running.
    • On a separate command line, type rosservice call /calc without pressing Enter.
    • Type the Tab key twice, the service request fields "first: 0 second: 0" should appear. Enter any two integers to replace the two 0s respectively.
    • Type Enter to send a service request to the /calc service.
    • You should see some console output in adder that it received the service request, and a service response in the $ rosservice call terminal.
rosservice call /calc "first: 1 second: 2" sum: 3
$ rosrun lab5 adder.py [INFO] [1563187743.875656]: Ready to add two ints. [INFO] [1563188150.886430]: Received [1, 2], returning 3
  • The service request has been made, and the callback(req) function in adder.py has been invoked with the request from the terminal arguments.

Section 2. Client

  • To make a node call or make requests to a service, code for a service client is needed.
  • The service client will make request to the service server which hosts the service.

The code

  • Look at the content of lab5/src/caller.py.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/env python3
import rospy, random
from lab5.srv import *

if __name__ == "__main__":
    rospy.init_node('caller')

    calc_client = rospy.ServiceProxy('calc', AddTwoInts)

    r = rospy.Rate(1)
    while not rospy.is_shutdown():
        a = random.randint(1, 10)
        b = random.randint(1, 10)

        rospy.loginfo("Generated [%d, %d], sending addition request..." % (a, b))

        req = AddTwoIntsRequest()
        req.first = a
        req.second = b

        resp = calc_client(req)

        # resp = calc_client(a, b)

        rospy.loginfo("Received response: %d" % resp.sum)

        r.sleep()

Explanations

Let's look at the new parts related to service clients.

calc_client = rospy.ServiceProxy('calc', AddTwoInts)
This creates a service client object in calc_client that issues service requests to the service calc which is of srv type AddTwoInts.
req = AddTwoIntsRequest(); req.first = a; req.second = b
Creates an AddTwoIntsRequest object in req and assigns values to the variable fields inside AddTwoIntsRequest.
resp = calc_client(req)

call the service client for the service calc with request information inside req. The service response information returned by the service server is stored inside resp. You might notice that the syntax is similar to calling a function in python.

  • resp = calc_client(a, b): You can also choose to pass in the request parameters directly into the service client without creating a request object (req) by yourself.
rospy.loginfo("Received response: %d" % resp.sum)
The fields inside the AddTwoIntsResponse object can also be accessed via the . (dot) operator.

Running the node

  • Kill adder so it is not running.
  • Let's run the caller service client node: $ rosrun lab5 caller.py
  • An error should occur in caller:
rosrun lab5 caller.py [INFO] [1563201313.949032]: Generated [1, 3], sending addition request... Traceback (most recent call last): File "/home/m2/catkin_ws/src/lab5/src/caller.py", line 23, in resp = calc_client(req) ... (omitted) rospy.service.ServiceException: service [/calc] unavailable

Bug

The error is due to the service /calc being unavailable. This is because the service server (in adder) is not running.

To solve this error:

  • First, run adder: rosrun lab5 adder.py
    Then, run the caller service client node again: rosrun lab5 caller.py
    Now, there should be two terminals, one running adder and the other running caller.
  • The caller is now sending service requests to the service server in adder.
rosrun lab5 caller.py [INFO] [1563201592.473211]: Generated [3, 3], sending addition request... [INFO] [1563201592.477805]: Received response: 6 [INFO] [1563201593.474373]: Generated [4, 2], sending addition request... [INFO] [1563201593.479907]: Received response: 6 [INFO] [1563201594.474587]: Generated [2, 5], sending addition request... [INFO] [1563201594.480513]: Received response: 7 ... (omitted)
rosrun lab5 adder.py [INFO] [1563201590.020832]: Ready to add two ints. [INFO] [1563201592.477067]: Received [3, 3], returning 6 [INFO] [1563201593.478826]: Received [4, 2], returning 6 [INFO] [1563201594.480082]: Received [2, 5], returning 7 ... (omitted)
  • Now, while caller is running, kill adder. An error should occur in caller:
[INFO] [1563201595.589237]: Generated [2, 5], sending addition request... [INFO] [1563201595.597013]: Received response: 7 Traceback (most recent call last): File "/home/m2/catkin_ws/src/lab5/src/caller.py", line 23, in resp = calc_client(req) ... (omitted) rospy.service.ServiceException: service [/calc] unavailable
  • The error is due to the service /calc being unavailable. This is because the service server (in adder) is not running.
  • You can fix the above error by adding the following line after before the service client is called, such that it waits for the service to be available before calling it:
        req = AddTwoIntsRequest()
        req.first = a
        req.second = b

        rospy.wait_for_service('calc')

        resp = calc_client(req)
  • You can try killing adder while caller is running again. There should be no error this time.

Extra (optional): Difference between communication via topics and services

  • Communication via topics is broadcast : Once a node publishes a message to a topic, the callback functions of all nodes that are subscribed to that topic are invoked.
  • Communication via services is one-to-one : When a node makes a request to a service, only that node (client) and the node hosting the service (server) are involved.
  • It is difficult to imitate the response-request communication in services using topics:
    • One of the attempts of implementing the addition service example using topics may be as follows:
    • We can create two topic streams: calc_request and calc_response.
    • The request of caller is published onto calc_request, which is a message containing the two numbers.
    • adder can subscribe to calc_request, perform the arithmetic, and publish the result onto calc_response.
    • caller can receive the arithmetic result by subscribing to calc_response.
    • Not only is this implementation much more tedious, it also does not allow caller to easily distinguish which responses are corresponding to which requests, especially when multiple nodes would like to use the calc functionality.
  • Topics are generally used for continuous data flow (e.g. updating the reading of a sensor).
  • Services are generally used for procedures that terminate quickly (e.g. getter/setter of node states, quick calculations).
  • In services, the receiver (server) not running and hence not being able to receive data (requests) will cause an error in the service client node. In topics, the subscriber not running will not cause the publisher to have an error.

Checklist and Assignment

Completion

Congratulations! You have successfully run a service server and client and make two nodes communicate with each other!

Now, you should have enough knowledge to complete the remaining tasks in Assignment 1 (m2_ps4 manual control).

  • Hosting a service server (declare server, self-defined callback)
  • Using a service client (declare client object, call function)
  • Command line tools: rosservice
  • Difference between communication via topics and services

References