Introduction to Programming with ROS2-Services
This post is the second of a three part series introducing basic concepts of message passing and communication in ROS2. In my previous post, I introduced topics and in this one I will introduce services in ROS2. As in the previous post, I use ROS2 Crystal to demonstrate the concepts below. The next post on actions will be available soon.
Services
ROS2 Services provide a client-server based model of communication between ROS2 nodes. As in the case of topics, ROS2 services are very similar to their ROS1 counterparts from the practical perspective. In this post we will create a very simple service that allows the client to send two numbers to the server. The server sums them up and returns the result to the client. We will create 3 packages, similar to what we did for ROS2 topics-a custom srv package, a server in C++ and a client in Python.
The client libraries for C++ and Python are the only ones maintained by ROS2 core team. However, one of the great things with ROS2 being open source is that people have also developed client libraries for other languages like Java, GO, etc, making it possible to implement nodes in these languages as well.
Unlike my previous post, I will only go through the process of building packages briefly here. So in case of doubts during the build process please refer to my previous post.
Creating a srv package
First let us create a ROS2 workspace. We can also reuse the workspace we used to create ROS2 topics. We shall then create a package for our custom srv. Similar to ROS1, both messages and services have the same dependencies.
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
ros2 pkg create my_services --dependencies builtin_interfaces rosidl_default_generators --build-type ament_cmake
Add an srv folder to store the services and create a new .srv file for our new service. The structure of srv files in ROS2 are similar to those of ROS1. For our case of creating a service for adding two numbers one simple way to create the srv file would be as follows.
float64 a
float64 b
---
float64 sum
Next we need to build this package. This process is exactly the same as the one we followed to build our msg package previously. So go ahead and try to make the necessary modifications to the package.xml and CMakeLists.txt files and come back here if you get stuck along the way.
The code to be added to the CMakeLists.txt file is as follows.
set(
srv_files
"srv/Add.srv") rosidl_generate_interfaces(
${PROJECT_NAME}
${srv_files}
DEPENDENCIES builtin_interfaces) ament_export_dependencies(rosidl_default_runtime)
The code to be added to the package.xml file is as follows. Remember to change the format for the package.xml to “3”.
<build_depend>builtin_interfaces</build_depend> <buildtool_depend>rosidl_default_generators</buildtool_depend> <exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
Finally we can build the package from the workspace using colcon build. Once the packages is built, source the required files and then check if our service has been installed correctly using the ros2 srv command
cd ~/ros2_ws
colcon build --symlink-install --packages-select my_services
ros2 srv list | grep my_services
The ros2 srv command should display my_services/Add.srv. This confirms that the service has been installed correctly and that we can move on to the server.
Server
We shall implement our server in C++. First let us create the new package.
ros2 pkg create server --dependencies my_services rclcpp --build-type ament_cmake
Commands like the one above should be familiar by now. A great resource of the available CLI tools with ROS2 is available here. Next, let us create our server.cpp file inside the src folder of the package.
First let us include the necessary header files.
#include "my_service/srv/add.hpp"
#include "rclcpp/rclcpp.hpp"
#include <iostream>
#include <memory>
Then we shall create a server class that inherits the rclcpp Node class as this is the recommended way of implementing nodes in ROS2 and is also the most clean way of doing so.
class Server : public rclcpp::Node {
public: Server() : Node("my_server") {
auto handle_add = [this](
const std::shared_ptr<rmw_request_id_t> request_header,
const std::shared_ptr<my_service::srv::Add::Request> request,
std::shared_ptr<my_service::srv::Add::Response> response) ->
void {
(void)request_header;
response->sum = request->a + request->b;
RCLCPP_INFO(this->get_logger(), "Incoming request %f %f. Response is %f", request->a, request->b, response->sum);
};
srv_ = this->create_service<my_service::srv::Add>("add_floats", handle_add);
}
private:
rclcpp::Service<my_service::srv::Add>::SharedPtr srv_;
};
The create_service method of the Node class is used to create a server, that listens for requests on the “add_floats” service. The second argument of this method is the request handler. Here, requests are handled using a lambda expression that takes the request_header, request and response as input. The expression simply fills up the sum field of the response. RCLCPP_INFO is a mechanism for logging information to the standard output. It is ROS2 equivalent of the ROS_INFO() function in ROS1.
We then create an instance of the Server class in the main function and allow it to spin forever.
int main(int argc, char *argv[]) {
rclcpp::init(argc, argv);
auto node = std::make_shared<Server>();
rclcpp::spin(node);
return 0;
}
With the code for our server ready, we can now move on to building the package. Once again try to modify the CMakeLists.txt and package.xml files and come back here if you get stuck somewhere along the way.
add_executable(server src/server.cpp)ament_target_dependencies(server rclcpp my_service) install(
TARGETS server
DESTINATION lib/${PROJECT_NAME})
This is the additions to the CMakeLists.txt file and the additions to the package.xml file are as follows.
<build_depend>rclcpp</build_depend> <build_depend>my_service</build_depend> <exec_depend>my_service</exec_depend> <exec_depend>rclcpp</exec_depend>
We then follow procedures that should be familiar by now- build the package, source the setup file and test if the package has been built correctly by running it.
cd ~/ros2_ws
colcon build --symlink-install --packages-select server
source install/setup.bash
ros2 run server server
We can check if the service is running by using the ros2 service command. Using the list option along with this command lists all the running services and the service “add_floats” must be one among the services that are currently up and running. We can also use the call option along with this command to call the server from the command line
ros2 service list
ros2 service call /add_floats my_services/Add.srv "{a: 2, b: 3}"
The second command calls the server and gives you the result i.e. 5. Now, that the server is working we can move on to implementing the client.
Client
We shall implement the client in Python. Currently, Python packages are a little more trickier to develop than C++ packages due to the lack of CLI support. We cannot use the build-type argument with the parameter ament_python. This forces us to add the files required to build a Python package manually.
First let us create a new package for the client. Once the package is created we need to remove the CMakeLists.txt file since this is a Python package.
ros2 pkg create client --dependencies my_services rclpy
Next we create two folders inside the package. One folder has the same name as the package name. This folder represents a normal python package and therefore, it must have an __init__.py file. For our purposes, this file can be left empty. We then create a resource folder containing an empty file.
cd src/client
mkdir client
mkdir resources
cd resources
touch client
cd ../client
touch __init__.py
Next we can create our Python module client.py within the client folder. First we need to import required packages and modules
import rclpy
from rclpy.node import Node
from my_service.srv import Add
import random
Next we create a Client Class inheriting the rclpy.node.Node class
class Client(Node):
def __init__(self):
super().__init__('client')
self.client = self.create_client(Add, 'add_floats')
self.request = Add.Request()
while not self.client.wait_for_service(timeout_sec = 10.0):
self.get_logger().info('Waiting for service')
def send_request(self):
self.request.a = random.uniform(2043, 343294)
self.request.b = random.uniform(3234, 45849054)
wait = self.client.call_async(self.request)
rclpy.spin_until_future_complete(self, wait)
if wait.result() is not None:
self.get_logger().info("Request was '%f' '%f'. Response is '%f'" %(self.request.a, self.request.b, wait.result().sum))
else:
self.get_logger().info("Request failed")
In the class we initialize the node ‘client’. Then we create a service client using the create_client() method. In the next line we create a request object for the Add service. We then wait for the service to be started by the server.
The send_request function is called whenever a request is to be sent to the server. Here, we have implemented a synchronous method of sending calls to the server. However, asynchronous implementations can also be done and I have included links to Github repositories containing such examples in the references. The send_request fills the request object and calls the server. It then waits until a reply arrives from the server and does some sanity checks to ensure that the call is successful before printing the output to the standard output.
Next, we implement the main function.
def main(args = None):
rclpy.init(args = args)
node = Client()
while rclpy.ok():
node.send_request()
rclpy.spin_once(node)
node.destroy_node()
rclpy.shutdown() if __name__ == "__main__":
main()
In the main method we first initialize the Python module and create an instance of the Client class. The while loop that follows executes as long as the node is up and running. During each iteration of this loop we send a request to server and wait for the response before proceeding to the next iteration. The last two lines are necessary to ensure clean termination.
Now we can move on to building the package. This process is slightly tricky and since I had explained when I created the subscriber for ROS2 topics, I will go over it briefly here. First we create the setup.py and setup.cfg files.
from setuptools import setup
package_name = 'client'setup(
name=package_name,
version='0.0.0',
packages=[package_name],
data_files=[
('share/ament_index/resource_index/packages', ['resource/' + package_name]), ('share/' + package_name, ['package.xml']), ('share/' + package_name, ['launch/client.launch.py']),],
install_requires=['setuptools'],
zip_safe=True,
author='Daniel Jeswin',
author_email='danieljeswin@gmail.com',
maintainer='Daniel Jeswin'
maintainer_email='danieljeswin@gmail.com',
keywords=['ROS'],
classifiers=[ 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python', 'Topic :: Software Development', ],
description=' Examples of service client using rclpy.',
license='Apache License, Version 2.0',
tests_require=['pytest'],
entry_points={'console_scripts': ['client = client.client:main',],},)
This template can be used for any other Python package. The main parameters to be changed are the package name, version, data_files and entry_points. The setup.cfg file is as follows.
[develop]
script-dir=$base/lib/client
[install]
install-scripts=$base/lib/client
Finally we edit the package.xml file. The buildtool and export dependencies on ament_cmake need to be removed first and the following code has to be added.
<exec_depend>rclpy</exec_depend>
<exec_depend>my_msgs</exec_depend>
<export>
<build_type>ament_python</build_type>
</export>
The complete implementation of everything in this post is available in my Github repository. Now we can build the package using colcon build and then run both the client and the server.
cd ~/ros2_ws
colcon build --symlink-install --packages-select client
source install/setup.bash
ros2 run server server
ros2 run client client
Now both the client and server are up and running and the output can be seen on the terminal. We have now reached the end of this introduction to services with ROS2. While we start the client and server separately using ros2 run commands, they can also be started with a single launch file. I will explain launch files in ROS2 in a separate post. Goodbye for now!