Service
A service is a communication consisting requests and responses. There are a server and clients for a service as follows.
graph TD; Server--response-->Client1; Server--response-->Client2; Server--response-->Client3; Client1--request-->Server; Client2--request-->Server; Client3--request-->Server;
A client sends a request and the server replies the request.
In this tutorial, we will describe how to implement a client and a server in Rust by using safe_drive
.
Create Base Files
In this tutorial, we prepare 3 Rust's projects and 1 C's project. The following table shows the directories we use.
Directories | Description |
---|---|
srvtest/src/server | server in Rust |
srvtest/src/client | client in Rust |
srvtest/src/srvmsg | message type |
srvmsg
is a ROS2's project to define a user defined type for a service we implement.
$ mkdir -p srvtest/src
$ cd srvtest/src
$ cargo new server
$ cargo new client
$ ros2 pkg create --build-type ament_cmake srvmsg
In addition to that, create the workspace's Cargo.toml
as follows.
srvtest/src/Cargo.toml
[workspace]
members = [ "client", "server"]
Define Protocol
First of all, let's define a protocol for a service.
ROS2 provides a special format to define a protocol,
and it should be described in a .srv
file.
Usually, .srv
files are placed in a srv
directory.
So, we create srv
directory as follows.
$ cd srvtest/src/srvmsg
$ mkdir srv
Create srvmsg/srv/AddTwoInts.srv
Then, create AddTwoInts.srv
file in which a protocol is specified as follows.
uint32 x
uint32 y
---
uint32 result
uint32 x
and uint32 y
above --
are types which must be
included in a request, and uint32 result
is a type which must be
included in a response.
We will implement a server which takes 2 integer values
and returns the summation of the values.
Edit srvmsg/CMakeLists.txt
To generate shared libraries from AddTwoInts.srv
,
CMakeLists.txt
must be updated as follows.
# srvtest/src/srvmsg/CMakeLists.txt
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"srv/AddTwoInts.srv"
)
Edit srvmsg/package.xml
In addition to that, the following lines must be added to package.xml
.
srvtest/src/srvmsg/package.xml
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
Server
A server can be implemented in a straightforward way.
Edit server/Cargo.toml
To generate Rust types from .srv
files,
we have to edit Cargo.toml
as follows.
[dependencies]
safe_drive = "0.4"
srvmsg = { path = "/tmp/safe_drive_tutorial/srvtest/srvmsg" }
tokio = { version = "1", features = ["full"] }
[package.metadata.ros]
msg = ["srvmsg"]
msg_dir = "/tmp/safe_drive_tutorial/srvtest"
safe_drive_version = "0.3"
Create server/package.xml
package.xml
is also required as follows.
srvtest/src/server/package.xml
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>server</name>
<version>0.0.0</version>
<description>Server in Rust</description>
<maintainer email="yuuki.takano@tier4.jp">Yuuki Takano</maintainer>
<license>Apache License 2.0</license>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<depend>srvmsg</depend>
<export>
<build_type>ament_cargo</build_type>
</export>
</package>
Don't forget <depend>srvmsg</depend>
.
Generate Types
AddTwoInts.srv
will be translated to struct AddTwoInts
as follows.
#![allow(unused)] fn main() { #[derive(Debug)] pub struct AddTwoInts; impl ServiceMsg for AddTwoInts { type Request = AddTwoInts_Request; type Response = AddTwoInts_Response; fn type_support() -> *const rcl::rosidl_service_type_support_t { unsafe { rosidl_typesupport_c__get_service_type_support_handle__srvmsg__srv__AddTwoInts() } } } }
struct AddTwoInts
implements ServiceMsg trait
.
ServiceMsg::Request
and ServiceMsg::Response
are types of request and response, respectively.
AddTwoInts_Request
and AddTowInts_Response
are as follows.
#![allow(unused)] fn main() { #[repr(C)] #[derive(Debug)] pub struct AddTwoInts_Request { pub x: u32, pub y: u32, } #[repr(C)] #[derive(Debug)] pub struct AddTwoInts_Response { pub result: u32, } }
Edit server/src/main.rs
You have to create a server by create_server()
method and register a callback function to a selector as follows.
use safe_drive::{context::Context, error::DynError, logger::Logger, pr_error, qos::Profile}; use srvmsg::srv::{AddTwoInts, AddTwoInts_Response}; fn main() -> Result<(), DynError> { // Create a context. let ctx = Context::new()?; // Create a node. let node = ctx.create_node("server_node", None, Default::default())?; // Create a server. let server = node.create_server::<AddTwoInts>("my_service", Some(Profile::default()))?; // Create a selector. let mut selector = ctx.create_selector()?; // Create a logger. let logger = Logger::new("server"); selector.add_server( server, Box::new(move |msg, _header| { let mut response = AddTwoInts_Response::new().unwrap(); pr_error!(logger, "recv: {:?}", msg); response.result = msg.x + msg.y; response }), ); loop { selector.wait()?; // Spin. } }
Let's dig into detail.
#![allow(unused)] fn main() { // Create a server. let server = node.create_server::<AddTwoInts>("my_service", Some(Profile::default()))?; }
AddTwoInts
of create_server::<AddTwoInts>
indicates the protocol of the server.
The arguments of "my_service"
and Some(Profile::default())
are the service name and QoS.
If you pass None
instead of Some(Profile::default())
, Profile::default()
is used.
The callback was passed as follows.
#![allow(unused)] fn main() { selector.add_server( server, Box::new(move |msg, _header| { let mut response = AddTwoInts_Response::new().unwrap(); pr_error!(logger, "recv: {:?}", msg); response.result = msg.x + msg.y; response }), ); }
The callback function must take a message sent by a client and a header including sequence number, time, etc.
msg
's types is AddTwoInts_Request
and a value of AddTwoInts_Response
, which is a response, must be returned.
Client
Edit client/Cargo.toml
safe_drive
, and tokio
must be added to Cargo.toml
as follows.
srvtest/src/client/Cargo.toml
[dependencies]
safe_drive = "0.4"
srvmsg = { path = "/tmp/safe_drive_tutorial/srvtest/srvmsg" }
tokio = { version = "1", features = ["full"] }
[package.metadata.ros]
msg = ["srvmsg"]
msg_dir = "/tmp/safe_drive_tutorial/srvtest"
safe_drive_version = "0.3"
Create client/package.xml
package.xml
is also required as follows.
srvtest/src/client/package.xml
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>client</name>
<version>0.0.0</version>
<description>Client in Rust</description>
<maintainer email="yuuki.takano@tier4.jp">Yuuki Takano</maintainer>
<license>Apache License 2.0</license>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<depend>srvmsg</depend>
<export>
<build_type>ament_cargo</build_type>
</export>
</package>
Don't forget <depend>srvmsg</depend>
.
Edit client/src/main.rs
We recommend to use async/await to implement a client, because a client wait a response, but when the response is replied is unpredictable. In this tutorial, we use Tokio, which is the most popular asynchronous library of Rust.
use safe_drive::{ context::Context, error::DynError, logger::Logger, pr_error, pr_info, pr_warn, qos::Profile, }; use srvmsg::srv::{AddTwoInts, AddTwoInts_Request}; use std::time::Duration; use tokio::time::timeout; #[tokio::main] async fn main() -> Result<(), DynError> { // Create a context and a node. let ctx = Context::new()?; let node = ctx.create_node("client", None, Default::default())?; let logger = Logger::new("client"); // Create a client. let mut client = node.create_client::<AddTwoInts>("my_service", Some(Profile::default()))?; let mut n = 0; loop { let mut request = AddTwoInts_Request::new().unwrap(); request.x = n; request.y = n + 1; n += 1; // Send a request. let client_rcv = client.send(&request)?; // Receive a request. let mut receiver = client_rcv.recv(); match timeout(Duration::from_secs(1), &mut receiver).await { Ok(Ok((c, response, _header))) => { pr_info!(logger, "received {:?}", response); client = c; } Ok(Err(e)) => { pr_error!(logger, "error: {e}"); return Err(e); } Err(elapsed) => { pr_warn!(logger, "timeout: {elapsed}"); client = receiver.give_up(); } } tokio::time::sleep(Duration::from_secs(1)).await; } }
Because a response will be never returned,
we use tokio::time::timeout
to receive a response.
timeout(Duration::from_secs(1), &mut receiver).await
takes a duration and a awaitable future.
The awaitable future can be taken by client_rcv.recv()
.
Note that it uses tokio::time::sleep
instead of
std::thread::sleep
because to avoid calling blocking functions.
client.send()
consumes client
and it returns client_recv
to receive a response.
client_rcv.recv()
consumes client_recv
and client_rcv.recv().await
returns new client, a response, and a header.
The new client must be used to send next request.
This means that you cannot ignore receiving a response. If you forget to receive a response, you cannot send any request again, because you need to get a new client by receiving a response.. Otherwise, you encountered compilation errors.
Execution
First of all, you have to compile it and execute a server as follows.
$ cd srvtest
$ colcon build --cargo-args --release
$ . ./install/setup.bash
$ ros2 run server server
Then, in another terminal, execute a client as follows.
$ cd srvtest
$ . ./install/setup.bash
$ ros2 run client client
[WARN] [1659604527.720730018] [client]: timeout: deadline has elapsed
[INFO] [1659604528.722220697] [client]: received AddTwoInts_Response { result: 3 }
[INFO] [1659604529.723525686] [client]: received AddTwoInts_Response { result: 5 }
[INFO] [1659604530.724820326] [client]: received AddTwoInts_Response { result: 7 }
Nicely done! We used async/await for the client, and it can be used for the server as follows.
#![allow(unused)] fn main() { async { server.recv().await }; }