ROS2 Jazzy 自定义 Action 实现小车导航(实时进度反馈)
·
一、项目简介
ROS2 中 Action 专门用于长时间运行的任务(导航、机械臂运动、拍照巡检等),相比 Service 一问一答、Topic 单向发布,Action 拥有三段完整交互:
- Goal:客户端下发执行目标坐标
- Feedback:服务端循环推送实时进度(剩余距离)
- 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 导航服务端


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

运行效果: 客户端等待导航任务执行完成,最终输出关键日志: [INFO] [nav_client]: 任务完成!最终坐标 x=6.94 y=7.93 代表完整 Action 通信闭环,客户端成功接收服务端返回的导航结果。
八、华为云开发环境踩坑总结
ros2 run提示 Package not found 云开发空间环境索引存在 bug,ros2 pkg检索失效,直接执行install目录下二进制文件即可绕过该问题;- 直接运行二进制提示缺少
.so动态库 必须前置执行source install/setup.bash加载自定义动作库路径,否则程序找不到nav_interfaces接口动态文件; - 编译出现黄色 CATKIN 警告 环境内置旧 catkin 兼容变量,无任何功能影响,无需修改 CMakeLists 屏蔽;
- 旧版本客户端红色
unknown response日志已修复 本次更新后的客户端代码规避了 Jazzy 版本 API 兼容报错,运行日志仅保留正常 INFO 信息,无多余红色警告。
九、整体流程梳理
- 客户端构造导航目标 (7.0,8.0),异步发送给 Action 服务端;
- 服务端接收目标,新开独立子线程模拟小车行驶;
- 循环计算小车与目标的距离,每秒推送
Feedback实时进度; - 小车距离目标小于 0.1,判定抵达终点,封装
Result结果; - 客户端接收任务完成结果,打印小车实际抵达坐标; 完整实现 ROS2 Action「下发目标 - 实时进度反馈 - 任务结果返回」全套标准通信流程。
十、拓展方向
- 在代码中增加取消任务逻辑,测试
on_cancel回调; - 编写 launch 文件,一键同时启动服务端 + 客户端;
- 新增多组导航目标,实现连续多点导航;
- 结合 TF2 / 激光雷达,替换纯模拟行驶逻辑,对接真实机器人底盘。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)