Note: This is part of a series where I follow along with the ROS 2 Humble and Gazebo Fortress tutorials and demos. This is not original content.
Today we’ll follow along with this tutorial from the ROS 2 Humble docs, which will teach us how to create a ROS 2 service and client using Python.
Here we go!
Background Link to heading
You can take a look at yesterday’s post for a more complete summary.
Essentially, ROS nodes form a ROS graph, and they talk to each other by sending messages. One way ROS nodes send messages is through services. A client sends a message to the service, and the service sends back a response. That’s the mode of communication we’ll be looking at today.
Getting set up Link to heading
Let’s create another package. We’ll reuse our ros2_ws_srvcli
workspace from last time. Mine is in ~/Documents
, so I the following command to move to my workspace’s src
directory:
cd ~/Documents/ros2_ws_srvcli/src
Next, we create a new package:
ros2 pkg create --build-type ament_python --license Apache-2.0 py_srvcli --dependencies rclpy example_interfaces
You’ll see some output like this:
going to create a new package
package name: py_srvcli
destination directory: /home/ubuntu/Documents/ros2_ws_srvcli/src
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['ubuntu <ubuntu@todo.todo>']
licenses: ['Apache-2.0']
build type: ament_python
dependencies: ['rclpy', 'example_interfaces']
creating folder ./py_srvcli
creating ./py_srvcli/package.xml
creating source folder
creating folder ./py_srvcli/py_srvcli
creating ./py_srvcli/setup.py
creating ./py_srvcli/setup.cfg
creating folder ./py_srvcli/resource
creating ./py_srvcli/resource/py_srvcli
creating ./py_srvcli/py_srvcli/__init__.py
creating folder ./py_srvcli/test
creating ./py_srvcli/test/test_copyright.py
creating ./py_srvcli/test/test_flake8.py
creating ./py_srvcli/test/test_pep257.py
If you run ls -1
(that’s the number one, 1
), you’ll see we have two packages in the src
directory now:
cpp_srvcli
py_srvcli
Great! Our C++ code is still there, and we now have a new Python package too.
Like last time, we’ve used the --dependencies
flag to automatically add our dependencies to package.xml
. We should still update the <description>
, <maintanier>
, and <license>
fields, of course.
Updating setup.py
Link to heading
Because we’re building a Python package, we also have to make sure seutp.py
has the correct fields in setup.py
, which are:
maintainer=
maintainer_email=
description=
license=
Write the service code Link to heading
Create a new file called service_member_function.py
inside the py_srvcli/py_srvcli
director (which currently holds __init__.py
). It should look like this:
from example_interfaces.srv import AddTwoInts
import rclpy
from rclpy.node import Node
class MinimalService(Node):
def __init__(self):
super().__init__('minimal_service')
self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback)
def add_two_ints_callback(self, request, response):
response.sum = request.a + request.b
self.get_logger().info('Incoming request\na: %d b: %d' % (request.a, request.b))
return response
def main():
rclpy.init()
minimal_service = MinimalService()
rclpy.spin(minimal_service)
rclpy.shutdown()
if __name__ == '__main__':
main()
Once again we’re creating a service (using the MinimalService
class constructor), giving the service the name add_two_ints
, and creating a callback which will accept requests containing two integers and return their sum as the response.
Add an entrypoint Link to heading
The code is ready to go, but we can’t launch our node with ros2 run
because we haven’t set up an entrypoint. We need to update service.py
to include the line 'service = py_srvcli.service_member_function:main'
inside the brackets for console_scripts
. Here’s a complete setup.py with this change already made:
from setuptools import find_packages, setup
package_name = 'py_srvcli'
setup(
name=package_name,
version='0.0.0',
packages=find_packages(exclude=['test']),
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='ubuntu',
maintainer_email='ubuntu@todo.todo',
description='TODO: Package description',
license='Apache-2.0',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'service = py_srvcli.service_member_function:main',
],
},
)
Write client code Link to heading
Again, inside the workspace’s src/py_srvcli/py_srvcli
folder, create a new .py
file called client_member_function.py
and the following content:
import sys
from example_interfaces.srv import AddTwoInts
import rclpy
from rclpy.node import Node
class MinimalClientAsync(Node):
def __init__(self):
super().__init__('minimal_client_async')
self.cli = self.create_client(AddTwoInts, 'add_two_ints')
while not self.cli.wait_for_service(timeout_sec=1.0):
self.get_logger().info('service not available, waiting again...')
self.req = AddTwoInts.Request()
def send_request(self, a, b):
self.req.a = a
self.req.b = b
return self.cli.call_async(self.req)
def main():
rclpy.init()
minimal_client = MinimalClientAsync()
future = minimal_client.send_request(int(sys.argv[1]), int(sys.argv[2]))
rclpy.spin_until_future_complete(minimal_client, future)
response = future.result()
minimal_client.get_logger().info(
'Result of add_two_ints: for %d + %d = %d' %
(int(sys.argv[1]), int(sys.argv[2]), response.sum))
minimal_client.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Again the client appears more complex than the service, because there’s a need to wait for the service to become available and wait for responses to come back after a request is made.
Note that it matters what the client’s base class is here. From the docs:
The MinimalClientAsync class constructor initializes the node with the name minimal_client_async. The constructor definition creates a client with the same type and name as the service node. The type and name must match for the client and service to be able to communicate. The while loop in the constructor checks if a service matching the type and name of the client is available once a second. Finally it creates a new AddTwoInts request object.
Add an entrypoint Link to heading
Again, we need to add an entrypoint so we can use ros2 run
to call the client later. With the new entrypoint added, setup.py
should now look like this:
from setuptools import find_packages, setup
package_name = 'py_srvcli'
setup(
name=package_name,
version='0.0.0',
packages=find_packages(exclude=['test']),
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='ubuntu',
maintainer_email='ubuntu@todo.todo',
description='TODO: Package description',
license='Apache-2.0',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'service = py_srvcli.service_member_function:main',
'client = py_srvcli.client_member_function:main',
],
},
)
From the root of the workspace, run rosdep
to make sure all the dependencies are there:
rosdep install -i --from-path src --rosdistro humble -y
Then, build the packages with:
colcon build --packages-select py_srvcli
Open two new terminals. In the first, run:
source install/setup.bash
ros2 run py_srvcli service
And in the second, run:
source install/setup.bash
ros2 run py_srvcli client 3 4
You should see a message like this in the terminal where the server is running:
[INFO] [1724063589.528090562] [minimal_service]: Incoming request
a: 3 b: 4
And one like this from the client:
[INFO] [1724063589.538533197] [minimal_client_async]: Result of add_two_ints: for 3 + 4 = 7
When you’re done, hit Ctrl+C
in each terminal to kill the client and service.
Awesome! We’ve now learned how to build both C++ and Python services and clients.
In the next post, we’ll take a look at creating custom msg and srv files