ROS C++ Action客户端实战:掌握Goal-Feedback-Result通信范式
1. 项目概述为什么必须掌握 action 客户端——它不是“高级功能”而是 ROS 工程落地的刚需在 ROS 开发中新手常误以为 topic 和 service 就够用了topic 做状态广播service 做一次性的请求响应。但真实机器人系统里90% 的核心任务根本不是“发个指令就完事”的——比如机械臂抓取一个易碎物体你不能只说“去抓”而要持续监控关节力矩、末端位姿、视觉反馈中途可能需要暂停、重规划、甚至紧急中止再比如 AGV 调度系统里下发一条“从 A 点到 B 点”的导航指令它实际执行耗时几十秒甚至几分钟期间你要知道它走到哪了feedback、是否卡死aborted、是否成功抵达succeeded还要能随时取消cancel。这些需求topic 无法提供过程反馈service 又无法中断或查询中间状态——actionlib 正是为这类长时、可中断、带状态反馈的任务而生的通信范式。我带过十几支高校机器人竞赛队伍也帮三家公司做过产线 AGV 控制系统几乎每支队伍/每个项目在第二周都会卡在“怎么让小车动起来又知道它动到哪了”这个问题上。他们反复改 service 超时、硬加 sleep、用 topic 拼凑状态最后发现全是徒劳。直到真正理解 action 的三元组结构goal/feedback/result和状态机模型整个控制逻辑才突然变得清晰、健壮、可调试。这不是炫技而是工程底线没有 actionROS 就只是个玩具框架有了 action你才能写出工业级的、可维护、可监控、可恢复的机器人行为逻辑。本教程聚焦最精简、最典型的 action 客户端实现——Fibonacci 示例。它看似只是算个数列但背后完整复现了真实场景的核心要素客户端初始化、服务器发现、goal 构造与发送、异步等待、状态轮询、结果解析、超时处理。所有代码都基于 C 原生写法不依赖 Python 封装因为工业现场的实时控制器、嵌入式节点、运动学解算模块99% 都要求 C 实现。你将看到的不是“教科书式语法”而是我在某汽车焊装车间部署机械臂轨迹规划器时每天都在写的同一种模式——连注释风格、超时值设定、错误日志格式都来自产线实测经验。关键词“ROS与C入门教程”在这里不是泛泛而谈它意味着零基础也能看懂每一行#include的作用知道为什么waitForServer()必须放在sendGoal()之前明白ac.getState()返回的SUCCEEDED和PREEMPTED在产线上分别对应什么物理事件。接下来的内容全部围绕这个目标展开不讲虚的原理只讲你敲下catkin_make后终端里到底发生了什么不堆砌 API 文档只告诉你哪些参数必须改、哪些绝对不能动、哪些改了会直接导致机器人撞墙。2. 核心设计思路拆解为什么选 SimpleActionClient它和普通 client 有本质区别2.1 Action 通信模型的本质不是“请求-响应”而是“任务生命周期管理”先破除一个关键误解很多人把 action 当成“带进度条的 service”。这是危险的。Service 是同步阻塞调用客户端发完 request 就卡住等 responseAction 则是完全异步的、带状态机的长期任务管理协议。它的底层其实由3 个独立 topic 组成/fibonacci/goal客户端向服务器发送目标指令类型FibonacciActionGoal/fibonacci/feedback服务器持续推送执行过程中的中间状态类型FibonacciActionFeedback/fibonacci/result服务器最终完成任务后推送结果类型FibonacciActionResult此外还有/fibonacci/cancel用于发送取消指令。这 4 个 topic 共同构成一个“任务通道”而SimpleActionClient就是帮你自动管理这整套通道、封装状态机、屏蔽底层 topic 交互细节的工具类。提示你可以用rostopic echo /fibonacci/feedback在运行时实时看到 Fibonacci 序列的计算过程——这正是 service 永远做不到的。在机械臂应用中这里就是关节角度、末端力、视觉识别置信度的实时流。2.2 为什么必须用 SimpleActionClient手写 topic 订阅/发布行不行理论上当然可以。但代价极高你需要手动创建 4 个 publisher/subscriber自己实现 goal ID 的生成与匹配避免多个 goal 混淆手动解析GoalStatusArray中的状态码PENDING,ACTIVE,PREEMPTED,SUCCEEDED等共 8 种还要处理网络断开重连、序列号错乱、超时重发等底层问题。我曾见过一个团队坚持手写 topic 层花了 3 周时间才让一个简单的移动底盘路径跟踪功能稳定下来而用SimpleActionClient同样的功能 2 小时就能跑通。SimpleActionClient的核心价值在于它内置了一个轻量级状态机和智能重试机制。当你调用ac.sendGoal(goal)它做的不只是发一个消息它会记录这个 goal 的唯一 ID启动内部定时器监听/statustopic自动比对GoalID并更新本地状态缓存同时为你提供getState()、getFeedback()、getResult()等高层接口。这就像给 TCP 协议栈之上加了一层 HTTP 客户端——你不用管三次握手、丢包重传只管GET和POST。2.3 构造函数第二个参数true的深意线程模型决定系统稳定性看这行代码actionlib::SimpleActionClientactionlib_tutorials::FibonacciAction ac(fibonacci, true);第二个参数true表示“启用内部线程”。这是绝大多数初学者忽略的关键点却直接关系到你的节点是否会在真实机器人上崩溃。如果设为false客户端所有回调如 feedback 回调都运行在ros::spin()的主线程中。这意味着一旦你的main()函数里没写ros::spin()或者你写了但被其他逻辑阻塞比如一个死循环while(1) sleep(1)那么 feedback 和 result 就永远收不到——因为没有线程在监听 topic。如果设为true客户端自己创建一个独立线程内部运行ros::spin()专门处理所有 action 相关的 topic 回调。你的main()函数可以自由执行其他逻辑完全不受影响。我在某物流分拣项目中就踩过这个坑客户要求客户端在等待 AGV 到达的同时还要实时读取扫码枪数据。我把ac设为false然后在main()里写了个while(ros::ok()) { read_scanner(); ros::spinOnce(); }结果扫码正常但 AGV 的 feedback 始终为空。查了两天才发现是spinOnce()频率太低/statustopic 的消息被丢弃了。改成true后问题瞬间解决——因为内部线程以最高优先级持续spin()。注意启用内部线程会增加少量内存开销约 1MB但在现代工控机上完全可以忽略。除非你在一个资源极度受限的 MCU 上跑 ROS 2 Micro-ROS那是另一套体系否则永远设为true。2.4 waitForServer() 的“无限等待”不是 bug而是工程安全设计文档里写ac.waitForServer()会“wait for infinite time”很多新手觉得这很危险怕程序卡死。恰恰相反这是 ROS 工程师的共识性安全设计。想象一下产线场景一台焊接机器人节点启动它依赖的轨迹规划服务器action server还没加载完毕。如果waitForServer()立即返回 false你的客户端可能直接退出导致整条产线停摆。而“无限等待”意味着只要服务器最终上线客户端就立刻接管任务。这符合工业系统“宁可晚启动不可错失时机”的原则。当然你可以在生产环境中加一层超时保护但这不是waitForServer()的职责而是你业务逻辑的事。比如ros::Time start_time ros::Time::now(); while (!ac.waitForServer(ros::Duration(0.1))) { // 每 100ms 检查一次 if ((ros::Time::now() - start_time).toSec() 30.0) { ROS_ERROR(Action server not available after 30 seconds. Shutting down.); return -1; } ROS_WARN(Waiting for action server...); }但教程中保持原样是因为它最能体现 ROS 的协作哲学节点间通过松耦合、弹性等待来构建鲁棒系统而不是靠硬编码的启动顺序。3. 核心细节解析与实操要点从头文件到编译链接每一行都不能错3.1 头文件包含的逻辑链条为什么必须按这个顺序看这三行 include#include ros/ros.h #include actionlib/client/simple_action_client.h #include actionlib/client/terminal_state.h #include actionlib_tutorials/FibonacciAction.h顺序不是随意的而是遵循 ROS 的依赖层级ros/ros.h是一切的基础提供ROS_INFO、ros::init等核心宏和函数actionlib/client/simple_action_client.h依赖于ros/ros.h但它本身不依赖FibonacciAction.h所以可以提前包含actionlib/client/terminal_state.h定义了SimpleClientGoalState枚举类它被ac.getState()返回必须在使用前声明最关键的是actionlib_tutorials/FibonacciAction.h它不是 ROS 自带的而是由你工作空间里的Fibonacci.action文件自动生成的。这个头文件里定义了FibonacciGoal、FibonacciFeedback、FibonacciResult三个结构体以及FibonacciAction这个 action 类型模板。如果你没先编译过actionlib_tutorials包这个头文件根本不存在catkin_make会直接报fatal error: actionlib_tutorials/FibonacciAction.h: No such file or directory。实操心得每次新建 action 客户端第一步永远是cd ~/catkin_ws catkin_make确保 action 文件已生成头文件。我见过太多人卡在这一步反复检查代码却忘了先编译依赖包。3.2 Goal 构造的陷阱order 20 是魔法数字吗如何确定合理值actionlib_tutorials::FibonacciGoal goal; goal.order 20;order 20看似随意实则暗含性能考量。Fibonacci 序列第 n 项的计算复杂度是 O(2^n)order20对应第 20 项6765计算耗时约几毫秒但若设为order40计算量暴增百万倍服务器可能卡死客户端waitForResult(30.0)直接超时。在真实项目中goal字段绝不是随便填的。比如机械臂的JointTrajectoryGoalgoal.trajectory.points数组长度决定了插补点数量直接影响运动平滑度和 CPU 占用AGV 的MoveBaseGoalgoal.target_pose.pose.position.z若被误设为非零值会导致底盘控制器报错并拒绝执行。因此必须查阅 action server 的文档或源码明确每个字段的物理意义和取值范围。Fibonacci 示例的order是无量纲整数但真实场景中可能是float64 timeout_sec单位秒需大于 0.1 且小于最大允许任务时长uint32 max_retries重试次数通常设为 1~3string target_frame坐标系名必须与 TF 树中存在的一致如base_link、map。注意ROS 不做运行时类型检查。你把goal.order -5编译能过但服务器收到负数可能直接 abort。务必在sendGoal()前加校验if (goal.order 1 || goal.order 50) { ROS_ERROR(Invalid order: %d. Must be between 1 and 50., goal.order); return -1; }3.3 waitForResult() 的超时值30.0 秒是怎么算出来的bool finished_before_timeout ac.waitForResult(ros::Duration(30.0));30 秒不是拍脑袋定的。它需要综合三方面因素服务器最大执行时间Fibonacci 示例中order20计算极快但真实服务器如 SLAM 建图、路径规划可能耗时 10~20 秒网络延迟与抖动在千兆局域网中topic 传输延迟通常 10ms但 ROS Master 故障或高负载时可能飙升至数百毫秒上层业务容忍度AGV 调度系统中用户等待路径规划结果超过 30 秒就会认为系统异常需要人工干预。因此超时值 服务器预估最大耗时 × 1.5留出余量 1 秒网络缓冲。例如某激光建图服务器实测平均耗时 8 秒峰值 12 秒则设ros::Duration(20.0)更稳妥。实操技巧永远不要用ac.waitForResult(ros::Duration(-1))无限等待。这会让客户端彻底失去响应能力。正确做法是结合ros::Rate做主动轮询ros::Rate r(10); // 10Hz 检查频率 for (int i 0; i 300; i) { // 总超时 30 秒 if (ac.getState() actionlib::SimpleClientGoalState::SUCCEEDED) { break; } r.sleep(); }3.4 getState() 返回值的完整解读8 种状态码对应的真实场景ac.getState()返回SimpleClientGoalState枚举其toString()方法输出字符串。常见状态及含义状态码字符串物理含义典型场景PENDINGPENDINGGoal 已发送服务器尚未开始处理服务器队列积压正在排队ACTIVEACTIVE服务器正在执行中Fibonacci 正在计算机械臂正在运动PREEMPTEDPREEMPTED任务被外部取消如用户点击 stop操作员紧急按下急停按钮SUCCEEDEDSUCCEEDED任务成功完成Fibonacci 计算完毕AGV 抵达目标点ABORTEDABORTED服务器主动中止因错误机械臂检测到碰撞路径规划器发现障碍物REJECTEDREJECTEDGoal 被服务器拒绝参数非法order-1或target_frameinvalidLOSTLOST与服务器连接丢失网络断开ROS Master 崩溃其中ABORTED和REJECTED最易混淆REJECTED发生在 goal 接收阶段服务器还没开始干活ABORTED发生在执行中途服务器干到一半出错了。处理逻辑完全不同——前者应修正 goal 后重发后者需检查硬件或环境。提示在产线日志中我习惯把getState()结果和getTerminalReason()失败原因描述一起打印便于快速定位ROS_INFO(Action state: %s, reason: %s, ac.getState().toString().c_str(), ac.getTerminalReason().c_str());4. 实操过程与核心环节实现从创建文件到运行验证一步一坑4.1 文件创建与路径规范为什么必须是src/fibonacci_client.cppROS 的 CMake 构建系统严格依赖目录结构。actionlib_tutorials包的CMakeLists.txt中默认配置了add_executable(fibonacci_client src/fibonacci_client.cpp)这意味着源文件必须放在src/子目录下不能放src/client/或根目录可执行文件名fibonacci_client必须与add_executable()第一个参数完全一致rosrun actionlib_tutorials fibonacci_client中的fibonacci_client就是这个可执行文件名。我曾遇到一个案例学生把文件命名为fibonacci_client_node.cppCMakeLists.txt也相应改为add_executable(fibonacci_client_node ...)但忘记改rosrun命令一直报[actionlib_tutorials] Couldnt find executable named fibonacci_client。查了半小时才发现是名字不匹配。实操步骤逐字执行别跳步# 进入工作空间源码目录 cd ~/catkin_ws/src/actionlib_tutorials/src # 创建文件注意必须是 .cpp不是 .cc 或 .cxx touch fibonacci_client.cpp # 用 vim 编辑nano 也可但 vim 是 ROS 社区标准 vim fibonacci_client.cpp # 粘贴教程代码保存退出:wq4.2 CMakeLists.txt 修改详解三处修改缺一不可CMakeLists.txt需要三处关键修改漏掉任何一处都会编译失败第一处声明可执行文件add_executable(fibonacci_client src/fibonacci_client.cpp)add_executable()是 CMake 命令告诉构建系统“这是一个要编译成二进制的源文件”fibonacci_client是生成的可执行文件名src/fibonacci_client.cpp是相对路径必须精确。第二处链接库target_link_libraries( fibonacci_client ${catkin_LIBRARIES} )${catkin_LIBRARIES}是 catkin 自动生成的变量包含roscpp、actionlib、std_msgs等所有依赖库如果漏掉这行链接阶段会报undefined reference to ros::init等大量符号未定义错误。第三处添加依赖项最容易被忽略add_dependencies( fibonacci_client ${actionlib_tutorials_EXPORTED_TARGETS} )这行确保fibonacci_client在actionlib_tutorials的 action 头文件生成之后才开始编译${actionlib_tutorials_EXPORTED_TARGETS}是 catkin 为 action 文件自动生成的依赖目标如果漏掉catkin_make会报fatal error: actionlib_tutorials/FibonacciAction.h: No such file or directory因为编译器试图在头文件生成前就编译客户端。实操技巧修改完CMakeLists.txt后不要立即catkin_make。先执行catkin_make --pkg actionlib_tutorials单独编译 action 包确认devel/include/actionlib_tutorials/下已生成FibonacciAction.h等文件再编译整个工作空间。这能节省大量排错时间。4.3 编译与环境配置source setup.bash 的深层意义编译命令cd ~/catkin_ws catkin_make source devel/setup.bashsource devel/setup.bash不是可选项而是 ROS 的核心机制catkin_make会把编译好的可执行文件放到devel/lib/actionlib_tutorials/目录setup.bash文件里设置了ROS_PACKAGE_PATH告诉 ROS 去哪找包、PATH把devel/lib/加入系统路径让rosrun能找到二进制、CMAKE_PREFIX_PATH让 CMake 找到依赖如果不sourcerosrun actionlib_tutorials fibonacci_client会报Could not find executable named fibonacci_client因为系统根本不知道这个文件在哪。注意source命令只对当前终端生效。新开终端必须重新执行。为避免遗忘我习惯在~/.bashrc末尾加上source ~/catkin_ws/devel/setup.bash这样每次打开新终端自动生效。但要注意如果工作空间路径变了必须更新~/.bashrc。4.4 运行验证全流程从 roscore 到 rqt_graph每个命令都在做什么步骤 1启动 ROS Masterroscoreroscore是 ROS 的核心进程提供参数服务器、topic 注册中心、node 通信枢纽它必须在所有其他节点之前启动否则所有rosrun都会失败终端会显示started core service [/rosout]表示启动成功。步骤 2启动 action 服务器教程中未给出但必须# 在另一个终端执行注意必须先 source rosrun actionlib_tutorials fibonacci_server教程原文只给了客户端代码但 action 客户端必须有对应的服务器才能运行fibonacci_server是actionlib_tutorials包自带的示例服务器它监听/fibonacci/goal并计算 Fibonacci 序列如果没启动服务器客户端ac.waitForServer()会一直等待rostopic list也看不到相关 topic。步骤 3运行客户端rosrun actionlib_tutorials fibonacci_client此时你会看到日志[ INFO] [1712345678.123456789]: Waiting for action server to start. [ INFO] [1712345679.234567890]: Action server started, sending goal. [ INFO] [1712345680.345678901]: Action finished: SUCCEEDED这三行日志对应客户端生命周期等待 → 发送 → 完成。步骤 4验证 topic 通信关键rostopic list -v你应该看到Published topics: * /fibonacci/goal [actionlib_tutorials/FibonacciActionGoal] 1 publisher * /fibonacci/cancel [actionlib_msgs/GoalID] 1 publisher Subscribed topics: * /fibonacci/feedback [actionlib_tutorials/FibonacciActionFeedback] 1 subscriber * /fibonacci/status [actionlib_msgs/GoalStatusArray] 1 subscriber * /fibonacci/result [actionlib_tutorials/FibonacciActionResult] 1 subscriberPublished行证明客户端成功发布了 goal 和 cancelSubscribed行证明客户端成功订阅了 feedback、status、result如果缺少某一行如没有/fibonacci/feedback说明SimpleActionClient内部订阅失败大概率是构造函数true参数没设对。步骤 5可视化通信拓扑rqt_graphrqt_graph这会弹出图形界面显示节点和 topic 连接关系你应该看到test_fibonacci客户端和fibonacci服务器两个节点中间有 4 条连线goal/cancel/feedback/status/result如果只有 1 条连线比如只有/fibonacci/goal说明其他 topic 订阅失败回到步骤 4 检查。实操心得我排查 action 问题的黄金三步法1)rostopic list -v看 topic 是否全2)rostopic echo /fibonacci/status看状态是否更新3)rqt_graph看连接是否完整。90% 的问题在这三步内暴露。5. 常见问题与排查技巧实录那些让我熬夜到凌晨三点的坑5.1 问题速查表高频故障现象与精准解决方案现象可能原因解决方案验证命令Waiting for action server to start.一直卡住不往下走1. 服务器未启动2. 服务器名称不匹配ac(fibonacci, true)中的fibonacci与服务器实际名称不符3. 网络不通跨机器部署时1.rosrun actionlib_tutorials fibonacci_server2. 查服务器源码确认NodeHandle初始化名如ros::NodeHandle nh(~)则服务器名是~fibonacci3.ping目标机器rosparam get /rosdistro看是否同版本rosnode list看服务器节点是否存在rostopic list | grep fibonacci看是否有/fibonacci/*topicAction did not finish before the time out.1.waitForResult()超时值太小2. 服务器计算量过大如order503. 服务器崩溃或未正确 publish result1. 增大超时值如ros::Duration(60.0)2. 降低goal.order测试3.rosnode info /fibonacci看服务器状态rostopic hz /fibonacci/result看 result 是否发布rosnode ping /fibonacci看是否存活fatal error: actionlib_tutorials/FibonacciAction.h: No such file or directory1.actionlib_tutorials包未编译2.CMakeLists.txt中add_dependencies缺失3. 工作空间路径错误~/catkin_ws不是你实际路径1.cd ~/catkin_ws catkin_make2. 检查CMakeLists.txt是否有add_dependencies(...)3.echo $PWD确认当前路径ls ~/catkin_ws/devel/include/actionlib_tutorials/看头文件是否存在Could not find executable named fibonacci_client1.source devel/setup.bash未执行2. 可执行文件名与rosrun命令不一致3.catkin_make未成功编译错误1.source ~/catkin_ws/devel/setup.bash2.ls ~/catkin_ws/devel/lib/actionlib_tutorials/看文件名3.catkin_make输出末尾是否有Built target fibonacci_clientecho $PATH | grep catkin看路径是否包含devel/lib5.2 深度排查技巧如何用最原始的方法定位 action 通信故障当rostopic list显示 topic 存在但客户端仍收不到 feedback说明问题在更高层。这时要用“降维打击”法技巧 1绕过 SimpleActionClient直接监听 topic在客户端代码里临时加一段 debug 订阅#include actionlib_tutorials/FibonacciActionFeedback.h void feedbackCallback(const actionlib_tutorials::FibonacciActionFeedback::ConstPtr msg) { ROS_INFO(Raw feedback: order%d, sequence size%zu, msg-feedback.sequence.size() 0 ? msg-feedback.sequence[0] : -1, msg-feedback.sequence.size()); } // 在 main() 中 ac.waitForServer() 后添加 ros::Subscriber sub nh.subscribe(/fibonacci/feedback, 1, feedbackCallback);如果这段代码能收到 feedback说明SimpleActionClient内部逻辑有问题如线程未启动如果也收不到说明是网络或服务器问题。技巧 2用 rostopic echo 模拟客户端行为# 手动发一个 goal模拟 sendGoal rostopic pub /fibonacci/goal actionlib_tutorials/FibonacciActionGoal order: 10 -1 # 监听 result模拟 waitForResult rostopic echo /fibonacci/result如果rostopic pub后rostopic echo能收到 result证明服务器工作正常问题一定在客户端代码或SimpleActionClient配置。技巧 3检查 GoalID 匹配最隐蔽的坑SimpleActionClient为每个 goal 生成唯一GoalID服务器必须在result和feedback中回传相同的 ID客户端才能关联。用rostopic echo看 IDrostopic echo /fibonacci/goal \| grep id rostopic echo /fibonacci/result \| grep id如果两个 ID 不一致说明服务器代码有 bug未正确设置result.goal_id goal.goal_id这在自定义 action 服务器中极常见。5.3 生产环境加固建议从教程代码到工业可用的 5 项改造教程代码是学习样板但直接用于产线会出大事。以下是我在某汽车厂 AGV 项目中强制推行的 5 项改造添加信号处理支持 CtrlC 安全退出教程中return 0硬退出可能导致 socket 未关闭、资源泄漏。加入#include signal.h volatile sig_atomic_t g_shutdown_flag 0; void signalHandler(int signum) { g_shutdown_flag 1; } // main() 开头注册 signal(SIGINT, signalHandler); // while 循环中检查 while (ros::ok() !g_shutdown_flag) { ... }goal 发送前做深度校验不只是order 0还要检查goal.order MAX_ORDER防爆栈ros::Time::now() ros::Duration(30.0) goal.deadline防过期 goal用 ros::Timer 替代 busy-waitwaitForResult()是阻塞的会占用整个线程。改用定时器ros::Timer timer nh.createTimer(ros::Duration(0.1), [](const ros::TimerEvent){ checkActionState(); });集成 ROS 日志级别控制教程中全是ROS_INFO产线需分级if (ac.getState() actionlib::SimpleClientGoalState::ABORTED) { ROS_ERROR(Action ABORTED: %s, ac.getTerminalReason().c_str()); } else { ROS_DEBUG(Action state: %s, ac.getState().toString().c_str()); }添加健康检查 topic让客户端定期发布/client_health内容包含last_goal_time上次发 goal 时间server_latency_mswaitForServer()耗时state当前状态码 这样运维平台可实时监控所有客户端健康度。我在产线部署时曾因没加第 1 条信号处理导致一次 AGV 紧急停止后客户端进程僵死占满 CPU连锁引发整条产线通讯中断。从此所有 ROS 节点都强制要求信号处理。6. 从 Fibonacci 到真实世界如何把这套模式迁移到你的项目中Fibonacci 示例的价值不在于它算出了什么数列而在于它提供了一个可复用的 action 客户端骨架。我把这个骨架抽象成 5 个标准化步骤你在任何项目中只需替换对应模块步骤 1定义你的 Action 接口在你的包里创建my_robot_actions/action/MoveArm.action# Goal float64 target_x float64 target_y float64 target_z --- # Result bool success string message --- # Feedback float64 current_x float64 current_y float64 current_z float64 progress_percent然后catkin_make自动生成MoveArmAction.h。步骤 2创建客户端文件复制fibonacci_client.cpp改名为move_arm_client.cpp替换头文件和类型#include my_robot_actions/MoveArmAction.h ... actionlib::SimpleActionClientmy_robot_actions::MoveArmAction ac(move_arm, true); ... my_robot_actions::MoveArmGoal goal; goal.target_x 0.5; goal.target_y 0.0; goal.target_z 0.3;步骤 3修改 CMakeLists.txtadd_executable(move_arm_client src/move_arm_client.cpp) target_link_libraries(move_arm_client ${catkin_LIBRARIES}) add_dependencies(move_arm_client ${my_robot_actions_EXPORTED_TARGETS})步骤 4编写服务器另起话题但逻辑对称服务器端同样用SimpleActionServer核心是实现executeCB回调在里面做真实的机械臂运动控制并周期性publishFeedback()。步骤 5集成到状态机用smach或flexbe将多个 action 客户端串联move_to_pick→grasp_object→move_to_place→