ROS Services work with a client-server model to provide an interface for IPC (interprocess communication) based on request / wait / check response async programming.
This type of programming is not new, being one of the most common models in computer programming. It allows us to request another part of the robot to do something and then go process something else for later on, check the result of that request.
This is not a callback based response, rather a "check later" based response, which is suitable for very fast tasks or for some other use cases. It is good because its very simple and easy to understand, so if the async model will not require a complex multi-threading event-driven based request/response model, one should be just fine using this approach.
A service is therefore comprised of three parts, at least:
- A server, responsible for creating the service and responding to it's request.
- A client, which will consume the service.
- A service message format which will define what is the format of the data being shared between those nodes
ROS provides a very easy way of building this message format. All we need to do is to create one file: a .srv file, which will specify the message's format for a service.
In our example, we'll define a service that returns the greatest of two int64 numbers
ros2 pkg create --build-type ament_cmake greater2_interface --dependencies rclcpp
Now, create a folder called srv and inside it, a file called GreaterInt.srv
int64 a
int64 b
---
int64 greater
This file will specify a service message which will receive two int64 a and b and return another int64 greater
Then, we need to teach cmake how to compile our code. Edit the CMakeLists.txt file and add
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"src/srv/GreaterInt.srv"
)
Then, edit the package.xml file and add
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
Now, we can compile this package to import it later
colcon build --packages-select greater2_interface
We can also check if the interface is ok by issuing:
. install/setup.bash
ros2 interface show greater2_interface/srv/GreaterInt
int64 a
int64 b
---
int64 greater
That means our interface is prep and ready to be imported as a model to a service. Now, let's create a package to implement the server and the client to that server:
ros2 pkg create --build-type ament_cmake greater2_service --dependencies rclcpp greater2_interface
Then, let's create the server file greater_int_server.cpp
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "greater2_interface/srv/greater_int.hpp"
void greater(
const std::shared_ptr<greater2_interface::srv::GreaterInt::Request> request,
const std::shared_ptr<greater2_interface::srv::GreaterInt::Response> response)
{
response->greater = request->a > request -> b ? request->a : request->b;
}
int main (int argc, char **argv) {
rclcpp::init(argc, argv);
auto node = rclcpp::Node::make_shared("greater_int_server");
auto service = node->create_service<greater2_interface::srv::GreaterInt>(
"greater_int", &greater);
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "GreaterInt service is up");
rclcpp::spin(node);
rclcpp::shutdown();
}
We can see that creating a service is very easy. The #include directive will include the generated struct for the data format we expect to handle. ROS also generates Request / Response interfaces for us.
Then, we create a method to implement our logic and attach it to a service.
Finally, we create a node and put ROS to spin it (run it) so it can handle service requests.
The client is also very straightforward to implement. Let's check it out:
#include <chrono>
#include <cstdlib>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "greater2_interface/srv/greater_int.hpp"
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 %s [int] [int]",
argv[0]);
return 1;
}
auto node = rclcpp::Node::make_shared("greater_int_client");
auto client = node->create_client<greater2_interface::srv::GreaterInt>(
"greater_int");
auto request =
std::make_shared<greater2_interface::srv::GreaterInt::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 [yet]");
}
auto result_req = client->async_send_request(request);
auto result_code = rclcpp::spin_until_future_complete(node, result_req);
if (result_code == rclcpp::FutureReturnCode::SUCCESS) {
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Greater: %ld",
result_req.get()->greater);
} else {
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"),
"Failed to call service add_2");
}
rclcpp::shutdown();
return 0;
}
First, we build a node object to be able to execute this client code as a node on ROS.
Next, we create a client object which will allow us to send async requests to the specified service name
After that, we build the request object itself, with the data that we want to send to the service.
And then we use the client object to build a async request object. This object will only send the data once it gets spinned by ROS. We do so by using rclcpp::spin_until_future_complete ( )
Finally, we check the result code and show the result data.
This architecture is very similar to the async / await achitecture for those who program javascript.
In order to compile our service package, we still need to set it up by editing CMakeFiles.txt and packages.xml for it (remember that we did it already, but for the interface package)
Our CMakeFiles.txt will look like this:
cmake_minimum_required(VERSION 3.8)
project(greater2_service)
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
# find dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(greater2_interface REQUIRED)
add_executable(server src/greater_int_server.cpp)
ament_target_dependencies(server
rclcpp
greater2_interface)
add_executable(client src/greater_int_client.cpp)
ament_target_dependencies(client
rclcpp
greater2_interface)
install(TARGETS
server
client
DESTINATION lib/${PROJECT_NAME})
ament_package()
Notice that we are instructing cmake to find packages greater2_interface in find_package () and then we're inserting it when compiling our exec file in add_executable( )
To our packages.xml, we just need to have the dependencies right:
<depend>rclcpp</depend>
<depend>greater2_interface</depend>
If everything is done right, we should be able to execute the server and the client, both in different terminals and check the results:
. install/setup.bash
ros2 run greater2_service server
[INFO] [1649290854.198941166] [rclcpp]: GreaterInt service is up
. install/setup.bash
ros2 run greater2_service client 3 7
[INFO] [1649290900.712837482] [rclcpp]: Greater: 7
Nenhum comentário:
Postar um comentário