简介:ROS动作通信机制用于处理需要持续监控的机器人任务,区别于一次性请求-响应服务。该源码包围绕这一主题,提供动作服务器与客户端的C++实现,基于actionlib库演示SimpleActionServer与SimpleActionClient的完整用法,适合正在学习ROS机器人行为控制的开发者。资源源自学习笔记7,包含5个文件、总大小8KB:2个cpp源码分别展示服务端与客户端核心逻辑,1个action文件定义Goal、Result、Feedback消息结构,另附xml配置与txt说明,帮助理解功能包的组织方式和编译依赖。压缩包体量轻巧、目录结构清晰,方便直接对照代码掌握动作通信流程——从目标下发、任务执行到反馈监控与结果回传。已有227人学习使用,可作为机器人行为控制开发的入门参考,也能结合tf、geometry_msgs等组件扩展到实际项目中,缩短动作接口的搭建与调试时间。
1. 动作服务器与客户端这套源码,到底解决什么问题
拿到一个「学习笔记7源码-动作服务器与客户端.zip」,多数人的第一反应是解压、跑起来、看终端输出。但先别急着解压。这个标题其实已经把 ROS 里最重要的一类通信机制讲清楚了:动作服务器(Action Server)和动作客户端(Action Client)。在机器人系统里,话题(Topic)适合高频单向数据流,服务(Service)适合一问一答的短任务,而像导航、机械臂抓取这种耗时几秒甚至几分钟、中途要回报进度、还要能被叫停的操作,前两者都撑不住。动作(Action)就是专门为这类任务设计的通信原语,这套源码给的,正是一个能直接编译运行的 Action 最小工程。把它拆透,你就把 ROS 通信机制里最后一块短板补上了。适合正在学 ROS 通信、想从「能编译」跨到「能设计通信流程」的开发者。
2. 拆开 zip 看结构:Action 通信模型与三类原语
2.1 为什么长任务不能硬套话题和服务
很多新手是从话题和服务转过来学动作的,所以最容易犯的错,就是拿旧思路套新场景。话题是发布-订阅模型,发布者把数据丢到总线上,订阅者按自己的节奏取,发布者根本不知道数据有没有被消费。做高速里程计、图像流没问题,但要是发一个「去抓杯子」的指令,订阅端做完了还是做砸了,发布端完全无感知。服务是请求-响应模型,客户端发一个请求,服务端算完把结果返回,一问一答干脆利落,可一旦这个「算」持续十秒钟,客户端就得一直阻塞等,中途想取消、想看进度,协议里根本没有这些字段。
动作的设计目标很明确:一个能持续跟踪的任务句柄。它比服务多出三个能力,第一是反馈,服务端在执行过程中可以不断给客户端汇报百分比或状态;第二是取消,客户端随时可以发取消请求,服务端收到后停止动作并释放资源;第三是目标状态机,动作从提出、被接受、执行中到成功、被抢占、被取消,都有明确状态,调用方不用靠猜。所以判断一个通信需求该用哪种原语有个很粗暴的标准:超过两三秒、要进程条、可能要中断,直接上动作,别犹豫。
2.2 动作通信的三个目标与四段流程
明白为什么选动作之后,得把它的数据流拆清楚,否则后面读源码会卡在回调函数上。动作通信里只有三个数据对象。目标(Goal)是客户端告诉服务端「我要你做什么」的参数集合;反馈(Feedback)是服务端执行中回给客户端的中间状态;结果(Result)是任务结束时交付的最终输出。一个典型的动作流程分成四段:客户端先发送 Goal,服务端收到后决定接受还是拒绝;接受之后服务端开始干活,执行期间周期性发布 Feedback;任务正常结束,服务端返回 Result;任意时刻客户端如果不想等了,可以发 Cancel 请求,服务端确认后把任务标记为取消。
很多资料把动作描述成「带反馈的服务」,这个说法容易误导人。动作底层确实用了一组话题来传递数据,比如 ROS 2 里每个动作会生成 goal、result、feedback、cancel 五个话题,但你在应用层把它当成「一个可追踪的任务」来理解更合适。因为动作的核心价值不在传输,而在状态管理——谁在跟踪这个任务、它现在处于哪个阶段、怎么处理异常,这些才是动作比服务复杂也更好用的地方。
2.3 zip 里应该有哪些文件:一个动作工程的最小骨架
虽然每个课程整理源码的格式不一样,但一个能编译的动作工程通常跑不了这几样东西,拿到 zip 后可以对着目录核对。一般会有接口定义文件、服务端实现、客户端实现、构建配置和启动文件四类,少了任何一个都跑不起来。下面是这个 zip 里最可能出现的内容,我把它整理成一张清单,作为排查的第一步。
| 文件/目录 | 作用 | 缺失时的现象 |
|---|---|---|
| action/ 目录(如 Fibonacci.action) | 定义 Goal、Feedback、Result 三段的字段 | 找不到动作类型,编译直接报错 |
| CMakeLists.txt / package.xml | 注册动作接口、声明依赖 | 编译不生成消息头文件,代码里 import 失败 |
| action_server.py / action_client.py | 服务端与客户端的具体实现 | 节点起来了但没有动作服务,客户端等不到响应 |
| launch 文件或一键运行脚本 | 把两个节点串起来 | 要手动开两个终端,不利于验证 |
提示:如果 zip 里没有单独的动作接口定义,而是复用了 ROS 官方教学包里带的基本动作类型,也不要慌。重点看服务端代码里回调函数绑的是哪个动作类型,能用命令行工具查到就能跑通。
3. 把源码跑起来:工作空间、依赖与两条构建命令
3.1 先确认你的 ROS 版本,再谈编译
动作通信在 ROS 1 和 ROS 2 里都有,但实现差别很大,这是这套源码能不能跑起来的分水岭。ROS 1 用的是 actionlib 库,动作定义写在 .action 文件里,靠 actionlib_msgs 和 actionlib 两个包支撑;ROS 2 把动作做成了内置概念,用 rosidl 生成接口,客户端库从 actionlib 变成了 rclpy_action 或 rclcpp_action,回调命名和底层话题数量都不一样。所以你拿到 zip 后第一件事,是看代码里 import 的是 actionlib 还是 rclpy_action,这一步判断错了,后面所有命令都是白敲。
我见过不少人把 ROS 2 的源码塞进 ROS 1 工作空间,编译报错后第一反应是「代码写错了」,其实只是版本错配。常见做法是看包描述文件 package.xml 里对 actionlib_msgs 的依赖写法——ROS 1 直接写<depend>actionlib_msgs</depend>,ROS 2 通常还会带上action_msgs和rclpy_action。这一步花十秒就能定位,比折腾十分钟编译错误划算得多。
3.2 解压进工作空间:目录位置决定成败
确定版本匹配后,把 zip 解压到工作空间的 src 目录下。注意这里有个所有人都会踩的坑:不要在解压后直接在当前目录编译,更不要把 zip 解压出一个嵌套很多层的目录结构。ROS 的构建系统要求包描述文件 package.xml 位于 src 下的某个包的根目录,如果解压出来是src/某个课程目录/包名/package.xml还好,最怕的是src/包名-副本/学习笔记7/包名/package.xml,这种嵌套会让包发现机制直接失效。我一般先解压到 /tmp 看一眼顶层结构,确认是包根目录再挪进 src,省得反复搬家。
# 假设你的 ROS 2 工作空间叫 ros2_ws,先看包结构再放进去 cd /tmp unzip 学习笔记7源码-动作服务器与客户端.zip -d note7 ls note7/ # 期望看到包名目录,而不是又一个压缩包或 README 开头的文件 # 确认结构没问题之后: cp -r note7/包名 ~/ros2_ws/src/ cd ~/ros2_ws rosdep install --from-paths src --ignore-src -r -y colcon build --packages-select 包名 source install/setup.bash这段命令里最关键的是rosdep install。它负责把 package.xml 里声明的依赖全部装好,比如rclpy_action、action_msgs。很多动作源码编译失败不是代码问题,而是少了某一个消息库,rosdep 会把这一步补齐。--packages-select 包名只构建目标包,不要偷懒省掉,否则整个工作空间几十个包一起编译,报错信息会被埋在一大屏日志里,找不到重点。
3.3 跑通最小启动:先起服务端,再起客户端
编译通过只代表代码没有语法错误,通信逻辑有没有问题,要跑起来才知道。动作通信的两个节点有先后关系——客户端在最开始会尝试连接服务端,如果服务端没起来,客户端不会崩溃,但会一直打印等待连接的日志。所以验证顺序要固定:先启动服务端,等它打印出「动作服务器已就绪」之类的消息,再启动客户端。两个节点可以开两个终端,也可以直接用 launch 文件一起拉起来,但分开启动更容易观察每一边的日志。
# 终端 1:启动动作服务器 ros2 run 包名 action_server # 终端 2:确认动作服务已经注册 ros2 action list -t # 期望输出类似 /your_action 的类型名,这一步能证明服务端注册成功 # 终端 3:启动动作客户端 ros2 run 包名 action_clientros2 action list -t是排查动作通信最值钱的一条命令。它列出当前所有可用的动作服务和动作类型,如果服务端起来了但这条命令里看不到东西,问题基本出在服务端的类型注册上,而不是客户端。客户端启动后观察两边的日志:服务端应该打印出收到 goal 的请求,客户端应该周期性刷新 feedback,最后两边都打印 result,这条链路就算通了。
3.4 ROS 1 用户对照:catkin 构建路径
如果你的环境还是 ROS 1,构建命令换成 catkin 体系,依赖里确保有 actionlib。ROS 1 的动作源码里通常会有actionlib/server/simple_action_server.h这样的头文件包含,编译前先确认 actionlib 库装没装,没装的话跑一遍 apt 安装即可。启动后排查工具也换成rostopic list,能看到some_action/goal、some_action/feedback、some_action/result三组话题,就说明通信链路已经建立。ROS 1 的取消话题是独立的,ROS 2 里取消了专用取消话题,改由客户端库直接发取消请求,这个底层差异不用改业务代码,但理解日志时要注意。
4. 按自己的需求改:服务端 goal 校验与客户端反馈接收
4.1 服务端骨架:回调函数是动作服务器的命脉
跑通自带源码只是第一步。做实际项目时,你几乎一定会改动作接口和回调逻辑,比如想换成「去目标点导航」「夹爪开合」这种你自己的任务。改接口文件要重新编译,改回调逻辑则集中在三个函数里:接收到 goal 时调用、执行中周期发布 feedback、结束时回传 result。下面给一个 ROS 2 的 Python 服务端最小实现,结构可以当成模板套用。
# action_server_template.py import rclpy from rclpy.node import Node from rclpy.action import ActionServer from 你的包.action import YourTask # 你的自定义动作类型 class ActionServerTemplate(Node): def __init__(self): super().__init__('action_server_template') self._action_server = ActionServer( self, YourTask, 'your_action_name', execute_callback=self.execute_callback ) def execute_callback(self, goal_handle): # 1. 可以在这里做 goal 合法性校验 # 不合法时直接返回失败结果,别进执行逻辑 self.get_logger().info(f'收到目标: {goal_handle.request.need_value}') feedback_msg = YourTask.Feedback() feedback_msg.progress = 0.0 # 2. 模拟一个耗时任务,按进度发布反馈 for i in range(1, 11): # 如果客户端已取消,立即收尾退出 if goal_handle.is_cancel_requested: goal_handle.canceled() return YourTask.Result(success=False) feedback_msg.progress = float(i) / 10.0 goal_handle.publish_feedback(feedback_msg) self.get_clock().sleep_for(rclpy.duration.Duration(seconds=1)) # 3. 任务完成,标记成功并返回结果 goal_handle.succeed() return YourTask.Result(success=True, result_value='done') def main(): rclpy.init() node = ActionServerTemplate() rclpy.spin(node) rclpy.shutdown()这段代码里值得细看的是goal_handle.is_cancel_requested这个判断。动作服务器的取消不是自动的,客户端发取消请求后,服务端必须主动检查、调用canceled()并返回结果,任务才算真正终止。如果漏掉这个分支,取消请求会一直挂着,客户端永远等不到响应。校验 goal 的逻辑我习惯放在回调函数开头,条件不满足直接返回失败 Result,这比接受后再执行到一半返错要干净得多。
4.2 客户端骨架:反馈回调里不要做耗时操作
客户端端代码的套路更固定:先发目标,然后等反馈和结果。很多新手把反馈和结果都塞进一个回调里,结果被高频反馈打爆。正确的写法是给反馈单独一个回调,这个回调里只更新 UI 或日志;结果回调里做最终处理。
# action_client_template.py import rclpy from rclpy.node import Node from rclpy.action import ActionClient from 你的包.action import YourTask class ActionClientTemplate(Node): def __init__(self): super().__init__('action_client_template') self._client = ActionClient( self, YourTask, 'your_action_name' ) def send_goal(self): goal_msg = YourTask.Goal() goal_msg.need_value = 10 # wait_for_server 是连接状态检测的关键 if not self._client.wait_for_server(5.0): self.get_logger().error('服务端 5 秒未就绪,退出') return self._send_goal_future = self._client.send_goal_async( goal_msg, feedback_callback=self.feedback_callback ) self._send_goal_future.add_done_callback(self.goal_response_callback) def feedback_callback(self, feedback_msg): # 这里只做展示,不做计算,反馈频率可能很高 progress = feedback_msg.feedback.progress self.get_logger().info(f'进度: {progress:.0%}') def goal_response_callback(self, future): # 这个回调处理服务端是否接受目标 goal_handle = future.result() if not goal_handle.accepted: self.get_logger().info('目标被拒绝') return self.get_logger().info('目标被接受,等待结果...') self._get_result_future = goal_handle.get_result_async() self._get_result_future.add_done_callback(self.result_callback) def result_callback(self, future): result = future.result().result self.get_logger().info(f'执行结果: {result.result_value}') rclpy.shutdown() def main(): rclpy.init() node = ActionClientTemplate() node.send_goal() rclpy.spin(node)wait_for_server(5.0)这个调用值得重点说。它解决的是客户端比服务端先启动时连不上的问题,参数 5.0 表示最多等 5 秒,这里可以根据实际部署调整。反馈回调里我特意只放了一行日志——动作的 feedback 频率由服务端控制,如果服务端用高频循环发反馈,回调会被频繁触发,在里面做复杂计算会拖垮节点主循环,这是高频踩坑点。
4.3 改了接口之后:必须重新构建接口包
自定义动作类型时,还有一个周期性出现的麻烦:改了 .action 文件,但代码里总是看不到新字段,编译还提示找不到生成的头文件。原因几乎都是构建顺序错了。动作接口包必须先构建,依赖它的服务端和客户端包必须后构建,两个包放在同一个工作空间时,colcon build默认按依赖排序,但如果动作接口和节点在同一个包里,就要注意清理旧的生成文件。
# 修改 .action 文件后,重新构建必须带 --cmake-clean-cache cd ~/ros2_ws rm -rf build/接口包名 install/接口包名 colcon build --packages-select 接口包名 --cmake-clean-cache source install/setup.bash colcon build --packages-select 你的功能包--cmake-clean-cache是解决「改了接口但编译还是旧结构」的后悔药。动作接口文件会生成一大套 Python/C++ 消息类,CMake 缓存里残留旧配置时,新字段常常不生效。先删掉 build 和 install 里对应包的残留,再带这个参数重新构建,通常一次就好。别偷懒在构建时省略这一步,省下的时间会在排查诡异报错时加倍补回来。
5. 动作通信避坑手册:5 个高频翻车现场与排查顺序
5.1 现象:客户端一直提示「等待服务端连接」,但服务端明明在跑
原因:服务端注册的动作名字和客户端请求的动作名字不一致。动作名就像一个约定好的门牌号,服务端把动作注册在move_robot上,客户端却发到go_to_pose,两边永远对不上。这类问题用代码审查很难发现,因为服务端和客户端各自编译都通过,日志也正常。
解决:用ros2 action list -t查看当前所有已注册动作和类型,然后比对两端代码里的动作名字符串。顺手再检查一下是否真的source install/setup.bash——两个不同终端如果 source 了不同工作空间,也会出现这种「各自为政」的怪象。这个命令比读代码快得多,我一直把它排在排查首位。
5.2 现象:客户端发了 goal,服务端日志没有任何反应
原因:最常见的是服务端节点用的是多线程 executor,但回调里被一个长循环占住了,新 goal 的回调根本分不到执行权。第二个常见原因是客户端发的 goal 类型与服务端注册的类型不一致,比如服务端注册的是YourTask,客户端构造的是另一个结构类似的动作类型,编译不报错但运行静默失败。
解决:先看服务端消费端日志有没有打印「收到目标」;没打印就先在服务端终端里 Ctrl+C,用ros2 run重启,排除多线程占用的可能性。再看类型匹配,直接在日志里确认两边的动作类型完全一致,包括包名。分不清时就用整条ros2 action info /动作名查看。
5.3 现象:feedback 收不到,但最终 result 能收到
原因:这是入坑不久最容易疑惑的问题。反馈收不到,基本可以断定是服务端没有调用publish_feedback,或者把反馈回调放在了条件分支之外。还有一种情况是客户端构造send_goal_async时漏传了feedback_callback参数,此时反馈数据其实在传输,只是你的回调没有订阅。
解决:在服务端的execute_callback里加一行日志打印反馈发送,确认服务端确实发布了。然后检查客户端send_goal_async(goal_msg, feedback_callback=self.feedback_callback)是否绑定了回调。ROS 1 的SimpleActionClient如果漏传反馈回调,同样会静默丢弃,这个坑跨版本通用。
5.4 现象:客户端取消请求发出去了,但服务端任务还在跑
原因:服务端的执行回调里没有检查取消标志。动作的取消是基于协作的,客户端发取消请求只是通知服务端,服务端如果埋头执行循环、从不检查is_cancel_requested,这个取消就永远不会生效。这也是新手最容易把「取消」误解为「强杀」的地方。
解决:在耗时循环里定期检查取消标志,收到取消请求后清理资源、调用canceled()并返回结果。正确处理后,客户端能得到一个确认取消的反馈,任务才真正终止。实现时把检查频率放在循环的前面位置,保证每次迭代都能响应。
5.5 现象:所有代码都对,构建却报找不到动作类型的头文件
原因:动作接口包没有被先构建,或者接口包构建时没有通过 CMakeLists.txt 里的rosidl_generate_interfaces注册 .action 文件。接口定义和节点实现放一起时,这种情况尤其容易发生。
解决:按依赖顺序从底往上构建,接口包单独构建并确认生成的文件出现在 install 目录里。然后检查 CMakeLists.txt 里接口包是否引用了正确的 .action 文件路径。这一步的排查口诀是:接口变了就清缓存,依赖没构建就按顺序来,别在一次构建里试图解决所有问题。
6. 把取消机制用起来:机器人任务中最值钱的那个细节
动作三件套里,Goal 和 Result 容易理解,Feedback 直观,真正拉开设计和实现差距的,是取消。实际部署机器人任务时,一次取消动作做得好不好,直接决定系统在异常场景下是「优雅收尾」还是「失控」。我见过一个真实案例:某图像处理 Demo 里,机械臂正在执行抓取动作,操作员发现目标放歪了,按了取消,结果服务端还在继续执行完整个轨迹才停下来,因为执行回调里压根没有取消分支。这个教训值很多钱。
要想把取消做得好,在两个地方下功夫。第一,取消检查要放在执行循环的头部,而不是尾部——循环体很长时,放尾部意味着每次取消至少要多等一个完整周期。第二,取消后的清理动作要和取消标志一起处理,比如停止电机、释放夹爪、记录日志,这些收尾操作应该写在canceled()调用之前,确保任务状态机已经回到可控状态。一个常见做法是单独写一个cleanup()函数,在取消和异常返回时共用同一套收尾逻辑。
# 用命令行直接测试取消链路,不必每次改代码 ros2 action send_goal /your_action your_package/action/YourTask "{need_value: 20}" --feedback # 在反馈刷新的过程中,按 Ctrl+C 会触发取消请求 # 观察服务端日志是否打印了取消确认这条命令是验证动作服务器是否「真正支持取消」的最快路径。--feedback让它持续打印反馈,过程中按 Ctrl+C,服务端如果正确实现了取消分支,日志会显示收到了取消请求并终止任务;如果任务还在继续数秒,说明取消分支没生效,比写一整套测试代码高效得多。命令行验证通过之后,再去测试客户端程序里的取消调用,逻辑就顺了。
动作服务器与客户端这套源码,学到这会发现它给了两个能力:一是把 ROS 通信里最后一种原语用最小工程跑通了,二是给了你一套可对照的验收方法。动作接口设计、反馈频率控制、取消协作这些细节,都是在真正调试机器人时才会被逼着搞明白的。以后你再遇到带 action 的工程,先敲一遍ros2 action list,再看服务端有没有取消分支,代码骨架好坏心里就有数了。我也踩过取消分支漏写的坑,好在一次比一次记得牢。希望帮到你。
本文还有配套的精品资源,点击获取