ROS2 Services

Services in ROS2 are another type of communication between different ROS2 nodes. After you learned how to use Topics, you might wonder, why you would need something else for communication, right?

As explained in the overview, different communication types are used for different purposes. While Topics are mainly used for continuous streams of data, Services will be used for tasks where a constant data stream would be overkill, especially when the data exchange is less frequent. Another use case would be when your program relies on receiving feedback from a sent information, for example when your robot finished a task.

You can find more information about Services in the official ROS2 documentation.

What are Services?

A Service is a type of communication that adopts the idea of a handshake protocol as it is implemented by having a client application that will send a request to the server to perform a task. After the server has finished the task, it will send a message back to the client to notify it of the result. The client will patiently wait for a response before it will continue with other tasks. Especially this last behaviour is one of the major drawbacks of using services as the client is basically unresponsive while waiting for the response. The advantage is that it provides a simple and elegant solution that receives a response rather than broadcasting into the unknown while producing a massive amount of unnecessary data traffic.

If you would like to reproduce the same strategy with Topics, you would need to have two publishers and two subscribers that constantly send the request and the answer to make sure the message has been sent and the result has been received. This would create unnecessary data traffic to obtain the same result.

After all these explanations, let’s have a look at a service message. As already mentioned, there is two-way communication which means the messages have two components. An example is the service message /turtle1/set_pen which is a message for the TurtleSim program seen earlier.

Launch the turtlesim node with:

$ ros2 run turtlesim turtlesim_node

Now, you can have a look at the services that are available with the following command:

$ ros2 service list

The following list will appear:

ros2 service list with the turtlesim application running

We can see there is a service called /turtle1/set_pen. We can have a closer look at this service with the following command:

$ ros2 service type /turtle1/set_pen

This command will return the message type for this service. In this case, the service type is turtlesim/srv/SetPen.

/turtle1/set_pen message type

To get more information about this service message type, use the interface command:

$ ros2 interface show turtlesim/srv/SetPen

This command will show you the service message description.

turtlesim/srv/SetPen interface description

For the /turtle1/set_pen message, this message looks as follows:

uint8 r
uint8 g
uint8 b
uint8 width
uint8 off
---

In general, the service message has a request and a response part, with their respective data types (which can be any type such as int8, uint16, string, bool, …), that are separated by three dashes as seen below:

requestType request
---
responseType response

Note: The request can be composed of several components or it can be empty, like in the SetPen interface. The same holds true for the response. In the above example, the values of the three base colours are given as part of the request while the response is an empty message. This means that there is a response, but it does not contain any data. Pay attention that, even though the response is empty, it needs to be sent as otherwise, the client will keep waiting for a response forever.

The message type of the /turtle1/set_pen message is a /turtlesim/srv/SetPen message, a custom message for this application. There are only a few standard message types for services as they are often very specific for a task. It is fairly common to create a new service message for each service used, unlike topics that usually rely on standard message types.

Using Services in Terminal

In case you only want to get some information about existing Services or you just want to test the function of a service, there is no need to write a ROS2 program to do that. Instead, you can simply use the terminal to do the job.

Starting TurtleSim

If you have no turtlesim node running, you can start it with the following command:

$ ros2 run turtlesim turtlesim_node

The first thing you need to know is, how to find which Services are already available. This allows you to use the Services that already exist.

Finding Information about the Services

As seen above, you can show a list of the existing Services, use the following command:

$ ros2 service list

This command will list all the Services that are already available. The Services available with the turtesim node are the following:

/clear
/kill
/reset
/spawn
/turtle1/set_pen
/turtle1/teleport_absolute
/turtle1/teleport_relative
/turtlesim/describe_parameters
/turtlesim/get_parameter_types
/turtlesim/get_parameters
/turtlesim/list_parameters
/turtlesim/set_parameters
/turtlesim/set_parameters_atomically

Let’s have a closer look at the /spawn service:

$ ros2 service type /spawn

This Service is of the type turtlesim/srv/Spawn. Let’s have a look at its description:

float32 x
float32 y
float32 theta
string name # Optional.  A unique name will be created and returned if this is empty
---
string name

Looks like this Service requires 4 different fields to be called.

Calling the Service

You can call the Service as follows:

$ ros2 service call /spawn turtlesim/srv/Spawn "{x: 2, y: 2, theta: 0, name: 'foxy_turtle'}"

When you execute this command, you will see the following response:

waiting for service to become available...
requester: making request: turtlesim.srv.Spawn_Request(x=2.0, y=2.0, theta=0.0, name='foxy_turtle')

response:
turtlesim.srv.Spawn_Response(name='foxy_turtle')

And you will see that a second turtle has appeared in the turtlesim node:

spawning a new turtle with the spawn service

Moreover, the new turtle has its own set of Services. You can find them with the following command:

$ ros2 service list

The following new Services are available now:

/foxy_turtle/set_pen
/foxy_turtle/teleport_absolute
/foxy_turtle/teleport_relative

These new Services use the name specified in the Service call: /foxy_turtle.

ros2 service list to see new services

In the previous part about Topics, you learned how to Subscribe to Topics and how to Publish them. In this case, though a Service also consists of two parts, you can only use one part in the terminal: the Service Client. The Client sends a request (like you did through the terminal) to a Service Server. The Server performs an algorithm with a return value that is returned back to the Client. As the Server part is usually more complex, it cannot be done in the terminal.

As with the Topics, using the terminal to interact with Services is usually only done for testing or quickly checking if the Services are running.

Using Services in Python

The usual method of using Services is through ROS2 nodes. In the next parts, you will see how to use Services with Python.

As mentioned earlier, there are two parts of the Service: the Client and the Server. These two parts can be split up into two separate Python nodes.

Service Servers

The Server is the part of a Service that is being called and performs an action as a result. When the action is finished, the Server provides a response to the Client that sent the Request.

First, go to the directory containing the Python programs that you made earlier.

cd ~/ros2_ws/src/my_turtlesim/my_turtlesim/

Now, create a new empty file and open it with your text editor.

gedit my_service_server.py

Before you can type the code for running your Service Server node, you want to decide which message type you will use for calling the Service. There are some standard message types that you can use (see here). Let’s say you want to create a service that will allow you to make the turtle move in a circle after the service call. The message will not contain any data except for the information on when to start but you do want to know if the message has been received correctly. Therefore, the Trigger Service message seems ideal as there is no input data and you receive feedback.

Have a look at the Trigger message description:

ros2 interface show std_srvs/srv/Trigger

The Trigger message contains the following information:

---
bool success   # indicate successful run of triggered service
string message # informational, e.g. for error messages

The idea is that the client can send a message to the server to request that the turtle starts moving. As soon as the service call is received the server will reply and make the turtle move in circles.

The code for the Service Server will look as seen here below. Note that the code also contains code to publish to a Topic.

import rclpy
from rclpy.node import Node
from std_srvs.srv import Trigger
from geometry_msgs.msg import Twist


class MyServiceServer(Node):

    def __init__(self):
        super().__init__('my_service_server')
        self.my_service = self.create_service(Trigger, 'draw_circle', self.draw_circle_callback)
        self.publisher_ = self.create_publisher(Twist, 'turtle1/cmd_vel', 10)

    def draw_circle_callback(self, request, response):
        request # request must be specified even if it is not used
        self.get_logger().info('Received request to draw a circle!')
        response.success = True
        response.message = "Starting to draw a circle!"
        timer_period = 0.5  # seconds
        self.timer = self.create_timer(timer_period, self.publish_velocity_callback)
        self.i = 0

        return response

    def publish_velocity_callback(self):
        my_velocity = Twist()
        my_velocity.linear.x = 0.5
        my_velocity.angular.z = 0.5
        self.publisher_.publish(my_velocity)
        self.get_logger().info(f"Publishing velocity: \
            \n\t linear.x: {my_velocity.linear.x}; \
            \n\t linear.z: {my_velocity.linear.x}")
        self.i += 1


def main(args=None):
    rclpy.init(args=args)

    my_service_server = MyServiceServer()

    rclpy.spin(my_service_server)
    rclpy.shutdown()


if __name__ == '__main__':
    main()

In the first part, you only tell ROS2 which modules you import. In this case, you need to import the Service message of the type std_srvs/srv/Trigger and also the geometry_msgs/msg/Twist message for publishing the velocity in a Topic.

import rclpy
from rclpy.node import Node
from std_srvs.srv import Trigger
from geometry_msgs.msg import Twist

Next, you create a class that inherits from the Node class. In its __init__() function, you define the node name, the publisher for the velocity commands and the Service server. The Service takes the message type, the Service name and the callback function as arguments.

class MyServiceServer(Node):

    def __init__(self):
        super().__init__('my_service_server')
        self.my_service = self.create_service(Trigger, 'draw_circle', self.draw_circle_callback)
        self.publisher_ = self.create_publisher(Twist, 'turtle1/cmd_vel', 10)

The moment that a service call is received, it will trigger a callback function in the code. In this case, the function is responsible to make the TurtleSim move in circles. This is done by creating a publisher and then sending messages to the topic /turtle1/cmd_vel. In this implementation, the topic is published repeatedly in a timer callback function. The function finishes by returning the service response. At this point, the turtle starts drawing circles.

    def draw_circle_callback(self, request, response):
        request # request must be specified even if it is not used
        self.get_logger().info('Received request to draw a circle!')
        response.success = True
        response.message = "Starting to draw a circle!"
        timer_period = 0.5  # seconds
        self.timer = self.create_timer(timer_period, self.publish_velocity_callback)
        self.i = 0

        return response

The callback function of the timer simply sets the velocity command to be published and prints the command message to the screen.

    def publish_velocity_callback(self):
        my_velocity = Twist()
        my_velocity.linear.x = 0.5
        my_velocity.angular.z = 0.5
        self.publisher_.publish(my_velocity)
        self.get_logger().info(f"Publishing velocity: \
            \n\t linear.x: {my_velocity.linear.x}; \
            \n\t linear.z: {my_velocity.linear.x}")
        self.i += 1

The main function initialises the ROS2 communication and creates an instance of our MyServiceServer class. Then, it keeps the node alive until CTRL+C is pressed.

def main(args=None):
    rclpy.init(args=args)

    my_service_server = MyServiceServer()

    rclpy.spin(my_service_server)
    rclpy.shutdown()


if __name__ == '__main__':
    main()

Save the code with your text editor and close it. Don’t forget to make your Python program executable:

$ chmod +x my_service_server.py

You need to add the new ROS2 Python script to the setup.py file. Open it with:

$ gedit ~/ros2_ws/src/my_turtlesim/setup.py

Add an entry for your new Python program in the entry_points so it should look as follows:

entry_points={
    'console_scripts': [
        'my_first_node = my_turtlesim.my_first_program:main',
        'my_simple_publisher = my_turtlesim.my_simple_publisher:main',
        'my_simple_subscriber = my_turtlesim.my_simple_subscriber:main',
        'my_service_server = my_turtlesim.my_service_server:main',
    ],
},

Now go to the root directory of your workspace and build the package:

$ cd ~/ros2_ws/
$ colcon build --packages-select my_turtlesim --symlink-install
$ source install/local_setup.bash

The colcon build command should build the package without any issues.

colcon build the my_turtlesim package

If you want to run and test the new service server, you will need to have 3 terminals: one for the turtlesim node, one for the Service Server node and one to call the Service.

In the first terminal, start the turtlesim node:

$ ros2 run turtlesim turtlesim_node

In the second terminal, start the Service Server:

$ ros2 run my_turtlesim my_service_server

In the third terminal, call the Service:

$ ros2 service call /draw_circle std_srvs/srv/Trigger {}

You will see that the turtle will start moving after you called the Service:

turtle making circles after calling the /draw_circle service

After executing the command, you can see how the turtle starts moving in a circle.

Calling the Service from the terminal is useful to test if the Service s working properly. On a real robot, it is more likely that the Service is called by another ROS2 program. This is called a Service Client.

Service Clients

The Client is the part that calls the Service. This means it sends a request to the Server and then waits for a response. One node can consist of several Clients that call different Services.

To start, you can go to the package you already made earlier:

$ cd ~/ros2_ws/src/my_turtlesim/my_turtlesim/

Now you can create a new file called my_service_client.py in which you will write the Python code to create a Service Client node:

$ gedit my_service_client.py

Now, an empty text editor window will pop up where you can type down the following code:

import rclpy
from rclpy.node import Node
from std_srvs.srv import Trigger


class MyServiceClient(Node):

    def __init__(self):
        super().__init__('my_service_client')
        self.my_client = self.create_client(Trigger, 'draw_circle')
        while not self.my_client.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('Waiting for service to become avilable...')
        self.req = Trigger.Request()

    def send_request(self):
        self.future = self.my_client.call_async(self.req)
        rclpy.spin_until_future_complete(self, self.future)
        return self.future.result()


def main(args=None):
    rclpy.init(args=args)

    my_service_client = MyServiceClient()
    response = my_service_client.send_request()
    my_service_client.get_logger().info(
        f'Result for triggering "Draw Circle: \
        \n\tSuccessful: {response.success} \
        \n\tMessage: {response.message}')

    my_service_client.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

What this code does, is first import the rclpy library and the Service message type Trigger from the subfolder srv from the ROS package called std_srvs.

import rclpy
from rclpy.node import Node
from std_srvs.srv import Trigger

The next part creates a class inherited from the Node class. This class, called MyServiceClient initiates the node with the node name my_service_client and creates a Service Client instance using the Service message type and the name of the Service. Next, we create an empty Service request message.

class MyServiceClient(Node):

    def __init__(self):
        super().__init__('my_service_client')
        self.my_client = self.create_client(Trigger, 'draw_circle')
        while not self.my_client.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('Waiting for service to become avilable...')
        self.req = Trigger.Request()

The function send_request() defines a variable called self.future which receives the response from the Service Server. Then, the program spins until the response has been received with the rclpy.spin_until_future_complete() function. In this function, you need to specify which variable contains the received response. The variable name self.future can be chosen freely, but for clarity, it contains the word future to specify it is the response that will be obtained in the near future after making the request. The function ends with returning the received answer.

    def send_request(self):
        self.future = self.my_client.call_async(self.req)
        rclpy.spin_until_future_complete(self, self.future)
        return self.future.result()

The main function initialises the ROS2 communication and then creates an instance of the node class. Then, we added a log entry to display in the terminal that we created a service request for the service /draw_circle. We end the program by destroying the node and shutting down the program.

def main(args=None):
    rclpy.init(args=args)

    my_service_client = MyServiceClient()
    response = my_service_client.send_request()
    my_service_client.get_logger().info(
        f'Result for triggering "Draw Circle: \
        \n\tSuccessful: {response.success} \
        \n\tMessage: {response.message}')

    my_service_client.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

Save the code and then make the file executable with the following command:

$ chmod +x my_service_client.py

Now, add the new node to the setup.py file:

$ gedit ~/ros2_ws/src/my_turtlesim/setup.py

And add a new entry:

entry_points={
    'console_scripts': [
        'my_first_node = my_turtlesim.my_first_program:main',
        'my_simple_publisher = my_turtlesim.my_simple_publisher:main',
        'my_simple_subscriber = my_turtlesim.my_simple_subscriber:main',
        'my_service_server = my_turtlesim.my_service_server:main',
        'my_service_client = my_turtlesim.my_service_client:main',
    ],
},

Save the file and close it. Now go to the root directory of your workspace, build the package and source the workspace:

$ cd ~/ros2_ws/
$ colcon build --packages-select my_turtlesim --symlink-install
$ source install/local_setup.bash

Now, you can test the Service Client. For this, you need 3 terminals: one for the turtlesim node, one for the Service Server node and one to call the Service with your new node.

In the first terminal, start the turtlesim node:

$ ros2 run turtlesim turtlesim_node

In the second terminal, start the Service Server:

$ ros2 run my_turtlesim my_service_server

In the third terminal, run the Service Client:

$ ros2 run my_turtlesim my_service_client

You will see that the turtle will start moving after you run the Service Client node:

run service client

If everything went correctly, the turtle should start moving and a message should appear that the Service call was successful.

Since the program is only spinning until the response has been received, the program will stop automatically. This also means that the program is not responsive until the response has been received. If the server is taking longer or is not responding at all, the client node can be stuck. This could disrupt the behaviour of the robot. Therefore, Services should only be used for tasks where the server will finish quickly such as taking a picture, saving a map or maybe closing or opening a gripper. For tasks that take longer, it is recommended to use ROS2 Actions. Since ROS2 Actions are a bit more advanced, they will not be covered here.