
零基础学 ROS2很多人不是被概念劝退的而是被“第一步”劝退的装了好几天环境还没见到第一个节点跑起来跟着旧教程敲完代码却因为 API 变更报一堆错刚搞懂话题Topic是什么又被服务Service和动作Action搞得一头雾水。这篇文章会用一条主线把 ROS2 的六个核心概念串起来——环境搭建、工作空间、功能包、节点、话题、服务、动作——并且给你一套可以直接复制的代码和命令。读完你能做两件事一是听懂 ROS2 里各个模块到底在干什么二是在自己的电脑上跑通一个包含发布订阅、服务调用、动作执行的最小机器人程序。先说一个明确的判断ROS2 相比 ROS1最大的变化不是“多了几个命令”而是通信架构从“中心化”变成了“去中心化”的 DDS 模型。这意味着节点之间可以直接通信、可以跨机器部署也意味着你用ros2命令行工具看到的“节点图”比 ROS1 时代更容易理解。零基础阶段你不需要深挖 DDS 的每一个细节但一定要理解节点、话题、服务、动作这几个抽象概念分别解决什么问题。把这四条通信机制弄清楚后面学 Nav2、MoveIt、Gazebo 仿真都会顺畅很多。这篇文章适合这样的读者刚接触 ROS2、在 Ubuntu 上装环境反复失败、学过 ROS1 但想快速切换思维、以及准备用 ROS2 做毕业设计或机器人比赛却不知道从哪里下手的人。如果你已经在跑通了 TurtleSim并且自己写过发布订阅那这篇文章对你来说偏基础可以重点看服务通信和动作通信的部分。1. ROS2 到底是什么为什么它和 ROS1 不一样1.1 没有 ROS 时机器人程序是怎么写的先想象一个没有 ROS 的机器人项目你要同时处理激光雷达数据、控制电机、做路径规划、显示可视化界面。最朴素的做法是写一个超大程序里面把所有模块都串起来# 伪代码单体机器人程序 lidar_data read_lidar() pose estimate_pose(lidar_data) path plan_path(pose, target) cmd compute_speed(path) send_motor_cmd(cmd)看起来逻辑清晰但实际维护起来非常痛苦想换一个更快的路径规划算法你得把整个程序拆开想让导航在另一台电脑上运行你得自己写网络通信不同模块的开发人员要同时改一个文件冲突不断。ROS 把“机器人软件”拆成一个个可独立运行的节点Node节点之间通过定义好的接口通信于是每个模块都可以单独开发、单独测试、单独替换。1.2 ROS2 的 DDS 架构和 ROS1 有什么本质区别ROS1 里有一个“主节点”roscore负责管理所有节点之间的连接节点要先找到 roscore 才能互相通信。如果 roscore 挂了整个系统就瘫痪。ROS2 直接采用 DDSData Distribution Service数据分发服务作为底层通信中间件节点之间通过 DDS 的“域”发现彼此不再需要中心节点。这意味着多机部署变得自然只要在同一网络、同一ROS_DOMAIN_ID下不同电脑上的节点能直接通信。系统少了单点故障DDS 本身就支持可靠传输、QoS 策略等高级特性。实时性更好更接近工业级机器人的通信需求。核心概念对比概念ROS1 时代ROS2 时代中心节点必须有 roscore没有中心节点靠 DDS 自动发现通信中间件自定义的 TCPROS/UDPROSDDS默认 Fast DDS / Cyclone DDS构建工具catkincolconPython 支持主要是 Python 2官方主推 Python 3命令行前缀rostopic、rosserviceros2 topic、ros2 service从学习角度看ROS2 的命令行设计更统一。ros2 node list、ros2 topic list、ros2 service list都是同一套风格这对新手是很友好的。1.3 你真正要掌握的四个抽象概念ROS2 中绝大多数功能都围绕四个抽象概念展开节点Node一个可执行程序中的功能单元负责完成某一类工作。一个机器人系统由很多节点组成。话题Topic一种“发布/订阅”式的单向数据流。发布者往话题上发消息订阅者接收消息。适合激光雷达数据、图像、里程计等持续产生的数据。服务Service一种“请求/响应”式的同步通信。客户端发送请求服务端处理后返回响应。适合调用一次就能拿到结果的场景比如“打开摄像头”“获取地图”。动作Action一种带目标、反馈和结果的长时间任务通信。适合导航到某个点、机械臂抓取这类需要持续监控的任务。打个比方话题像广播电台你只管播谁爱听谁听服务像打电话问客服一问一答动作像请人搬家你先说目的地对方过程中持续告诉你进度最后告诉你搬完了。2. 环境搭建Ubuntu 上安装 ROS2 的完整步骤2.1 版本选择Humble 还是 JazzyROS2 的版本很多零基础最关心的是选哪个版本能少踩坑。这里有一个比较实用的原则选长期支持版LTS且和你系统的 Ubuntu 版本严格对应。目前最常见的组合是Ubuntu 22.04 LTS ROS2 Humble HawksbillROS2 第一个 LTS 版本资料最多Ubuntu 24.04 LTS ROS2 Jazzy Jalisco2024 年发布的 LTS适合新装系统的用户如果你的系统是 Ubuntu 22.04推荐直接安装 Humble如果是 Ubuntu 24.04就选 Jazzy。虽然 ROS2 也支持通过 Docker 等方式在 Windows/macOS 上运行但对零基础用户来说原生 Ubuntu 环境最不容易出问题。搜索相关信息时能看到很多“Ubuntu 24.04 安装 ROS2”的热门词说明新版本已经是趋势。但要注意Jazzy 的教程资料比 Humble 少部分第三方库的适配也有滞后。如果你完全零基础又不想折腾兼容问题Ubuntu 22.04 Humble 仍然是更稳妥的方案。2.2 安装准备软件源和系统依赖以下步骤以 Ubuntu 22.04 Humble 为例Jazzy 用户把版本代号换成jazzy即可。首先确保系统已安装software-properties-common并开启 universe 软件源sudo apt update sudo apt install -y software-properties-common sudo add-apt-repository universe然后添加 ROS2 GPG 密钥。官方源在某些网络环境下可能较慢国内用户可以在 ROS 官方镜像站点找到对应镜像配置。这里演示用官方源的操作然后需要把 ROS2 apt 仓库加入系统sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg echo deb [arch$(dpkg --print-architecture) signed-by/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release echo $UBUNTU_CODENAME) main | sudo tee /etc/apt/sources.list.d/ros2.list /dev/null然后更新索引并安装完整桌面版sudo apt update sudo apt install -y ros-humble-desktop这一步会把 ROS2 的核心库、通信中间件、可视化工具如 RViz2、仿真工具如 Gazebo 或 TurtleSim一起装上。如果你只想跑最基础的功能可以用ros-humble-ros-base来缩小安装体积但第一次学习直接装ros-humble-desktop更容易。2.3 环境变量配置source 一下才能用安装完成后打开一个新终端执行source /opt/ros/humble/setup.bash然后运行ros2 run turtlesim turtlesim_node如果能看到一个带小乌龟的窗口说明 ROS2 已经能正常工作了。ros2 run的意思是运行某个功能包里的节点后面会详细解释功能包和节点的关系。为了避免每次打开终端都要手动 source推荐把环境变量写入~/.bashrcecho source /opt/ros/humble/setup.bash ~/.bashrc source ~/.bashrc这里有一个常见误区很多人只 source 了/opt/ros/humble/setup.bash发现自己创建的工作空间里的功能包找不到。实际上你自己的工作空间也要单独 source。每次用colcon build构建完都要 source 一下install/setup.bash否则新功能包不会被识别。后面章节会专门讲。2.4 环境验证启动小乌龟再打开一个终端运行ros2 run turtlesim turtle_teleop_key此时你可以在终端里按方向键控制小乌龟移动。同时再打开一个终端运行ros2 node list你会看到两个节点名/turtlesim /teleop_turtle再用ros2 topic list你会看到类似/turtle1/cmd_vel、/turtle1/pose这样的话题。小乌龟示例看起来简单但它完整地展示了一个发布者节点teleop把键盘指令发布到话题上另一个订阅者节点turtlesim接收指令并控制小乌龟运动的过程。这是 ROS2 通信机制最直观的演示。3. 工作空间与功能包ROS2 代码的基本组织方式3.1 工作空间是“所有功能包的集合”在 ROS2 中工作空间Workspace是一个目录结构最常见的名字是ros2_ws。标准结构如下ros2_ws/ ├── src/ # 存放所有功能包源码 ├── build/ # 构建过程中的中间文件 ├── install/ # 构建完成后的安装文件需要 source 这个目录 └── log/ # 日志src是你真正写代码的地方。build和log你不用手动维护colcon build会自动生成。install目录里会有每个功能包的setup.bash或local_setup.bashsource 之后系统才能找到你写的功能包。创建并初始化工作空间mkdir -p ~/ros2_ws/src cd ~/ros2_ws colcon build source install/setup.bash这里的关键点是每次添加新的功能包后都要重新执行colcon build否则新功能包不会出现在系统中。3.2 功能包是 ROS2 的“最小软件单元”功能包Package是 ROS2 中组织代码、配置、 launch 文件、消息定义等资源的最小单元。一个功能包可以理解为“一个模块”、一个“库”或者一个“可运行的程序”。功能包内部至少要有一个package.xml和一个构建配置文件CMakeLists.txt 或 setup.py。创建功能包的命令cd ~/ros2_ws/src ros2 pkg create my_pkg --build-type ament_python --node-name my_node参数说明--build-type ament_python表示用 Python 编写功能包。ROS2 支持ament_python和ament_cmake两种主流构建类型前者适合 Python 开发后者适合 C 开发。--node-name my_node自动创建一个最简单的节点文件。创建后的目录结构大致如下my_pkg/ ├── my_pkg/ │ ├── __init__.py │ └── my_node.py ├── resource/ ├── test/ ├── package.xml ├── setup.py └── setup.cfg3.3 如何写一个最小节点打开my_pkg/my_pkg/my_node.py改成如下内容#!/usr/bin/env python3 import rclpy from rclpy.node import Node class MyNode(Node): def __init__(self): super().__init__(my_node_name) self.get_logger().info(Hello ROS2!) def main(argsNone): rclpy.init(argsargs) node MyNode() node.destroy_node() rclpy.shutdown() if __name__ __main__: main()修改setup.py确保入口函数配置正确entry_points{ console_scripts: [ my_node my_pkg.my_node:main, ], },编译并运行cd ~/ros2_ws colcon build --packages-select my_pkg source install/setup.bash ros2 run my_pkg my_node终端会输出[INFO] [my_node_name]: Hello ROS2!这里的逻辑关系要理清my_pkg是功能包名my_node是你在命令行执行时的程序名my_node_name是节点在 ROS2 图中的名字。一个功能包可以包含多个节点一个节点程序可以创建多个 Node 对象初学者最容易混淆的就是这三个名字。4. 话题通信机器人系统里的“广播电台”4.1 话题通信的核心概念话题Topic是 ROS2 中最常用、最重要的通信方式。它的特点是单向数据从发布者Publisher流向订阅者Subscriber不需要对方回应。异步发布者不管订阅者是否存在只管发布订阅者只需要在消息到来时处理。多对多一个话题可以有多个发布者和多个订阅者。消息类型话题传输的数据必须符合某种消息类型Message Type比如std_msgs/msg/String、geometry_msgs/msg/Twist。实际项目中话题通信适合传感器数据流、状态反馈、日志消息等场景。例如激光雷达节点不断发布sensor_msgs/msg/LaserScan导航节点订阅这些数据做避障。4.2 为什么零基础要先学会话题从零基础的角度看话题通信几乎覆盖了 ROS2 编程的一半思维模式创建节点、发布数据、订阅数据、处理回调。搞懂话题后其他通信方式只是在“话题”基础上改变了交互方式。学习路线应该先掌握话题再学服务最后学动作。4.3 一个完整的话题发布订阅示例接下来我们写一个完整的 Python 示例包含发布者和订阅者两个节点。这里使用最简单的std_msgs/msg/String消息类型。发布者节点文件路径~/ros2_ws/src/my_pkg/my_pkg/talker.py#!/usr/bin/env python3 import rclpy from rclpy.node import Node from std_msgs.msg import String class Talker(Node): def __init__(self): super().__init__(talker) self.publisher_ self.create_publisher(String, chatter, 10) self.timer self.create_timer(0.5, self.timer_callback) self.count 0 def timer_callback(self): msg String() msg.data fHello ROS2: {self.count} self.publisher_.publish(msg) self.get_logger().info(fPublishing: {msg.data}) self.count 1 def main(argsNone): rclpy.init(argsargs) node Talker() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ __main__: main()发布者核心逻辑create_publisher(String, chatter, 10)创建了一个发布者消息类型为String话题名为chatter队列深度为 10。create_timer(0.5, ...)让节点每 0.5 秒回调一次往话题上发布消息。rclpy.spin(node)让节点保持运行状态不断处理定时器回调和订阅回调。订阅者节点文件路径~/ros2_ws/src/my_pkg/my_pkg/listener.py#!/usr/bin/env python3 import rclpy from rclpy.node import Node from std_msgs.msg import String class Listener(Node): def __init__(self): super().__init__(listener) self.subscription self.create_subscription( String, chatter, self.listener_callback, 10 ) def listener_callback(self, msg): self.get_logger().info(fI heard: {msg.data}) def main(argsNone): rclpy.init(argsargs) node Listener() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ __main__: main()订阅者核心逻辑create_subscription(String, chatter, callback, 10)订阅话题chatter消息到达后自动调用listener_callback。和发布者不同订阅者不需要定时器它一直“挂”在话题上等待消息。修改 setup.py把两个节点都注册到入口entry_points{ console_scripts: [ my_node my_pkg.my_node:main, talker my_pkg.talker:main, listener my_pkg.listener:main, ], },编译运行cd ~/ros2_ws colcon build --packages-select my_pkg source install/setup.bash开三个终端分别运行ros2 run my_pkg talker ros2 run my_pkg listener ros2 topic echo /chatter运行talker的终端会不断输出正在发布的消息运行listener的终端会不断输出听到的消息topic echo则直接把话题上的原始消息打印出来。三端数据一致说明话题通信已经跑通。查看节点和话题信息ros2 node list ros2 topic list ros2 topic info /chatter ros2 topic hz /chatterros2 topic hz可以查看消息发布频率这是排查“话题有没有数据”最常用的命令。4.4 话题通信的常见误区一个很常见的误区是发布者和订阅者节点必须同时启动才能通信。实际不是这样的。ROS2 话题通信是异步的发布者先启动、订阅者后启动订阅者依然能接收到后续消息。反过来也一样。这背后的原因是 DDS 会在节点启动时自动发现对方。另一个误区是把 QoS 当成可以随便忽略的配置。如果发布者和订阅者的 QoS 策略不兼容话题虽然显示存在但订阅者可能永远收不到数据。零基础阶段先使用默认 QoS 即可遇到“收不到消息”的问题时第一反应应该是检查 QoS 是否匹配、话题名是否拼错、消息类型是否一致。5. 服务通信一问一答的“请求—响应”模式5.1 什么时候需要服务通信话题通信解决的是“持续不断的单向数据流”。但有些任务需要“你问我答”客户端发请求服务端处理完返回结果。比如你想让机器人“拍一张照片”“设置一个目标点”“查询当前地图尺寸”。如果用话题来做你得自己设计一套请求消息和响应消息还要处理时序问题非常繁琐。ROS2 的服务Service机制专门解决这类同步调用问题。服务通信和话题通信的关键区别特性话题 Topic服务 Service方向单向流请求/响应同步性异步同步客户端等待响应典型场景传感器数据、控制指令查询、配置、触发一次性操作消息类型MsgSrv包含 Request 和 Response 两部分5.2 服务通信接口消息类型长什么样一个服务类型由三部分组成请求消息、响应消息以及一个固定的服务名。ROS2 内置了很多标准服务接口比如std_srvs/srv/SetBool、std_srvs/srv/Trigger。用Trigger来举例它请求为空响应为bool success string message我们可以直接使用它来做一个简单的服务示例不需要自己定义接口。5.3 一个完整的服务端与客户端示例这个例子模拟一个“做加法”的服务客户端发送两个整数服务端计算和并返回。服务端节点文件路径~/ros2_ws/src/my_pkg/my_pkg/add_two_ints_server.py#!/usr/bin/env python3 import rclpy from rclpy.node import Node from example_interfaces.srv import AddTwoInts class AddTwoIntsServer(Node): def __init__(self): super().__init__(add_two_ints_server) self.srv self.create_service(AddTwoInts, add_two_ints, self.add_two_ints_callback) def add_two_ints_callback(self, request, response): response.sum request.a request.b self.get_logger().info(fIncoming request: a{request.a}, b{request.b}) return response def main(argsNone): rclpy.init(argsargs) node AddTwoIntsServer() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ __main__: main()服务端逻辑create_service(AddTwoInts, add_two_ints, callback)创建了一个名为add_two_ints的服务。回调函数接收request处理完后必须返回response对象。客户端节点文件路径~/ros2_ws/src/my_pkg/my_pkg/add_two_ints_client.py#!/usr/bin/env python3 import rclpy from rclpy.node import Node from example_interfaces.srv import AddTwoInts class AddTwoIntsClient(Node): def __init__(self): super().__init__(add_two_ints_client) self.cli self.create_client(AddTwoInts, add_two_ints) while not self.cli.wait_for_service(timeout_sec1.0): self.get_logger().info(Service not available, waiting again...) self.req AddTwoInts.Request() self.req.a 3 self.req.b 5 def send_request(self): self.future self.cli.call_async(self.req) rclpy.spin_until_future_complete(self, self.future) result self.future.result() self.get_logger().info(fResult: {self.req.a} {self.req.b} {result.sum}) def main(argsNone): rclpy.init(argsargs) node AddTwoIntsClient() node.send_request() node.destroy_node() rclpy.shutdown() if __name__ __main__: main()客户端逻辑wait_for_service会阻塞等待服务端上线避免请求找不到服务。使用call_async异步调用服务配合spin_until_future_complete等待响应。Python 客户端常用异步调用方式比同步调用更安全不会卡死节点。注册入口并运行修改setup.pyentry_points{ console_scripts: [ my_node my_pkg.my_node:main, talker my_pkg.talker:main, listener my_pkg.listener:main, add_two_ints_server my_pkg.add_two_ints_server:main, add_two_ints_client my_pkg.add_two_ints_client:main, ], },编译cd ~/ros2_ws colcon build --packages-select my_pkg source install/setup.bash先启动服务端ros2 run my_pkg add_two_ints_server再启动客户端ros2 run my_pkg add_two_ints_client预期输出[INFO] [add_two_ints_client]: Result: 3 5 8同时服务端终端会显示收到了请求。也可以直接用命令行调用服务测试ros2 service list ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts {a: 10, b: 20}如果一切正常终端会直接返回response: example_interfaces.srv.AddTwoInts_Response(sum30)这里要注意服务通信中客户端如果先启动而服务端还没就绪调用会失败。所以代码里必须有wait_for_service这样的等待逻辑生产项目中更推荐在回调里处理重试和超时。6. 动作通信能反馈进度、能取消请求的“长任务”6.1 从 Service 到 Action解决了什么问题服务通信适合一次调用、立即返回结果的场景。但机器人世界里大量任务不是这样的让机器人导航到某个点位需要持续几十秒甚至几分钟让机械臂抓取一个物体过程包括移动、夹取、确认。这类任务有几个共同点耗时长需要持续监控状态。执行过程有中间反馈比如“已经走了一半路”。用户可以取消比如发现目标点错了立刻发一个取消请求。如果强行用服务通信客户端要一直阻塞等待服务端返回期间无法接收过程状态。动作Action就是专门为这类任务设计的通信模式。它的核心组件包括目标Goal客户端告诉服务端要完成什么。反馈Feedback服务端在执行过程中持续向客户端报告进度。结果Result任务完成或失败后服务端返回最终结果。取消Cancel客户端可以随时发送取消请求。6.2 动作通信的接口结构一个动作类型由三部分组成通常以.action文件定义。ROS2 内置了一个经典的示例动作接口example_interfaces/action/Fibonacciint32 order --- int32[] sequence --- int32[] partial_sequence三部分分别对应 Goal、Result、Feedback。这个接口非常简单我们直接用它来做示例。6.3 一个完整的动作服务端与客户端示例动作通信代码量较大但核心逻辑比服务通信清晰。我们用 Python 实现一个斐波那契数列计算动作客户端发送一个数字服务端持续反馈计算进度最后返回完整序列。动作服务端节点文件路径~/ros2_ws/src/my_pkg/my_pkg/fibonacci_action_server.py#!/usr/bin/env python3 import rclpy from rclpy.node import Node from rclpy.action import ActionServer from example_interfaces.action import Fibonacci class FibonacciActionServer(Node): def __init__(self): super().__init__(fibonacci_action_server) self._action_server ActionServer( self, Fibonacci, fibonacci, self.execute_callback ) self._feedback Fibonacci.Feedback() async def execute_callback(self, goal_handle): self.get_logger().info(fExecuting goal, order: {goal_handle.request.order}) sequence [0, 1] for i in range(1, goal_handle.request.order): # 更新反馈当前计算的序列 self._feedback.partial_sequence sequence goal_handle.publish_feedback(self._feedback) # 计算下一个斐波那契数 sequence.append(sequence[i] sequence[i - 1]) # 如果收到取消请求提前终止 if goal_handle.is_cancel_requested: goal_handle.canceled() self.get_logger().info(Goal canceled) return Fibonacci.Result() goal_handle.succeed() result Fibonacci.Result() result.sequence sequence self.get_logger().info(fGoal succeeded, sequence: {sequence}) return result def main(argsNone): rclpy.init(argsargs) node FibonacciActionServer() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ __main__: main()execute_callback是一个异步回调可以在内部持续处理。publish_feedback把当前进度发给客户端。goal_handle.is_cancel_requested用于检查客户端是否发起了取消请求。动作客户端节点文件路径~/ros2_ws/src/my_pkg/my_pkg/fibonacci_action_client.py#!/usr/bin/env python3 import rclpy from rclpy.node import Node from rclpy.action import ActionClient from example_interfaces.action import Fibonacci class FibonacciActionClient(Node): def __init__(self): super().__init__(fibonacci_action_client) self._action_client ActionClient(self, Fibonacci, fibonacci) def send_goal(self, order): goal_msg Fibonacci.Goal() goal_msg.order order self._action_client.wait_for_server() self._send_goal_future self._action_client.send_goal_async( goal_msg, feedback_callbackself.feedback_callback ) self._send_goal_future.add_done_callback(self.goal_response_callback) def feedback_callback(self, feedback_msg): feedback feedback_msg.feedback self.get_logger().info(fReceived feedback: {feedback.partial_sequence}) def goal_response_callback(self, future): goal_handle future.result() if not goal_handle.accepted: self.get_logger().info(Goal rejected) return self.get_logger().info(Goal accepted) self._get_result_future goal_handle.get_result_async() self._get_result_future.add_done_callback(self.get_result_callback) def get_result_callback(self, future): result future.result().result self.get_logger().info(fResult: {result.sequence}) rclpy.shutdown() def main(argsNone): rclpy.init(argsargs) node FibonacciActionClient() node.send_goal(10) rclpy.spin(node) if __name__ __main__: main()send_goal_async异步发送目标并传入feedback_callback实时接收反馈。goal_response_callback判断服务端是否接受了目标。get_result_async获取最终结果。注册入口并运行修改setup.py加入两个入口entry_points{ console_scripts: [ my_node my_pkg.my_node:main, talker my_pkg.talker:main, listener my_pkg.listener:main, add_two_ints_server my_pkg.add_two_ints_server:main, add_two_ints_client my_pkg.add_two_ints_client:main, fibonacci_action_server my_pkg.fibonacci_action_server:main, fibonacci_action_client my_pkg.fibonacci_action_client:main, ], },编译并启动服务端cd ~/ros2_ws colcon build --packages-select my_pkg source install/setup.bash ros2 run my_pkg fibonacci_action_server再启动客户端ros2 run my_pkg fibonacci_action_client客户端终端会输出[INFO] [fibonacci_action_client]: Received feedback: [0, 1] [INFO] [fibonacci_action_client]: Received feedback: [0, 1, 1] ... [INFO] [fibonacci_action_client]: Goal accepted [INFO] [fibonacci_action_client]: Result: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]在客户端运行过程中另开一个终端可以查看动作列表ros2 action list ros2 action info /fibonacci如果服务端在处理长时间任务时收到取消请求客户端可以调用goal_handle.cancel_goal()服务端会执行canceled()分支。6.4 动作通信和服务的选型建议进入实际项目时很多人会纠结“这个功能到底用服务还是动作”。这里给一个比较实用的判断标准调用后结果马上能拿到比如查询参数、切换模式用 Service。调用后需要执行一个过程且过程中需要给用户反馈或者需要支持取消用 Action。如果任务是持续不断的数据流比如发布速度指令、发布激光扫描数据用 Topic。如果任务执行时间不确定可能需要几分钟甚至几小时Action 是更稳妥的选择因为它天然支持状态查询、超时取消和反馈机制。从学习优先级来看Topic 必须精通Service 必须掌握Action 至少要能看懂代码结构并能跑通示例。7. 运行结果与效果验证如何判断系统真的通了7.1 验证话题通信话题通信是否正常用四个命令就能判断ros2 node list ros2 topic list ros2 topic info /chatter ros2 topic hz /chatter其中ros2 topic hz /chatter会周期性打印消息频率如果能看到类似average rate: 2.000的输出说明发布订阅链路完全通畅。如果topic info只显示发布者而没有订阅者说明订阅者没有成功连接应优先检查 QoS 和节点是否真的在运行。7.2 验证服务通信用命令行直接调用是最快的验证方式ros2 service list ros2 service type /add_two_ints ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts {a: 10, b: 20}如果返回了sum30说明服务端逻辑没有问题客户端调用失败大概率是服务未启动或请求格式不对。7.3 验证动作通信动作通信的验证重点是反馈链路ros2 action list ros2 action type /fibonacci然后在客户端终端观察是否能看到中间反馈和最终结果。由于动作通信涉及异步回调一个很实用的排查思路是先启动服务端用命令行工具确认动作服务器已注册再运行客户端观察日志是否在goal_response_callback处收到了Goal accepted。如果没有收到基本可以确定是动作类型不匹配或服务端没有执行rclpy.spin。7.4 如何快速定位失败原因大多数 ROS2 入门示例跑不通原因集中在五个地方没有 source 工作空间只 source 了 ROS2 基础环境没有 source 当前工作空间的install/setup.bash系统找不到你的自定义功能包。功能包名和节点名混淆ros2 run my_pkg my_node中第一个参数是功能包名第二个参数是 setup.py 里定义的入口名不是 Python 文件名。消息类型不匹配发布者用的是std_msgs/msg/String订阅者的回调里却想按自定义消息解析。检查ros2 interface show 类型。服务端未启动客户端启动后没有wait_for_service直接调用导致失败。RMW 实现不一致同一台机器上不同终端用了不同的 DDS 实现比如有的 thread 环境变量指向 Fast DDS有的指向 Cyclone DDS会导致节点互相发现不了。正常安装桌面版一般不会出现这个问题但如果用了 Docker 或自定义安装方式要检查RMW_IMPLEMENTATION环境变量。8. 常见问题与排查方法整理一份 ROS2 新手最常遇到的问题表问题现象可能原因排查方式解决方案ros2: command not found环境变量未配置执行source /opt/ros/humble/setup.bash后重试写入~/.bashrcPackage my_pkg not found没有 build 或没有 source installcolcon build后执行source install/setup.bash每次新开终端都要 source发布者和订阅者互相发现不了DDS 实现不同或没有在同一网络域用ros2 node list和ros2 topic list检查统一RMW_IMPLEMENTATION检查ROS_DOMAIN_ID话题能看到但收不到消息QoS 策略不匹配用ros2 topic info -v查看发布者和订阅者 QoS修改 QoS 为兼容策略colcon build报变量未定义缺少依赖一般是colcon-common-extensions安装sudo apt install python3-colcon-common-extensions安装对应开发工具服务调用报 “failed to call service”服务端未启动或服务名错误ros2 service list检查服务是否存在启动服务端检查服务名拼写动作客户端反馈无法到达回调函数未注册或节点没有 spin检查send_goal_async是否传入feedback_callback确保rclpy.spin(node)在运行from std_msgs.msg import String报错Python 环境的 ROS2 未 sourcesource 后再启动 Python检查当前终端是否 source 过环境一个比较实用的排查习惯是先看节点列表再看话题/服务/动作列表最后用 echo/call 直接验证数据链路。大多数问题都能在这一条链路上定位出来。9. 最佳实践与工程建议9.1 工作空间管理每台机器只保留一个主力工作空间很多人的~/ros2_ws越来越大功能包越来越多最后自己都分不清哪些依赖哪些。建议的做法是每个项目一个独立工作空间工作空间内部再按功能拆功能包。例如robot_ws/ ├── src/ │ ├── robot_bringup/ # 启动文件 │ ├── robot_core/ # 核心节点 │ ├── robot_msgs/ # 自定义消息 │ └── robot_ui/ # 可视化或交互这样做的好处是colcon build时只构建当前项目相关的包依赖关系清晰source install/setup.bash后也不会出现多个同名功能包冲突。9.2 命名规范节点名、话题名、服务名要有可读性TurtleSim 示例里的/turtle1/cmd_vel只是教学用途。实际项目中话题名建议采用“模块/数据类型”或“模块/语义动作”的格式比如/lidar/scan、/nav/goal、/arm/status。功能包名按“组织/模块”风格命名如my_robot_navigation避免使用test、demo、new这类含义不明的名字。9.3 节点生命周期管理不要把逻辑全塞进 main 函数从上面的示例可以看到ROS2 Python 节点的标准结构是“继承 Node 类在类里初始化组件在 main 中创建对象并 spin”。这不仅仅是为了写起来规范更重要的是后续可以通过生命周期管理Lifecycle Node控制节点的配置、激活、取消激活状态。零基础阶段至少要做到把发布者、订阅者、定时器、回调全部封装在类里不要在 main 里写业务逻辑。9.4 日志是真正的查错工具ROS2 节点的日志函数get_logger().info()不只是打印一行字。在实际项目中你应该在关键节点加上日志并且按级别区分info正常流程中的状态如“收到请求”“成功发布”。warn潜在问题如“服务暂时不可用正在重试”。error必须处理的问题如“传感器数据为空”。fatal无法恢复的异常。如果日志太多影响性能可以在启动命令中临时禁用某个节点的 debug 输出或者用ros2 launch的日志级别参数控制。初学者最容易忽略的就是这个习惯——出了错只在代码里加 print而不是去想想日志级别和上下文。9.5 多机部署时注意 ROS_DOMAIN_IDROS2 默认使用ROS_DOMAIN_ID0。如果同一局域网里有多个机器人项目同时运行节点会互相干扰。建议每个项目或每台机器人设置不同的ROS_DOMAIN_IDexport ROS_DOMAIN_ID1这个环境变量要保证所有相关节点的终端都一致。多机通信时同一ROS_DOMAIN_ID下的节点需要在同一局域网内并且最好开启多播通信。这也是 ROS2 与 ROS1 在多机支持上的最大区别之一。10. 下一步学习建议如果你完整跑通了这一篇的示例下一步可以从三个方向继续深入一是自定义消息接口。现在你用的都是std_msgs、example_interfaces等标准接口真实项目一定会定义自己的消息Message、服务Service和动作Action。建议学习如何在功能包里创建.msg、.srv、.action文件并理解 C 和 Python 中如何调用同一份接口。二是启动文件Launch。单个节点用ros2 run多个节点就要用ros2 launch。Launch 文件是 ROS2 项目组织运行入口的关键几乎所有的真实项目都以 launch 文件作为启动入口里面定义了节点参数、namespace、重映射规则。三是仿真与可视化。把 TurtleSim 换成 Gazebo 仿真环境再配合 RViz2 观察机器人模型和传感器数据。到这一步你才真正开始触碰机器人开发的业务逻辑。ROS2 的学习曲线不是线性的它更像是在一个“节点、话题、服务、动作”四象限里反复打转。每当你觉得概念都懂了真正写一个包含导航、避障、机械臂控制的项目时又会重新理解一遍。这篇文章提供的是一个最小闭环从安装环境到跑通三种通信方式之后无论你转向 Nav2、MoveIt 还是自己写控制算法核心的通信思维都不会变。建议把示例代码留在本地遇到问题再回来对照排查。