.. redirect-from:: Tutorials/Writing-A-Simple-Py-Service-And-Client .. _PySrvCli: Writing a simple service and client (Python) ============================================ .. container:: simple-ros-banner These are the tutorial docs for :doc:`simple-ros `, borrowed from the `ros2 project `__. **Goal:** Create and run service and client nodes using Python. **Tutorial level:** Beginner **Time:** 20 minutes .. contents:: Contents :depth: 2 :local: Background ---------- When :doc:`nodes <../Beginner-CLI-Tools/Understanding-ROS2-Nodes/Understanding-ROS2-Nodes>` communicate using :doc:`services <../Beginner-CLI-Tools/Understanding-ROS2-Services/Understanding-ROS2-Services>`, the node that sends a request for data is called the client node, and the one that responds to the request is the service node. The structure of the request and response is determined by a ``.srv`` file. The example used here is a simple integer addition system; one node requests the sum of two integers, and the other responds with the result. Prerequisites ------------- In previous tutorials, you learned how to :doc:`create a workspace <./Creating-A-Workspace/Creating-A-Workspace>` and :doc:`create a package <./Creating-Your-First-ROS2-Package>`. Tasks ----- 1 Create a package ^^^^^^^^^^^^^^^^^^ Open a new terminal and :doc:`source your ROS 2 installation <../Beginner-CLI-Tools/Configuring-ROS2-Environment>` so that ``ros2`` commands will work. Navigate into the ``ros2_ws`` directory created in a :ref:`previous tutorial `. Recall that packages should be created in the ``src`` directory, not the root of the workspace. Navigate into ``ros2_ws/src`` and create a new package: .. code-block:: console $ ros2 pkg create --build-type ament_python --license Apache-2.0 py_srvcli --dependencies rclpy example_interfaces Your terminal will return a message verifying the creation of your package ``py_srvcli`` and all its necessary files and folders. The ``--dependencies`` argument will automatically add the necessary dependency lines to ``package.xml``. ``example_interfaces`` is the package that includes `the .srv file `__ you will need to structure your requests and responses: .. code-block:: bash int64 a int64 b --- int64 sum The first two lines are the parameters of the request, and below the dashes is the response. 1.1 Update ``package.xml`` ~~~~~~~~~~~~~~~~~~~~~~~~~~ Because you used the ``--dependencies`` option during package creation, you don't have to manually add dependencies to ``package.xml``. As always, though, make sure to add the description, maintainer email and name, and license information to ``package.xml``. .. code-block:: xml Python client server tutorial Your Name Apache-2.0 1.2 Update ``setup.py`` ~~~~~~~~~~~~~~~~~~~~~~~ Add the same information to the ``setup.py`` file for the ``maintainer``, ``maintainer_email``, ``description`` and ``license`` fields: .. code-block:: python maintainer='Your Name', maintainer_email='you@email.com', description='Python client server tutorial', license='Apache-2.0', 2 Write the service node ^^^^^^^^^^^^^^^^^^^^^^^^ Inside the ``ros2_ws/src/py_srvcli/py_srvcli`` directory, create a new file called ``service_member_function.py`` and paste the following code within: .. code-block:: python from example_interfaces.srv import AddTwoInts import rclpy from rclpy.executors import ExternalShutdownException 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(): try: with rclpy.init(): minimal_service = MinimalService() rclpy.spin(minimal_service) except (KeyboardInterrupt, ExternalShutdownException): pass if __name__ == '__main__': main() 2.1 Examine the code ~~~~~~~~~~~~~~~~~~~~ The first ``import`` statement imports the ``AddTwoInts`` service type from the ``example_interfaces`` package. The following ``import`` statements import the necessary ROS 2 Python client library interfaces. .. code-block:: python from example_interfaces.srv import AddTwoInts import rclpy from rclpy.executors import ExternalShutdownException from rclpy.node import Node The ``MinimalService`` class constructor initializes the node with the name ``minimal_service``. Then, it creates a service and defines the type, name, and callback. .. code-block:: python def __init__(self): super().__init__('minimal_service') self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback) The definition of the service callback receives the request data, sums it, and returns the sum as a response. .. code-block:: python 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 Finally, the main class initializes the ROS 2 Python client library, instantiates the ``MinimalService`` class to create the service node and spins the node to handle callbacks. 2.2 Add an entry point ~~~~~~~~~~~~~~~~~~~~~~ To allow the ``ros2 run`` command to run your node, you must add the entry point to ``setup.py`` (located in the ``ros2_ws/src/py_srvcli`` directory). Add the following line between the ``'console_scripts':`` brackets: .. code-block:: python 'service = py_srvcli.service_member_function:main', 3 Write the client node ^^^^^^^^^^^^^^^^^^^^^^^ Inside the ``ros2_ws/src/py_srvcli/py_srvcli`` directory, create a new file called ``client_member_function.py`` and paste the following code within: .. code-block:: python from example_interfaces.srv import AddTwoInts import rclpy from rclpy.executors import ExternalShutdownException 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): self.req.a = 41 self.req.b = 1 return self.cli.call_async(self.req) def main(args=None): try: with rclpy.init(args=args): minimal_client = MinimalClientAsync() future = minimal_client.send_request() 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' % (minimal_client.req.a, minimal_client.req.b, response.sum)) except (KeyboardInterrupt, ExternalShutdownException): pass if __name__ == '__main__': main() 3.1 Examine the code ~~~~~~~~~~~~~~~~~~~~ As with the service code, we first ``import`` the necessary libraries. .. code-block:: python from example_interfaces.srv import AddTwoInts import rclpy from rclpy.executors import ExternalShutdownException from rclpy.node import Node 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. .. code-block:: python 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() Below the constructor is the ``send_request`` method, which will send the request and spin until it receives the response or fails. .. code-block:: python def send_request(self): self.req.a = 41 self.req.b = 1 return self.cli.call_async(self.req) Finally we have the ``main`` method, which constructs a ``MinimalClientAsync`` object, sends the request using the passed-in command-line arguments, calls ``rclpy.spin_until_future_complete`` to wait for the result, and logs the results. .. code-block:: python def main(args=None): try: with rclpy.init(args=args): minimal_client = MinimalClientAsync() future = minimal_client.send_request() 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' % (minimal_client.req.a, minimal_client.req.b, response.sum)) except (KeyboardInterrupt, ExternalShutdownException): pass .. warning:: Do not use ``rclpy.spin_until_future_complete`` in a ROS 2 callback. 3.2 Add an entry point ~~~~~~~~~~~~~~~~~~~~~~ Like the service node, you also have to add an entry point to be able to run the client node. The ``entry_points`` field of your ``setup.py`` file should look like this: .. code-block:: python entry_points={ 'console_scripts': [ 'service = py_srvcli.service_member_function:main', 'client = py_srvcli.client_member_function:main', ], }, 4 Build and run ^^^^^^^^^^^^^^^ It's good practice to run ``rosdep`` in the root of your workspace (``ros2_ws``) to check for missing dependencies before building: .. tabs:: .. group-tab:: Linux .. code-block:: console $ rosdep install -i --from-path src --rosdistro {DISTRO} -y .. group-tab:: macOS rosdep only runs on Linux, so you can skip ahead to next step. .. group-tab:: Windows rosdep only runs on Linux, so you can skip ahead to next step. Navigate back to the root of your workspace, ``ros2_ws``, and build your new package: .. code-block:: console $ colcon build --packages-select py_srvcli Open a new terminal, navigate to ``ros2_ws``, and source the setup files: .. tabs:: .. group-tab:: Linux .. code-block:: console $ source install/setup.bash .. group-tab:: macOS .. code-block:: console $ . install/setup.bash .. group-tab:: Windows .. code-block:: console $ call install/setup.bat Now run the service node: .. code-block:: console $ ros2 run py_srvcli service The node will wait for the client's request. Open another terminal and source the setup files from inside ``ros2_ws`` again. Start the client node. The client sends the request to the service, which computes the sum and returns the result: .. code-block:: console $ ros2 run py_srvcli client [INFO] [minimal_client_async]: Result of add_two_ints: for 41 + 1 = 42 Return to the terminal where your service node is running. You will see that it published log messages when it received the request: .. code-block:: console [INFO] [minimal_service]: Incoming request a: 41 b: 1 Enter ``Ctrl+C`` in the server terminal to stop the node from spinning. Summary ------- You created two nodes to request and respond to data over a service. You added their dependencies and executables to the package configuration files so that you could build and run them, allowing you to see a service/client system at work. Next steps ---------- Next, you'll learn how to :doc:`use parameters in a class <./Using-Parameters-In-A-Class-Python>`. Related content --------------- * In this tutorial, you used the ``call_async()`` API in your client node to call the service. There is another service call API available for Python called synchronous calls, which we do not recommend using.