Using coroutines with service calls
Repo Link: https://github.com/ctu-mrs/mrs_lib/tree/ros2/examples/coro_service
Expected prerequisites:
ROS2 Packages
ROS2 Nodes
ROS2 Services
Coroutines allow suspending functions and resuming them later. In context of ROS2 programs, this is very useful when calling services. If you do not use coroutines, the options used are usually either blocking on the future (cannot be done in single threaded context) or manually checking it (requires timers for checking readiness) or specifying callbacks. Coroutines solve this by writing code, that looks almost like the blocking case, while being functionally similar to writing the rest of the function as a callback.
Note
This tutorial covers how to use coroutines specifically with service client in mrs_lib. If you want more general information and how they work, see eg. https://cppreference.com/cpp/language/coroutines.
Important parts
In this section, we will walk through the important parts of the example. The full source code can be found at the bottom of this page or in the examples folder of mrs_lib repo.
Coroutines
Syntactically, coroutines have two major differences normal C++ functions.
Firstly, they must contain at least one of the
co_*keywords (co_await,co_returnorco_yield).Secondly, the return type must be a coroutine return type. For our usecase, it is enough to know that
mrs_lib::Task<T>is such a type. The template parameterTis the type returned from the coroutine when awaited (defaults to void).
An example coroutine is shown in the following listing.
The coroutine returns std::optional<std::shared_ptr<ServiceType::Response>> which is wrapped in mrs_lib::Task.
It takes parameters just like a normal function.
Instead of the normal return keyword, we must use co_return (just coroutine version of the same).
The co_await keyword in front of the client call tells the language to suspend the current coroutine here and resume once the awaited result is ready.
In this case, it means it sends the service request, suspends the coroutine, and resumes it once the server’s response arives.
36mrs_lib::Task<std::optional<std::shared_ptr<ServiceType::Response>>> call_service(bool data)
37{
38 auto request = std::make_shared<ServiceType::Request>();
39 request->data = data;
40 co_return co_await service_client_.callAwaitable(request);
41}
As you can see, the coroutine function looks almost like a normal function, just with some co_* keywords sprinkled in.
Next we will have a look at a second coroutine in our program. This time, it will be a timer callback.
45mrs_lib::Task<> service_timer_callback()
46{
47 RCLCPP_INFO_STREAM(logger, "Calling service...");
48 std::optional<std::shared_ptr<ServiceType::Response>> response_opt = co_await call_service(true);
49 if (response_opt.has_value())
50 {
51 auto response = response_opt.value();
52 std::string response_str = std::format("success: '{}'\n message: '{}'", response->success, response->message);
53 RCLCPP_INFO_STREAM(logger, "Service response:\n" << response_str);
54 } else
55 {
56 RCLCPP_WARN_STREAM(logger, "Failed to call service!!!");
57 }
58}
Once again, you can see it uses the co_await keyword.
This is required whenever you are calling another coroutine, even if it returns void.
You may also notice that there is no co_return this time.
Similarly to normal functions, if the coroutine returns void, you can omit the co_return at the end.
However, you still have to have at least one co_* keyword in it to make it a coroutine.
Warning
The coroutines implemented in mrs_lib are so called lazy coroutines.
This means they do nothing until you await them (eg. co_await my_coro()).
As a result, if you forget to await a coroutine, it will not run!
Coroutines as callbacks
You can use coroutines as callbacks, but there are currently some additional restrictions:
The object which calling the callbacks must support calling coroutines. (See API reference for the individual callback sources if they have overloads for coroutine callbacks.)
The rclcpp::CallbackGroup used must be reentrant [1].
In the following listing, you can see the constructor of the example node.
Firstly, we initialize the parent node, a local copy of the logger, the required reentrant callback group and the service client.
Next we initialize the timer with our coroutine callback.
For timer, we use a constructor overload taking mrs_lib::TimerHandlerOptions (contains node and callback group), rate and the callback.
The coroutine callback is passed as two parts – the method pointer and a pointer to the instance with which it is invoked.
Finally, we also initialize another timer to see the executor is not blocked during the service call.
18CoroServiceExample(const rclcpp::NodeOptions& opts)
19 : Node("coro_service_example", opts), logger(this_node().get_logger()),
20 reentrant_callback_group_(this_node().create_callback_group(rclcpp::CallbackGroupType::Reentrant)),
21 service_client_(this_node_ptr(), "my_service", nullptr),
22 service_timer_(std::make_unique<mrs_lib::ROSTimer>(std::invoke([this] {
23 mrs_lib::TimerHandlerOptions opts{this_node_ptr()};
24 opts.callback_group = reentrant_callback_group_;
25 return opts;
26 }),
27 rclcpp::Rate(5s), &CoroServiceExample::service_timer_callback, this)),
28 chatter_timer_(
29 std::make_unique<mrs_lib::ROSTimer>(this_node_ptr(), rclcpp::Rate(1s), std::bind_front(&CoroServiceExample::chatter_timer_callback, this)))
30{
31}
How to run
The example is automatically built when building mrs_lib with tests enabled.
Alternatively, you can copy both the example and CMakeLists.txt file from the repo to your package.
To run the example, you have to start both the server and the client. (Don’t forget to source your workspace.)
$ ros2 run mrs_lib example_coro_service_server
[INFO] [...]: Created service: /my_service
[INFO] [...]: Received service call. Starting work...
[INFO] [...]: Work done. Sending response...
[INFO] [...]: Received service call. Starting work...
[INFO] [...]: Work done. Sending response...
$ ros2 run mrs_lib example_coro_service
[INFO] [...]: Created client 'my_service' -> '/my_service'
[INFO] [...]: Chattering ... (0)
[INFO] [...]: Chattering ... (1)
[INFO] [...]: Chattering ... (2)
[INFO] [...]: Chattering ... (3)
[INFO] [...]: Calling service...
[INFO] [...]: Chattering ... (4)
[INFO] [...]: Chattering ... (5)
[INFO] [...]: Service response:
success: 'true'
message: 'Response to service with value: 'true''
[INFO] [...]: Chattering ... (6)
[INFO] [...]: Chattering ... (7)
[INFO] [...]: Chattering ... (8)
[INFO] [...]: Calling service...
[INFO] [...]: Chattering ... (9)
[INFO] [...]: Chattering ... (10)
[INFO] [...]: Service response:
success: 'true'
message: 'Response to service with value: 'true''
[INFO] [...]: Chattering ... (11)
From the output of the client, we can see that the chattering timer is running even during the service call. During the processing of the service call, the coroutine is suspended and thus it can handle the other callbacks, such as the chatter. When the service server finishes around a second later, the coroutine is resumed and it prints the response.
Full Source code
This is the full source code for the example. You can also find it in the examples directory of mrs_lib repo.
1#include <std_srvs/srv/set_bool.hpp>
2#include <mrs_lib/coro/task.hpp>
3#include <mrs_lib/node.h>
4#include <mrs_lib/service_client_handler.h>
5#include <mrs_lib/service_server_handler.h>
6#include <mrs_lib/timer_handler.h>
7
8
9namespace mrs_lib_examples
10{
11 using namespace std::chrono_literals;
12 using ServiceType = std_srvs::srv::SetBool;
13
14 class CoroServiceExample : public mrs_lib::Node
15 {
16 public:
17 // BEGIN CTOR
18 CoroServiceExample(const rclcpp::NodeOptions& opts)
19 : Node("coro_service_example", opts), logger(this_node().get_logger()),
20 reentrant_callback_group_(this_node().create_callback_group(rclcpp::CallbackGroupType::Reentrant)),
21 service_client_(this_node_ptr(), "my_service", nullptr),
22 service_timer_(std::make_unique<mrs_lib::ROSTimer>(std::invoke([this] {
23 mrs_lib::TimerHandlerOptions opts{this_node_ptr()};
24 opts.callback_group = reentrant_callback_group_;
25 return opts;
26 }),
27 rclcpp::Rate(5s), &CoroServiceExample::service_timer_callback, this)),
28 chatter_timer_(
29 std::make_unique<mrs_lib::ROSTimer>(this_node_ptr(), rclcpp::Rate(1s), std::bind_front(&CoroServiceExample::chatter_timer_callback, this)))
30 {
31 }
32 // END CTOR
33
34 private:
35 // BEGIN CORO
36 mrs_lib::Task<std::optional<std::shared_ptr<ServiceType::Response>>> call_service(bool data)
37 {
38 auto request = std::make_shared<ServiceType::Request>();
39 request->data = data;
40 co_return co_await service_client_.callAwaitable(request);
41 }
42 // END CORO
43
44 // BEGIN CORO CALLBACK
45 mrs_lib::Task<> service_timer_callback()
46 {
47 RCLCPP_INFO_STREAM(logger, "Calling service...");
48 std::optional<std::shared_ptr<ServiceType::Response>> response_opt = co_await call_service(true);
49 if (response_opt.has_value())
50 {
51 auto response = response_opt.value();
52 std::string response_str = std::format("success: '{}'\n message: '{}'", response->success, response->message);
53 RCLCPP_INFO_STREAM(logger, "Service response:\n" << response_str);
54 } else
55 {
56 RCLCPP_WARN_STREAM(logger, "Failed to call service!!!");
57 }
58 }
59 // END CORO CALLBACK
60
61
62 void chatter_timer_callback()
63 {
64 RCLCPP_INFO_STREAM(logger, "Chattering ... (" << message_number_ << ")");
65 message_number_ += 1;
66 }
67
68 rclcpp::Logger logger;
69
70 size_t message_number_ = 0;
71
72 std::shared_ptr<rclcpp::CallbackGroup> reentrant_callback_group_;
73 mrs_lib::ServiceClientHandler<ServiceType> service_client_;
74 std::unique_ptr<mrs_lib::MRSTimer> service_timer_;
75 std::unique_ptr<mrs_lib::MRSTimer> chatter_timer_;
76 };
77
78
79 class CoroServiceExampleServer : public mrs_lib::Node
80 {
81 public:
82 CoroServiceExampleServer(const rclcpp::NodeOptions& opts)
83 : Node("coro_service_example_server", opts), logger(this_node().get_logger()),
84 service_server_(this_node_ptr(), "my_service", std::bind_front(&CoroServiceExampleServer::service_callback, this))
85 {
86 }
87
88 private:
89 void service_callback(std::shared_ptr<ServiceType::Request> req, std::shared_ptr<ServiceType::Response> res)
90 {
91 RCLCPP_INFO_STREAM(logger, "Received service call. Starting work...");
92
93 // Simulated work...
94 // Warning: This blocks the executor.
95 // If this runs on a single threaded executor, no other callbacks
96 // on ANY node in this executor will be called until this one finishes.
97 // In the case of this example, it is ok, but you should be aware of this problem.
98 std::this_thread::sleep_for(1s);
99 res->success = true;
100 res->message = std::format("Response to service with value: '{}'", req->data);
101
102 RCLCPP_INFO_STREAM(logger, "Work done. Sending response...");
103 }
104
105 rclcpp::Logger logger;
106
107 mrs_lib::ServiceServerHandler<ServiceType> service_server_;
108 };
109
110} // namespace mrs_lib_examples
111
112#include <rclcpp_components/register_node_macro.hpp>
113RCLCPP_COMPONENTS_REGISTER_NODE(mrs_lib_examples::CoroServiceExample)
114RCLCPP_COMPONENTS_REGISTER_NODE(mrs_lib_examples::CoroServiceExampleServer)
Footnotes