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.xmlCMakeLists.txt中添加必要的依赖行。

example_interfaces包含了.srv的包,你将会需要这个文件来编写你的请求和响应的数据结构:

int64 a 
int64 b
---
int64 sum

短横线上方两行是请求的参数,短横线下方是响应的参数。

1.1 Update package.xml (更新 package.xml)

因为我们在创建包的时候使用了--dependencies选项,所以我们不必在package.xmlCMakeLists.txt中手动添加依赖。

一如既往,在package.xmldescriptionmaintaineremail等标签中写入信息。

<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

如果你选择了例子中的23,服务端应该受到如下响应:

[INFO] [rclcpp]: Sum: 5

回到你运行服务端节点的终端,你将会看到它在收到请求并收到数据的时候显示的日志消息,随后显示发送响应:

[INFO] [rclcpp]: Incoming request
a: 2 b: 3
[INFO] [rclcpp]: sending back response: [5]

在服务端节点终端中按下Ctrl+C来停止节点运行。

总结

我们创建了两个节点来通过服务请求和响应数据。而且我们已将其依赖项和可执行文件添加到包配置文件中,以便可以构建和运行它们,并查看正在工作的服务/客户端系统。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
可以使用ROS提供的服务(Service)来实现这个功能。首先,在ROS工作空间下创建一个新的包,命名为“gcd”,并在该包下创建一个名为“srv”的文件夹。在该文件夹下创建一个名为“Gcd.srv”的文件,内容如下: ``` int64 a int64 b --- int64 gcd ``` 其中,“int64 a”和“int64 b”表示客户端发送的两个数字,而“int64 gcd”表示服务端返回的最大公因数。 接下来,在该包下创建一个名为“src”的文件夹,在该文件夹下创建一个名为“gcd_server.cpp”的文件,内容如下: ```c++ #include "ros/ros.h" #include "gcd/Gcd.h" int64_t gcd(int64_t a, int64_t b) { if (b == ) { return a; } else { return gcd(b, a % b); } } bool gcdCallback(gcd::Gcd::Request& req, gcd::Gcd::Response& res) { res.gcd = gcd(req.a, req.b); ROS_INFO("Request: a=%ld, b=%ld", req.a, req.b); ROS_INFO("Response: gcd=%ld", res.gcd); return true; } int main(int argc, char **argv) { ros::init(argc, argv, "gcd_server"); ros::NodeHandle n; ros::ServiceServer service = n.advertiseService("gcd", gcdCallback); ROS_INFO("Ready to calculate gcd."); ros::spin(); return ; } ``` 在该文件中,我们定义了一个名为“gcd”的回调函数,用于处理客户端发送的请求。在该函数中,我们调用了一个名为“gcd”的函数,用于计算最大公因数,并将结果存储在服务端返回的响应中。最后,我们通过ROS提供的“ServiceServer”类将该回调函数注册为一个服务,并启动了ROS节点。 最后,在该包下创建一个名为“gcd_client.cpp”的文件,内容如下: ```c++ #include "ros/ros.h" #include "gcd/Gcd.h" int main(int argc, char **argv) { ros::init(argc, argv, "gcd_client"); ros::NodeHandle n; ros::ServiceClient client = n.serviceClient<gcd::Gcd>("gcd"); gcd::Gcd srv; srv.request.a = atoll(argv[1]); srv.request.b = atoll(argv[2]); if (client.call(srv)) { ROS_INFO("Gcd: %ld", (long int)srv.response.gcd); } else { ROS_ERROR("Failed to call service gcd"); return 1; } return ; } ``` 在该文件中,我们定义了一个名为“gcd”的服务客户端,并通过该客户端服务端发送了两个数字。最后,我们将服务端返回的最大公因数打印出来。 编译并运行该程序,可以通过以下命令启动服务端: ``` rosrun gcd gcd_server ``` 然后,在另一个终端中,可以通过以下命令向服务端发送请求: ``` rosrun gcd gcd_client 12 18 ``` 其中,“12”和“18”表示要计算最大公因数的两个数字。服务端将返回它们的最大公因数,即“6”。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值