ROS 2 Iron 教程 第二章 Client libraries 第五节 编写一个简单的服务端和客户端(C++))
前言
本系列文章是由笔者翻译自ROS 2 官方教程。笔者水平有限,如有错误,还请读者指正。
背景
当节点使用服务进行通信时,发送数据请求的节点被称为客户端,而响应请求的节点被称为服务端。请求和响应的数据结构被.srv
文件决定。
先决条件
在先前的教程中,我们已经学会如何创建一个工作空间,以及创建一个包。
任务
1 Create a package (创建一个包)
打开一个新终端,并添加 ROS 2 安装,使得ros2
命令能运作。
cd
进入先前教程创建的ros2_ws
目录。
回忆一下,包应被创建在src
目录中,而不是工作空间根目录。cd
进入ros2_ws/src
目录,随后创建一个新包:
ros2 pkg create --build-type ament_cmake --license Apache-2.0 cpp_srvcli --dependencies rclcpp example_interfaces
你的终端将会返回信息以证明cpp_srvcli
包的创建,以及必要文件和文件夹创建成功。
--dependencies
参数将会自动在package.xml
和CMakeLists.txt
中添加必要的依赖行。
example_interfaces
包含了.srv
的包,你将会需要这个文件来编写你的请求和响应的数据结构:
int64 a
int64 b
---
int64 sum
短横线上方两行是请求的参数,短横线下方是响应的参数。
1.1 Update package.xml (更新 package.xml)
因为我们在创建包的时候使用了--dependencies
选项,所以我们不必在package.xml
或CMakeLists.txt
中手动添加依赖。
一如既往,在package.xml
中description
,maintainer
,email
等标签中写入信息。
<description>C++ client server tutorial</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache License 2.0</license>
2 Write the service node (编写服务节点)
在ros2_ws/src/cpp_srvcli/src
目录中,创建名为add_two_ints_server.cpp
的新文件,并在文件中粘贴如下代码:
#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();
}
2.1 Examine the code (分析代码)
在代码开头的两个#include
语句声明了你的包依赖。
add
函数将来自请求的两个整数相加,并把相加值作为响应返回,于此同时将状态显示于终端。
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);
}
随后的main
函数一行一行地完成了如下工作:
- 初始化 ROS 2 C++ 库:
rclcpp::init(argc, argv);
- 创建名为
add_two_ints_server
的节点:
std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_server");
- 给节点创建一个名为
add_two_ints
的服务,并在节点网络上播发&add
方法:
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);
2.2 Add executable (添加可执行文件)
add_executable
宏定义生成了一个可执行文件,你就可以使用ros2 run
运行它。在CMakeLists.txt
中添加如下代码块来生成名为server
的可执行文件:
add_executable(server src/add_two_ints_server.cpp)
ament_target_dependencies(server rclcpp example_interfaces)
这样ros2 run
可以找到可执行文件,在CMakeLists.txt
文件的末尾、ament_package()
前添加如下代码:
install(TARGETS
server
DESTINATION lib/${PROJECT_NAME})
现在我们已经可以构建包了,但让我们创建一个客户端节点来查看完整系统的运行。
3 Write the client node (编写客户端节点)
在ros2_ws/src/cpp_srvcli/src
目录中创建一个名为add_two_ints_client.cpp
的文件,并在文件内粘贴如下代码:
#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;
}
3.1 Examine the code (分析代码)
与服务端节点相似的是,如下几行的代码创建了客户端节点以及创建了节点的服务:
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");
随后,请求被创建,其数据结构在先前提到的.srv
文件中定义。
auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
request->a = atoll(argv[1]);
request->b = atoll(argv[2]);
while
循环给客户端 1 秒钟的时间在节点网络中搜寻服务端。如果客户端无法找到服务端,它将会继续等待。
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again...");
如果客户端被取消(例如在终端中按下Ctrl+C
键),它将会返回一条提示自己中断的日志信息。
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting.");
随后客户端发送请求,客户端节点将在受到响应或是错误之前保持运行。
3.2 Add executable (添加可执行文件)
回到CMakeLists.txt
,为你的新节点添加可执行文集和目标。在删除自动生成的不必要的样板后,你的CMakeLists.txt
应该看上去是这样:
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()
4 Build and run (构建并运行)
构建之前在你的工作空间的根目录(ros2_ws
)运行rosdep
来检查是否有缺失的依赖是一个好习惯(Liunx):
rosdep install -i --from-path src --rosdistro iron -y
在你工作空间的根目录中(ros2_ws
)构建你的新包:
colcon build --packages-select cpp_srvcli
打开一个新终端,cd
进入ros2_ws
并添加启动文件:
source install/setup.bash
现在运行服务端节点:
ros2 run cpp_srvcli server
终端应该会显示如下信息,并等待:
[INFO] [rclcpp]: Ready to add two ints.
打开另一个终端,在ros2_ws
中添加启动文件。启动客户端节点,在命令后以空格分割随意两个整数:
ros2 run cpp_srvcli client 2 3
如果你选择了例子中的2
和3
,服务端应该受到如下响应:
[INFO] [rclcpp]: Sum: 5
回到你运行服务端节点的终端,你将会看到它在收到请求并收到数据的时候显示的日志消息,随后显示发送响应:
[INFO] [rclcpp]: Incoming request
a: 2 b: 3
[INFO] [rclcpp]: sending back response: [5]
在服务端节点终端中按下Ctrl+C
来停止节点运行。
总结
我们创建了两个节点来通过服务请求和响应数据。而且我们已将其依赖项和可执行文件添加到包配置文件中,以便可以构建和运行它们,并查看正在工作的服务/客户端系统。