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.

And so we begin.

Background Link to heading

You might recall that ROS nodes have several ways to communicate with each other. Last week, we explored writing publishers and subscribers, which communicate by sending (and receiving) messages via a topic.

This is only one way for ROS nodes to communicate.

ROS nodes can also communicate with one another by having one node act as a service and another node act as a client. The client node sends a request to the service node, which then returns a response.

Unlike the publisher-subscriber model, where the publisher simply sends messages to the topic as they are generated (or in response to some event the publisher is triggered by), a node which is running as a service will wait for a request from a client before sending any messages.

Getting set up Link to heading

As before, we’re going to need to set up a ROS workspace and create a package, so let’s do that. I want to start with a clean workspace, so I’ll create a brand new one:

mkdir -p ros2_ws_srvcli
mkdir -p ros2_ws_srvcli/src

Next, move into our new workspace and create a package

cd ros2_ws_srvcli/src
ros2 pkg create --build-type ament_cmake --license Apache-2.0 cpp_srvcli --dependencies rclcpp example_interfaces

Note: The commands above do not include ~/ at the start of the dirctory paths, meaning all these commands will be executed relative to your current directory. Make sure you are where you want to be.

That second command, ros2 pkg create, should output something like this:

dencies rclcpp example_interfaces
going to create a new package
package name: cpp_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_cmake
dependencies: ['rclcpp', 'example_interfaces']
creating folder ./cpp_srvcli
creating ./cpp_srvcli/package.xml
creating source and include folder
creating folder ./cpp_srvcli/src
creating folder ./cpp_srvcli/include/cpp_srvcli
creating ./cpp_srvcli/CMakeLists.txt

Note: Because we used the --dependencies flag, the dependencies listed after that flag were automatically added to package.xml and CMakeLists.txt for us. Of course you should still add author, license, and description info to package.xml.

Creating the service Link to heading

Let’s move from ros2_ws/src/cpp_srvcli/src into the subdirectory cpp_srvcli/src:

cd cpp_srvcli/src

Now, create a new .cpp file called add_two_ints_server.cpp, which should contain the following code:

#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/srv/add_two_ints.hpp"

#include <memory>

void add(const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> request,
          std::shared_ptr<example_interfaces::srv::AddTwoInts::Response>      response)
{
  response->sum = request->a + request->b;
  RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Incoming request\na: %ld" " b: %ld",
                request->a, request->b);
  RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "sending back response: [%ld]", (long int)response->sum);
}

int main(int argc, char **argv)
{
  rclcpp::init(argc, argv);

  std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_server");

  rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr service =
    node->create_service<example_interfaces::srv::AddTwoInts>("add_two_ints", &add);

  RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Ready to add two ints.");

  rclcpp::spin(node);
  rclcpp::shutdown();
}

Take a quick look at the code above. It’s diong a couple of things:

  • Pulling in dependencies on rclcpp and the code in example_interfaces which will add two integers together
  • Creating an add function which will take two integers from the request and return their sum in the response
  • Initializing the ROS client library
  • Creating a new node named add_two_ints_server
  • Creating a new service called add_two_ints
  • Advertising that service to other ROS nodes over the network with &add
  • Printing out a log message once the service is ready to go
  • Running spin(node) (this makes the service available)

Adding an executable Link to heading

We need to be able to run our service with ros2 run. To do this, we need to add an add_executable line to CMakeLists.txt to create a new executable for us called server. Open up CMakeLists.txt and add the following lines:

add_executable(server src/add_two_ints_server.cpp)
ament_target_dependencies(server rclcpp example_interfaces)

Note: You can add these lines just above if(BUILD_TESTING).

We also need to add a line just above ament_package() to make sure ROS 2 run can find our executable:

install(TARGETS
    server
  DESTINATION lib/${PROJECT_NAME})

Let’s create the client, too.

Creating the client Link to heading

Our next order of business will be creating a client that can call our new add service. Here’s the complete code for the client, which should go in a new .cpp file called add_two_ints_client.cpp, in the same directory as the service:

#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/srv/add_two_ints.hpp"

#include <chrono>
#include <cstdlib>
#include <memory>

using namespace std::chrono_literals;

int main(int argc, char **argv)
{
  rclcpp::init(argc, argv);

  if (argc != 3) {
      RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "usage: add_two_ints_client X Y");
      return 1;
  }

  std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_client");
  rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedPtr client =
    node->create_client<example_interfaces::srv::AddTwoInts>("add_two_ints");

  auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
  request->a = atoll(argv[1]);
  request->b = atoll(argv[2]);

  while (!client->wait_for_service(1s)) {
    if (!rclcpp::ok()) {
      RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting.");
      return 0;
    }
    RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again...");
  }

  auto result = client->async_send_request(request);
  // Wait for the result.
  if (rclcpp::spin_until_future_complete(node, result) ==
    rclcpp::FutureReturnCode::SUCCESS)
  {
    RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Sum: %ld", result.get()->sum);
  } else {
    RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Failed to call service add_two_ints");
  }

  rclcpp::shutdown();
  return 0;
}

Despite looking a lot more complicated, the client code is doing much the same work as the service code. It is spinning up a new node and creating a client for that node. The reason the code looks more complex is that the client code also needs to handle the logic to wait for the service to become available.

Again, we need to update CMakeLists.txt with another add_executable, and we need to add our client node into install(). Here’s a completed CMakeLists.txt you can simply copy-paste (note we’ve removed the unused build test code):

cmake_minimum_required(VERSION 3.5)
project(cpp_srvcli)

find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(example_interfaces REQUIRED)

add_executable(server src/add_two_ints_server.cpp)
ament_target_dependencies(server rclcpp example_interfaces)

add_executable(client src/add_two_ints_client.cpp)
ament_target_dependencies(client rclcpp example_interfaces)

install(TARGETS
  server
  client
  DESTINATION lib/${PROJECT_NAME})

ament_package()

Build and run everything Link to heading

Build the package Link to heading

First, return to the root of the workspace. In my case, I have put my workspace in ~/Documents, so I would run:

cd ~/Documents/ros2_ws_srvcli/

Next, make sure all the required dependencies are installed:

rosdep install -i --from-path src --rosdistro humble -y

Great. Nowe we can build our packages (or in this case, our single package cpp_servcli):

colcon build --packages-select cpp_srvcli

Run the service and client Link to heading

We need to source our new package so ros2 run can find it:

source install/setup.bash

Then we start the server:

ros2 run cpp_srvcli server

It should return our logging message:

[INFO] [1724038738.754282070] [rclcpp]: Ready to add two ints.

Now we open a new terminal window and run the client (don’t forget to source again, as I do here):

source install/setup.bash
ros2 run cpp_srvcli client 7 2

Note that I gave the two arguments. These are required (after all, we need to tell the service which two numbers we’d like it to add together).

The response should be:

[INFO] [1724038877.071179514] [rclcpp]: Sum: 9

Also note that the service has logged the request:

[INFO] [1724038877.070956577] [rclcpp]: Incoming request
a: 7 b: 2
[INFO] [1724038877.070996389] [rclcpp]: sending back response: [9]

Cool! We can use Ctrl+C now to kill the client and server. That’s it for this post.

In the next post I’ll follow along with this part of the ROS docs, which focuses on doing the exact same thing, only with Python this time.