警告

您正在阅读的 ROS 2 文档版本已达到 EOL(生命周期结束),不再受官方支持。如果您想了解最新信息,请访问 Jazzy.

编写动作服务器和客户端(C++)

目标 用 C++ 实现动作服务器和客户端。

辅导水平: 中级

时间 15 分钟

背景介绍

操作是 ROS 中的一种异步通信形式。 行动客户 将目标申请发送至 行动服务器. 行动服务器 将目标反馈和结果发送至 行动客户.

先决条件

您需要 动作教程界面 软件包和 Fibonacci.action 接口、 创建行动.

任务

1 创建 action_tutorials_cpp 软件包

正如我们在 创建第一个 ROS 2 软件包 教程,我们需要创建一个新的软件包来保存我们的 C++ 和辅助代码。

1.1 创建 action_tutorials_cpp 软件包

进入在上一教程中创建的动作工作区,为 C++ 动作服务器创建一个新包:

记住 将上一教程中的工作区作为源文件 首先)。

CD ~/action_ws/src ros2  创建 --依赖 动作教程界面 rclcpp rclcpp_action rclcpp_components -- 动作教程_cpp

1.2 加入可见度控制

为了使软件包能在 Windows 上编译和运行,我们需要添加一些 "可见性控制"。有关为什么需要这样做的详细信息,请参阅 这里.

打开 action_tutorials_cpp/include/action_tutorials_cpp/visibility_control.h并输入以下代码:

#ifndef ACTION_TUTORIALS_CPP__VISIBILITY_CONTROL_H_
#define ACTION_TUTORIALS_CPP__VISIBILITY_CONTROL_H_

#ifdef __cplusplus
外部 "C";
{
#endif

// 这个逻辑是从 gcc wiki 上的示例中借用(然后命名)的:
// https://gcc.gnu.org/wiki/Visibility

#if defined _WIN32 || defined __CYGWIN__
  #ifdef __GNUC__
    #define ACTION_TUTORIALS_CPP_EXPORT __attribute__ ((dllexport))
    #define ACTION_TUTORIALS_CPP_IMPORT __attribute__ ((dllimport))
  #else
    #define ACTION_TUTORIALS_CPP_EXPORT __declspec(dllexport)
    #define ACTION_TUTORIALS_CPP_IMPORT __declspec(dllimport)
  #endif
  #ifdef ACTION_TUTORIALS_CPP_BUILDING_DLL
    #define ACTION_TUTORIALS_CPP_PUBLIC ACTION_TUTORIALS_CPP_EXPORT
  #else
    #define ACTION_TUTORIALS_CPP_PUBLIC ACTION_TUTORIALS_CPP_IMPORT
  #endif
  #define ACTION_TUTORIALS_CPP_PUBLIC_TYPE ACTION_TUTORIALS_CPP_PUBLIC
  #define ACTION_TUTORIALS_CPP_LOCAL
#else
  #define ACTION_TUTORIALS_CPP_EXPORT __attribute__ ((visibility("default")))
  #define ACTION_TUTORIALS_CPP_IMPORT
  #if __GNUC__ >= 4
    #define ACTION_TUTORIALS_CPP_PUBLIC attribute____ ((visibility("default")))
    #define ACTION_TUTORIALS_CPP_LOCAL __attribute__ ((visibility("hidden")))
  #else
    #define ACTION_TUTORIALS_CPP_PUBLIC
    #define ACTION_TUTORIALS_CPP_LOCAL
  #endif
  #define ACTION_TUTORIALS_CPP_PUBLIC_TYPE
#endif

#ifdef __cplusplus
}
#endif

#endif  // action_tutorials_cpp__visibility_control_h_

2 编写行动服务器

让我们集中精力编写一个动作服务器,使用我们在 创建行动 教程。

2.1 编写行动服务器代码

打开 action_tutorials_cpp/src/fibonacci_action_server.cpp并输入以下代码:

  1#include 功能强大;
  2#include <内存>;
  3#include <thread>;
  4
  5#include "action_tutorials_interfaces/action/fibonacci.hpp";
  6#include "rclcpp/rclcpp.hpp";
  7#include "rclcpp_action/rclcpp_action.hpp";
  8#include "rclcpp_components/register_node_macro.hpp";
  9
 10#include "action_tutorials_cpp/visibility_control.h";
 11
 12命名空间 动作教程_cpp
 13{
 14 FibonacciActionServer :  rclcpp::节点
 15{
 16:
 17  使用 斐波那契 = 动作教程界面::行动::斐波那契;
 18  使用 目标手柄斐波纳契 = rclcpp_action::服务器目标句柄<;斐波那契>;;
 19
 20  行动教程_cpp_public
 21  不含糊 FibonacciActionServer( rclcpp::节点选项 及样品; 选项 = rclcpp::节点选项())
 22  : 节点("fibonacci_action_server";, 选项)
 23  {
 24    使用 命名空间 标准::占位符;
 25
 26    ->;行动服务器 = rclcpp_action::创建服务器<;斐波那契>;(
 27      ,
 28      "fibonacci";,
 29      标准::约束(及样品;FibonacciActionServer::处理目标, , _1, _2),
 30      标准::约束(及样品;FibonacciActionServer::处理取消, , _1),
 31      标准::约束(及样品;FibonacciActionServer::已接受, , _1));
 32  }
 33
 34私人:
 35  rclcpp_action::服务器<;斐波那契>::SharedPtr 行动服务器;
 36
 37  rclcpp_action::目标响应 处理目标(
 38     rclcpp_action::目标 UUID 及样品; uuid,
 39    标准::共享_ptr<; 斐波那契::目标>; 目标)
 40  {
 41    RCLCPP_INFO(->;get_logger(), "收到目标请求,订单 %d";, 目标->;订单);
 42    (空白)uuid;
 43    返回 rclcpp_action::目标响应::接受并执行;
 44  }
 45
 46  rclcpp_action::取消响应 处理取消(
 47     标准::共享_ptr<;目标手柄斐波纳契>; 目标句柄)
 48  {
 49    RCLCPP_INFO(->;get_logger(), 收到取消目标的请求";);
 50    (空白)目标句柄;
 51    返回 rclcpp_action::取消响应::接受;
 52  }
 53
 54  空白 已接受( 标准::共享_ptr<;目标手柄斐波纳契>; 目标句柄)
 55  {
 56    使用 命名空间 标准::占位符;
 57    // 需要快速返回,以避免阻塞执行器,因此需要启动一个新线程
 58    标准::线程{标准::约束(及样品;FibonacciActionServer::执行, , _1), 目标句柄}.脱离();
 59  }
 60
 61  空白 执行( 标准::共享_ptr<;目标手柄斐波纳契>; 目标句柄)
 62  {
 63    RCLCPP_INFO(->;get_logger(), "执行目标";);
 64    rclcpp::费率 循环速率(1);
 65     汽车 目标 = 目标句柄->;get_goal();
 66    汽车 反馈 = 标准::共享<;斐波那契::反馈意见>;();
 67    汽车 及样品; 顺序 = 反馈->;部分序列;
 68    顺序.推回(0);
 69    顺序.推回(1);
 70    汽车 结果 = 标准::共享<;斐波那契::结果>;();
 71
 72    对于 (int i = 1; (i <; 目标->;订单) &&; rclcpp::好的(); ++i) {
 73      // 检查是否有取消请求
 74      如果 (目标句柄->;is_canceling()) {
 75        结果->;顺序 = 顺序;
 76        目标句柄->;已取消(结果);
 77        RCLCPP_INFO(->;get_logger(), 目标取消";);
 78        返回;
 79      }
 80      // 更新序列
 81      顺序.推回(顺序[i] + 顺序[i - 1]);
 82      // 发布反馈意见
 83      目标句柄->;发布反馈(反馈);
 84      RCLCPP_INFO(->;get_logger(), "发布反馈";);
 85
 86      循环速率.睡眠();
 87    }
 88
 89    // 检查目标是否完成
 90    如果 (rclcpp::好的()) {
 91      结果->;顺序 = 顺序;
 92      目标句柄->;继承(结果);
 93      RCLCPP_INFO(->;get_logger(), "目标成功";);
 94    }
 95  }
 96};  // 类 FibonacciActionServer
 97
 98}  // 命名空间 action_tutorials_cpp
 99
100rclcpp_components_register_node(动作教程_cpp::FibonacciActionServer)

前几行包含了我们需要编译的所有头文件。

接下来,我们创建一个从 rclcpp::Node:

 FibonacciActionServer :  rclcpp::节点

的构造函数 FibonacciActionServer 类将节点名称初始化为 fibonacci_action_server:

  不含糊 FibonacciActionServer( rclcpp::节点选项 及样品; 选项 = rclcpp::节点选项())
  : 节点("fibonacci_action_server";, 选项)

构造函数还会实例化一个新的动作服务器:

    ->;行动服务器 = rclcpp_action::创建服务器<;斐波那契>;(
      ,
      "fibonacci";,
      标准::约束(及样品;FibonacciActionServer::处理目标, , _1, _2),
      标准::约束(及样品;FibonacciActionServer::处理取消, , _1),
      标准::约束(及样品;FibonacciActionServer::已接受, , _1));

行动服务器需要具备 6 个条件:

  1. 模板化操作类型名称: 斐波那契.

  2. 要添加操作的 ROS 2 节点: .

  3. 行动名称: 斐波那契.

  4. 用于处理目标的回调函数: 处理目标

  5. 用于处理取消的回调函数: 处理取消.

  6. 用于处理目标接受的回调函数: 接受.

文件的下一部分是各种回调的实现。需要注意的是,所有回调都需要快速返回,否则执行器就有可能处于饥饿状态。

我们从处理新目标的回调开始:

  rclcpp_action::目标响应 处理目标(
     rclcpp_action::目标 UUID 及样品; uuid,
    标准::共享_ptr<; 斐波那契::目标>; 目标)
  {
    RCLCPP_INFO(->;get_logger(), "收到目标请求,订单 %d";, 目标->;订单);
    (空白)uuid;
    返回 rclcpp_action::目标响应::接受并执行;
  }

这种实现方式只接受所有目标。

接下来是处理取消订单的回调:

  rclcpp_action::取消响应 处理取消(
     标准::共享_ptr<;目标手柄斐波纳契>; 目标句柄)
  {
    RCLCPP_INFO(->;get_logger(), 收到取消目标的请求";);
    (空白)目标句柄;
    返回 rclcpp_action::取消响应::接受;
  }

这种实现方式只是告诉客户端它接受了取消。

最后一个回调接受一个新目标并开始处理:

  空白 已接受( 标准::共享_ptr<;目标手柄斐波纳契>; 目标句柄)
  {
    使用 命名空间 标准::占位符;
    // 需要快速返回,以避免阻塞执行器,因此需要启动一个新线程
    标准::线程{标准::约束(及样品;FibonacciActionServer::执行, , _1), 目标句柄}.脱离();
  }

由于执行的是一个长期运行的操作,我们会产生一个线程来执行实际工作,并从 已接受 很快。

所有进一步的处理和更新都在 执行 方法:

  空白 执行( 标准::共享_ptr<;目标手柄斐波纳契>; 目标句柄)
  {
    RCLCPP_INFO(->;get_logger(), "执行目标";);
    rclcpp::费率 循环速率(1);
     汽车 目标 = 目标句柄->;get_goal();
    汽车 反馈 = 标准::共享<;斐波那契::反馈意见>;();
    汽车 及样品; 顺序 = 反馈->;部分序列;
    顺序.推回(0);
    顺序.推回(1);
    汽车 结果 = 标准::共享<;斐波那契::结果>;();

    对于 (int i = 1; (i <; 目标->;订单) &&; rclcpp::好的(); ++i) {
      // 检查是否有取消请求
      如果 (目标句柄->;is_canceling()) {
        结果->;顺序 = 顺序;
        目标句柄->;已取消(结果);
        RCLCPP_INFO(->;get_logger(), 目标取消";);
        返回;
      }
      // 更新序列
      顺序.推回(顺序[i] + 顺序[i - 1]);
      // 发布反馈意见
      目标句柄->;发布反馈(反馈);
      RCLCPP_INFO(->;get_logger(), "发布反馈";);

      循环速率.睡眠();
    }

    // 检查目标是否完成
    如果 (rclcpp::好的()) {
      结果->;顺序 = 顺序;
      目标句柄->;继承(结果);
      RCLCPP_INFO(->;get_logger(), "目标成功";);
    }
  }

该工作线程每秒处理一个斐波那契数列的序列号,每一步都会发布反馈更新。完成处理后,它将 目标句柄 并退出。

我们现在有了一个功能完备的行动服务器。让我们构建并运行它。

2.2 编译行动服务器

在上一节中,我们将动作服务器代码安装到位。为了让它编译和运行,我们还需要做几件事。

首先,我们需要设置 CMakeLists.txt,以便编译动作服务器。打开 action_tutorials_cpp/CMakeLists.txt之后添加以下内容 查找软件包 电话

add_library(action_server SHARED
  src/fibonacci_action_server.cpp)
target_include_directories(action_server PRIVATE
  $<BUILD_INTERFACE:${cmake_current_source_dir}/include>;
  $<INSTALL_INTERFACE:include>;)
target_compile_definitions(action_server
  PRIVATE "ACTION_TUTORIALS_CPP_BUILDING_DLL")
ament_target_dependencies(action_server
  "action_tutorials_interfaces";
  "rclcpp";
  "rclcpp_action";
  "rclcpp_components")
rclcpp_components_register_node(action_server PLUGIN "action_tutorials_cpp::FibonacciActionServer" EXECUTABLE fibonacci_action_server)
install(TARGETS
  行动服务器
  存档目的地 lib
  图书目录 lib
  运行时间目的地 bin)

现在我们可以编译软件包了。转到 action_ws然后运行:

胶管 构建

这将编译整个工作区,包括 fibonacci_action_server动作教程_cpp 包装

2.3 运行行动服务器

现在我们已经构建了行动服务器,可以运行它了。将我们刚刚构建的工作区(action_ws),并尝试运行动作服务器:

玫瑰2 运行 动作教程_cpp fibonacci_action_server

3 撰写行动客户

3.1 编写动作客户端代码

打开 action_tutorials_cpp/src/fibonacci_action_client.cpp并输入以下代码:

  1#include 功能强大;
  2#include 未来;
  3#include <内存>;
  4#include <字符串>;
  5#include 流>;
  6
  7#include "action_tutorials_interfaces/action/fibonacci.hpp";
  8
  9#include "rclcpp/rclcpp.hpp";
 10#include "rclcpp_action/rclcpp_action.hpp";
 11#include "rclcpp_components/register_node_macro.hpp";
 12
 13命名空间 动作教程_cpp
 14{
 15 FibonacciActionClient :  rclcpp::节点
 16{
 17:
 18  使用 斐波那契 = 动作教程界面::行动::斐波那契;
 19  使用 目标手柄斐波纳契 = rclcpp_action::客户目标句柄<;斐波那契>;;
 20
 21  不含糊 FibonacciActionClient( rclcpp::节点选项 及样品; 选项)
 22  : 节点("fibonacci_action_client";, 选项)
 23  {
 24    ->;客户端ptr_ = rclcpp_action::创建客户端<;斐波那契>;(
 25      ,
 26      "fibonacci";);
 27
 28    ->;定时器 = ->;创建隔离墙计时器(
 29      标准::计时器::毫秒数(500),
 30      标准::约束(及样品;FibonacciActionClient::发送目标, ));
 31  }
 32
 33  空白 发送目标()
 34  {
 35    使用 命名空间 标准::占位符;
 36
 37    ->;定时器->;取消();
 38
 39    如果 (!->;客户端ptr_->;等待行动服务器()) {
 40      RCLCPP_ERROR(->;get_logger(), "等待后行动服务器不可用";);
 41      rclcpp::关闭();
 42    }
 43
 44    汽车 goal_msg = 斐波那契::目标();
 45    goal_msg.订单 = 10;
 46
 47    RCLCPP_INFO(->;get_logger(), 发送目标";);
 48
 49    汽车 发送目标选项 = rclcpp_action::客户<;斐波那契>::发送目标选项();
 50    发送目标选项.目标响应回调 =
 51      标准::约束(及样品;FibonacciActionClient::目标响应回调, , _1);
 52    发送目标选项.反馈回调 =
 53      标准::约束(及样品;FibonacciActionClient::反馈回调, , _1, _2);
 54    发送目标选项.result_callback =
 55      标准::约束(及样品;FibonacciActionClient::result_callback, , _1);
 56    ->;客户端ptr_->;异步发送目标(goal_msg, 发送目标选项);
 57  }
 58
 59私人:
 60  rclcpp_action::客户<;斐波那契>::SharedPtr 客户端ptr_;
 61  rclcpp::定时器基数::SharedPtr 定时器;
 62
 63  空白 目标响应回调(标准::共享未来<;目标手柄斐波纳契::SharedPtr>; 未来)
 64  {
 65    汽车 目标句柄 = 未来.获取();
 66    如果 (!目标句柄) {
 67      RCLCPP_ERROR(->;get_logger(), 目标被服务器拒绝";);
 68    } 不然 {
 69      RCLCPP_INFO(->;get_logger(), 服务器接受目标,等待结果";);
 70    }
 71  }
 72
 73  空白 反馈回调(
 74    目标手柄斐波纳契::SharedPtr,
 75     标准::共享_ptr<; 斐波那契::反馈意见>; 反馈)
 76  {
 77    标准::字符串流 ss;
 78    ss <<; "收到序列中的下一个号码:";;
 79    对于 (汽车 编号 : 反馈->;部分序列) {
 80      ss <<; 编号 <<; " ";;
 81    }
 82    RCLCPP_INFO(->;get_logger(), ss.字符串().c_str());
 83  }
 84
 85  空白 result_callback( 目标手柄斐波纳契::包裹结果 及样品; 结果)
 86  {
 87    开关 (结果.代码) {
 88      个案 rclcpp_action::结果代码::成功:
 89        断裂;
 90      个案 rclcpp_action::结果代码::中止:
 91        RCLCPP_ERROR(->;get_logger(), 目标失败";);
 92        返回;
 93      个案 rclcpp_action::结果代码::取消:
 94        RCLCPP_ERROR(->;get_logger(), 目标被取消";);
 95        返回;
 96      默认:
 97        RCLCPP_ERROR(->;get_logger(), 未知结果代码";);
 98        返回;
 99    }
100    标准::字符串流 ss;
101    ss <<; 已收到结果: ";;
102    对于 (汽车 编号 : 结果.结果->;顺序) {
103      ss <<; 编号 <<; " ";;
104    }
105    RCLCPP_INFO(->;get_logger(), ss.字符串().c_str());
106    rclcpp::关闭();
107  }
108};  // 类 FibonacciActionClient
109
110}  // 命名空间 action_tutorials_cpp
111
112rclcpp_components_register_node(动作教程_cpp::FibonacciActionClient)

前几行包含了我们需要编译的所有头文件。

接下来,我们创建一个从 rclcpp::Node:

 FibonacciActionClient :  rclcpp::节点

的构造函数 FibonacciActionClient 类将节点名称初始化为 斐波那契行动客户端:

  不含糊 FibonacciActionClient( rclcpp::节点选项 及样品; 选项)
  : 节点("fibonacci_action_client";, 选项)

构造函数还会实例化一个新的动作客户端:

    ->;客户端ptr_ = rclcpp_action::创建客户端<;斐波那契>;(
      ,
      "fibonacci";);

行动客户需要具备 3 个条件:

  1. 模板化操作类型名称: 斐波那契.

  2. 要添加动作客户端的 ROS 2 节点: .

  3. 行动名称: 斐波那契.

我们还实例化了一个 ROS 定时器,它将启动对 发送目标:

    ->;定时器 = ->;创建隔离墙计时器(
      标准::计时器::毫秒数(500),
      标准::约束(及样品;FibonacciActionClient::发送目标, ));

计时器到期时,它会调用 发送目标:

  空白 发送目标()
  {
    使用 命名空间 标准::占位符;

    ->;定时器->;取消();

    如果 (!->;客户端ptr_->;等待行动服务器()) {
      RCLCPP_ERROR(->;get_logger(), "等待后行动服务器不可用";);
      rclcpp::关闭();
    }

    汽车 goal_msg = 斐波那契::目标();
    goal_msg.订单 = 10;

    RCLCPP_INFO(->;get_logger(), 发送目标";);

    汽车 发送目标选项 = rclcpp_action::客户<;斐波那契>::发送目标选项();
    发送目标选项.目标响应回调 =
      标准::约束(及样品;FibonacciActionClient::目标响应回调, , _1);
    发送目标选项.反馈回调 =
      标准::约束(及样品;FibonacciActionClient::反馈回调, , _1, _2);
    发送目标选项.result_callback =
      标准::约束(及样品;FibonacciActionClient::result_callback, , _1);
    ->;客户端ptr_->;异步发送目标(goal_msg, 发送目标选项);
  }

该功能执行以下操作

  1. 取消计时器(因此只调用一次)。

  2. 等待行动服务器出现。

  3. 实例化一个新的 Fibonacci::Goal.

  4. 设置响应、反馈和结果回调。

  5. 将目标发送到服务器。

服务器接收并接受目标后,会向客户端发送一个响应。该响应由 目标响应回调:

  空白 目标响应回调(标准::共享未来<;目标手柄斐波纳契::SharedPtr>; 未来)
  {
    汽车 目标句柄 = 未来.获取();
    如果 (!目标句柄) {
      RCLCPP_ERROR(->;get_logger(), 目标被服务器拒绝";);
    } 不然 {
      RCLCPP_INFO(->;get_logger(), 服务器接受目标,等待结果";);
    }
  }

假设服务器接受了目标,就会开始处理。对客户端的任何反馈都将由 反馈回调:

  空白 反馈回调(
    目标手柄斐波纳契::SharedPtr,
     标准::共享_ptr<; 斐波那契::反馈意见>; 反馈)
  {
    标准::字符串流 ss;
    ss <<; "收到序列中的下一个号码:";;
    对于 (汽车 编号 : 反馈->;部分序列) {
      ss <<; 编号 <<; " ";;
    }
    RCLCPP_INFO(->;get_logger(), ss.字符串().c_str());
  }

服务器处理完毕后,会向客户端返回一个结果。结果由 result_callback:

  空白 result_callback( 目标手柄斐波纳契::包裹结果 及样品; 结果)
  {
    开关 (结果.代码) {
      个案 rclcpp_action::结果代码::成功:
        断裂;
      个案 rclcpp_action::结果代码::中止:
        RCLCPP_ERROR(->;get_logger(), 目标失败";);
        返回;
      个案 rclcpp_action::结果代码::取消:
        RCLCPP_ERROR(->;get_logger(), 目标被取消";);
        返回;
      默认:
        RCLCPP_ERROR(->;get_logger(), 未知结果代码";);
        返回;
    }
    标准::字符串流 ss;
    ss <<; 已收到结果: ";;
    对于 (汽车 编号 : 结果.结果->;顺序) {
      ss <<; 编号 <<; " ";;
    }
    RCLCPP_INFO(->;get_logger(), ss.字符串().c_str());
    rclcpp::关闭();
  }

我们现在有了一个功能完备的行动客户端。让我们构建并运行它。

3.2 编译行动客户端

在上一节中,我们将动作客户端代码安装到位。为了让它编译和运行,我们还需要做几件事。

首先,我们需要设置 CMakeLists.txt,以便编译动作客户端。打开 action_tutorials_cpp/CMakeLists.txt之后添加以下内容 查找软件包 电话

add_library(action_client SHARED
  src/fibonacci_action_client.cpp)
target_include_directories(action_client PRIVATE
  $<BUILD_INTERFACE:${cmake_current_source_dir}/include>;
  $<INSTALL_INTERFACE:include>;)
target_compile_definitions(action_client
  PRIVATE "ACTION_TUTORIALS_CPP_BUILDING_DLL")
ament_target_dependencies(action_client
  "action_tutorials_interfaces";
  "rclcpp";
  "rclcpp_action";
  "rclcpp_components")
rclcpp_components_register_node(action_client PLUGIN "action_tutorials_cpp::FibonacciActionClient" EXECUTABLE fibonacci_action_client)
install(TARGETS
  动作客户端
  存档目的地 lib
  图书目录 lib
  运行时间目的地 bin)

现在我们可以编译软件包了。转到 action_ws然后运行:

胶管 构建

这将编译整个工作区,包括 斐波那契行动客户端动作教程_cpp 包装

3.3 运行行动客户端

现在我们已经创建了动作客户端,可以运行它了。首先,确保动作服务器在单独的终端中运行。现在,将我们刚刚构建的工作区 (action_ws),并尝试运行动作客户端:

玫瑰2 运行 动作教程_cpp 斐波那契行动客户端

你应该能看到目标被接受、反馈被打印以及最终结果的记录信息。

摘要

在本教程中,您将逐行组装 C++ 动作服务器和动作客户端,并配置它们以交换目标、反馈和结果。