一、项目简介

ROS2 中 Action 专门用于长时间运行的任务(导航、机械臂运动、拍照巡检等),相比 Service 一问一答、Topic 单向发布,Action 拥有三段完整交互:

  1. Goal:客户端下发执行目标坐标
  2. Feedback:服务端循环推送实时进度(剩余距离)
  3. Result:任务结束后返回最终执行结果

本案例基于华为云开发者空间 + ROS2 Jazzy,自定义Move.action导航动作,客户端下发目标 (7.0,8.0),服务端模拟小车渐进行驶、每秒推送剩余距离,抵达终点后返回实际坐标。

二、工程目录结构

action_nav_ws
├── build install log  # colcon编译自动生成缓存文件夹
└── src
    ├── nav_interfaces        # 规范分包:单独存放自定义Action接口
    │   └── action
    │       └── Move.action   # 导航动作定义文件
    └── nav_action_demo       # 业务包:Action服务端+客户端源码
        ├── src
        │   ├── nav_server.cpp # 导航动作服务端
        │   └── nav_client.cpp# 导航动作客户端
        ├── launch             # 预留启动文件目录
        ├── include
        ├── CMakeLists.txt
        └── package.xml

分层优势:接口与业务代码完全解耦,其他工程可直接复用nav_interfaces动作包。

三、自定义 Action 接口 Move.action

语法说明:三段内容使用---强制分隔,顺序固定为 Goal → Result → Feedback。

四、Action 服务端 nav_server.cpp

#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "nav_interfaces/action/move.hpp"

using Move = nav_interfaces::action::Move;

class NavServer : public rclcpp::Node
{
public:
  NavServer() : Node("nav_server")
  {
    // 创建Action服务,绑定3个核心回调函数
    server_ = rclcpp_action::create_server<Move>(
      this,
      "move_task",
      std::bind(&NavServer::on_goal, this, std::placeholders::_1),
      std::bind(&NavServer::on_cancel, this, std::placeholders::_1),
      std::bind(&NavServer::on_accept, this, std::placeholders::_1)
    );
  }

private:
  rclcpp_action::Server<Move>::SharedPtr server_;

  // 1. 收到客户端目标请求回调
  rclcpp_action::GoalResponse on_goal(const rclcpp_action::GoalUUID &, std::shared_ptr<const Move::Goal> goal)
  {
    RCLCPP_INFO(get_logger(), "收到目标点:%.2f , %.2f", goal->target_x, goal->target_y);
    // 接受本次导航任务
    return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
  }

  // 2. 客户端取消任务回调
  rclcpp_action::CancelResponse on_cancel(std::shared_ptr<rclcpp_action::ServerGoalHandle<Move>>)
  {
    RCLCPP_INFO(get_logger(), "任务被取消");
    return rclcpp_action::CancelResponse::ACCEPT;
  }

  // 3. 接受任务,新开子线程执行导航(防止阻塞节点spin)
  void on_accept(std::shared_ptr<rclcpp_action::ServerGoalHandle<Move>> handle)
  {
    std::thread{std::bind(&NavServer::execute, this, handle)}.detach();
  }

  // 导航核心执行逻辑
  void execute(std::shared_ptr<rclcpp_action::ServerGoalHandle<Move>> handle)
  {
    auto goal = handle->get_goal();
    auto feedback = std::make_shared<Move::Feedback>();
    auto result = std::make_shared<Move::Result>();

    float now_x = 0.0f;
    float now_y = 0.0f;
    rclcpp::Rate loop_rate(1); // 1Hz,每秒更新一次进度

    while (rclcpp::ok())
    {
      // 判断任务是否被客户端取消
      if (handle->is_canceling())
      {
        handle->canceled(result);
        return;
      }
      // 模拟小车缓慢向目标靠近
      now_x += (goal->target_x - now_x) * 0.2;
      now_y += (goal->target_y - now_y) * 0.2;
      // 计算当前距离目标的剩余距离
      float dist = sqrt(pow(goal->target_x - now_x, 2) + pow(goal->target_y - now_y, 2));
      feedback->remaining_dist = dist;
      // 向客户端推送实时进度反馈
      handle->publish_feedback(feedback);
      RCLCPP_INFO(get_logger(), "剩余距离:%.2f", dist);

      // 距离小于0.1,判定小车抵达终点
      if (dist < 0.1)
      {
        result->finish_state = true;
        result->final_x = now_x;
        result->final_y = now_y;
        handle->succeed(result);
        RCLCPP_INFO(get_logger(), "抵达目标!坐标:%.2f , %.2f", now_x, now_y);
        break;
      }
      loop_rate.sleep();
    }
  }
};

int main(int argc, char ** argv)
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<NavServer>());
  rclcpp::shutdown();
  return 0;
}

五、Action 客户端 nav_client.cpp

#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "nav_interfaces/action/move.hpp"

using Move = nav_interfaces::action::Move;
using namespace std::chrono_literals;

class NavClient : public rclcpp::Node
{
public:
  NavClient() : Node("nav_client")
  {
    // 客户端绑定服务端动作名称 move_task
    client_ = rclcpp_action::create_client<Move>(this, "move_task");
  }

  // 下发导航目标接口
  void send_task(float x, float y)
  {
    // 等待服务端上线,5秒未连接直接报错返回
    if (!client_->wait_for_action_server(5s))
    {
      RCLCPP_ERROR(get_logger(), "无法连接服务端");
      return;
    }
    // 构造目标坐标(栈对象规避Jazzy版本类型报错)
    Move::Goal goal;
    goal.target_x = x;
    goal.target_y = y;

    // 异步发送导航目标
    auto future = client_->async_send_goal(goal);
    rclcpp::spin_until_future_complete(this->get_node_base_interface(), future);
    auto goal_handle = future.get();

    if (!goal_handle)
    {
      RCLCPP_ERROR(get_logger(), "目标被服务端拒绝");
      return;
    }

    // 异步获取导航最终结果
    auto res_future = client_->async_get_result(goal_handle);
    rclcpp::spin_until_future_complete(this->get_node_base_interface(), res_future);
    auto result_wrap = res_future.get();

    // 导航成功,打印小车实际抵达坐标
    if (result_wrap.code == rclcpp_action::ResultCode::SUCCEEDED)
    {
      RCLCPP_INFO(get_logger(), "任务完成!最终坐标 x=%.2f y=%.2f",
                  result_wrap.result->final_x, result_wrap.result->final_y);
    }
  }

private:
  rclcpp_action::Client<Move>::SharedPtr client_;
};

int main(int argc, char ** argv)
{
  rclcpp::init(argc, argv);
  auto node = std::make_shared<NavClient>();
  // 下发导航目标:x=7.0 y=8.0
  node->send_task(7.0, 8.0);
  rclcpp::spin(node);
  rclcpp::shutdown();
  return 0;
}

六、完整编译命令与日志

日志说明

编译输出中黄色警告 CATKIN_INSTALL_INTO_PREFIX_ROOT 是华为云开发环境自带冗余变量,不属于代码错误,完全不影响编译、运行,可直接忽略; 日志末尾出现两行 Finished <<< 包名,代表编译全部成功,可执行文件生成在install文件夹内。

七、分终端运行测试

终端 1:启动 Action 导航服务端

运行效果:

  1. 接收客户端下发目标 7.00 , 8.00
  2. 每秒打印实时剩余距离,模拟小车持续向目标靠近
  3. 距离小于 0.1 时打印「抵达目标!坐标:6.94 , 7.93」,导航任务结束

终端 2:启动 Action 导航客户端

运行效果: 客户端等待导航任务执行完成,最终输出关键日志: [INFO] [nav_client]: 任务完成!最终坐标 x=6.94 y=7.93 代表完整 Action 通信闭环,客户端成功接收服务端返回的导航结果。

八、华为云开发环境踩坑总结

  1. ros2 run 提示 Package not found 云开发空间环境索引存在 bug,ros2 pkg 检索失效,直接执行install目录下二进制文件即可绕过该问题;
  2. 直接运行二进制提示缺少 .so 动态库 必须前置执行 source install/setup.bash 加载自定义动作库路径,否则程序找不到nav_interfaces接口动态文件;
  3. 编译出现黄色 CATKIN 警告 环境内置旧 catkin 兼容变量,无任何功能影响,无需修改 CMakeLists 屏蔽;
  4. 旧版本客户端红色unknown response日志已修复 本次更新后的客户端代码规避了 Jazzy 版本 API 兼容报错,运行日志仅保留正常 INFO 信息,无多余红色警告。

九、整体流程梳理

  1. 客户端构造导航目标 (7.0,8.0),异步发送给 Action 服务端;
  2. 服务端接收目标,新开独立子线程模拟小车行驶;
  3. 循环计算小车与目标的距离,每秒推送Feedback实时进度;
  4. 小车距离目标小于 0.1,判定抵达终点,封装Result结果;
  5. 客户端接收任务完成结果,打印小车实际抵达坐标; 完整实现 ROS2 Action「下发目标 - 实时进度反馈 - 任务结果返回」全套标准通信流程。

十、拓展方向

  1. 在代码中增加取消任务逻辑,测试on_cancel回调;
  2. 编写 launch 文件,一键同时启动服务端 + 客户端;
  3. 新增多组导航目标,实现连续多点导航;
  4. 结合 TF2 / 激光雷达,替换纯模拟行驶逻辑,对接真实机器人底盘。
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐