ROS 2 简介¶
1. 什么是 ROS 2?为什么需要它?¶
ROS 1(Robot Operating System 1,机器人操作系统 1)是机器人软件开发的开创性框架,但随着机器人产业的成熟,其若干根本性局限逐渐暴露:
- 不支持实时控制:ROS 1 的 TCP/UDP 传输层并非为实时或安全关键系统设计。
- 单主架构(Single-master):中心化的
roscore是单点故障。 - 安全性差:没有内置的身份验证、加密或访问控制。
- 多机器人支持不足:运行多个机器人需要复杂的命名空间(namespace)变通方案。
- 跨平台能力弱:主要仅支持 Linux。
- Python 2 遗留问题:ROS Noetic 最终迁移到了 Python 3,但过渡过程缓慢。
ROS 2 从零开始设计以解决这些问题。它采用 DDS(Data Distribution Service,数据分发服务) 中间件标准作为通信骨干——这是一种经过验证的发布/订阅协议,广泛应用于航空航天、国防和金融系统。
ROS 2 的核心设计目标:
| 目标 | ROS 2 如何实现 |
|---|---|
| 实时控制 | DDS 支持实时 QoS(Quality of Service,服务质量)策略 |
| 多机器人 | 去中心化发现机制;无需单一主节点 |
| 安全性 | DDS-Security(SROS2)提供身份验证和加密 |
| 嵌入式系统 | Micro-ROS 面向微控制器 |
| 生产就绪 | 为商业部署设计,而非仅限研究 |
| 跨平台 | Linux、macOS、Windows 同等支持 |
2. ROS 1 与 ROS 2 对比¶
| 特性 | ROS 1 (Noetic) | ROS 2 (Humble+) |
|---|---|---|
| 中间件 | 自定义 TCP/UDP(roscomm) | DDS(Data Distribution Service) |
| 发现机制 | 中心化 roscore(单主节点) |
去中心化(DDS 发现) |
| 实时性 | 不支持 | 通过 DDS QoS 策略支持 |
| 安全性 | 无内置安全 | DDS-Security(SROS2) |
| 多机器人 | 手动命名空间方案 | 原生支持 |
| 构建系统 | catkin | colcon + ament(CMake / Python setuptools) |
| 启动文件 | XML(.launch) |
Python(.launch.py) |
| 语言支持 | C++(roscpp)、Python(rospy) | C++(rclcpp)、Python(rclpy) |
| 参数 | 全局参数服务器 | 每个节点独立参数 |
| 动作服务器 | actionlib |
rclpy/rclcpp 内置 Action |
| 生命周期管理 | 无 | 托管节点生命周期(LifecycleNode) |
| 消息 IDL | .msg 文件 |
相同的 .msg + .srv + .action 文件 |
| 平台 | Linux(主要) | Linux、macOS、Windows |
| DDS 实现 | 无 | Fast DDS、Cyclone DDS、Connext 等 |
3. ROS 2 架构¶
ROS 2 采用分层架构,通过 RMW(ROS Middleware Interface,ROS 中间件接口)抽象中间件层。
+-------------------------------------------------------+
| 用户应用程序 |
| (你的 ROS 2 节点、启动文件) |
+-------------------------------------------------------+
| RCL(ROS 客户端库) |
| rclcpp(C++) / rclpy(Python) |
+-------------------------------------------------------+
| RMW(ROS 中间件接口) |
| rmw_fastrtps / rmw_cyclonedds / rmw_connext |
+-------------------------------------------------------+
| DDS 实现层 |
| eProsima Fast DDS / Eclipse Cyclone DDS |
+-------------------------------------------------------+
| 操作系统网络栈(UDP/IP) |
+-------------------------------------------------------+
各层说明:
- 应用层:你的节点(Node)、话题(Topic)、服务(Service)、动作(Action)。
- RCL(ROS Client Library,ROS 客户端库):语言特定的 API。
rclcpp用于 C++,rclpy用于 Python。提供熟悉的 ROS API(Node、Publisher、Subscriber等)。 - RMW(ROS Middleware Interface,ROS 中间件接口):抽象层,允许在不修改应用代码的情况下切换 DDS 实现。可选择
rmw_fastrtps(默认)、rmw_cyclonedds或其他实现。 - DDS 层:处理实际的发布/订阅传输、发现、序列化、QoS 以及(可选的)安全性。
DDS 服务质量(QoS)策略¶
QoS 策略让你可以调整通信行为:
+---------------------+------------------------------------------+
| 策略 | 选项 |
+---------------------+------------------------------------------+
| 可靠性 Reliability | RELIABLE(可靠) / BEST_EFFORT(尽力而为) |
| 持久性 Durability | TRANSIENT_LOCAL / VOLATILE |
| 历史记录 History | KEEP_LAST(n) / KEEP_ALL |
| 截止时间 Deadline | (周期) |
| 生命周期 Lifespan | (持续时间) |
| 活跃性 Liveliness | AUTOMATIC / MANUAL_BY_TOPIC |
+---------------------+------------------------------------------+
4. 核心概念¶
4.1 节点(Node)¶
**节点**是计算的基本单元。每个节点执行一个明确定义的功能(如相机驱动、路径规划器)。
import rclpy
from rclpy.node import Node
class MyNode(Node):
def __init__(self):
super().__init__('my_node')
self.get_logger().info('Hello from ROS 2!')
4.2 话题(Topic)¶
**话题**是用于异步发布/订阅通信的命名总线。多个发布者和订阅者可以连接到同一个话题。
+----------+ /camera/image +------------+
| 发布者 | -------> Topic -------> | 订阅者 |
| (camera) | | (detector) |
+----------+ +------------+
|
+------------+
| 订阅者 |
| (display) |
+------------+
4.3 服务(Service)¶
**服务**是同步的请求/响应通信(类似 RPC)。客户端发送请求并阻塞直到收到响应。
+----------+ request +----------+ response
| 客户端 | ------------> | 服务端 | ----------->
+----------+ +----------+
4.4 动作(Action)¶
**动作**结合了目标(goal)、反馈(feedback)和结果(result)。适用于需要周期性进度更新的长时间运行任务(如"导航到航点")。
+----------+ goal +----------+ feedback(周期性)
| 客户端 | --------> | 服务端 | ------------------->
+----------+ +----------+
^ |
| result(结果) |
+----------------------+
4.5 参数(Parameter)¶
**参数**是节点级别的配置值(整数、字符串、布尔值等),可在运行时查询或设置。
4.6 启动(Launch)¶
**启动文件**用于同时启动多个节点,支持可配置的参数、话题重映射和条件判断。ROS 2 使用基于 Python 的启动文件(.launch.py)。
5. ROS 2 命令行工具 vs ROS 1¶
| 任务 | ROS 1 | ROS 2 |
|---|---|---|
| 运行节点 | rosrun pkg node |
ros2 run pkg node |
| 列出节点 | rosnode list |
ros2 node list |
| 列出话题 | rostopic list |
ros2 topic list |
| 回显话题 | rostopic echo /topic |
ros2 topic echo /topic |
| 发布话题 | rostopic pub /topic ... |
ros2 topic pub /topic ... |
| 查看消息类型 | rosmsg show MsgType |
ros2 interface show MsgType |
| 调用服务 | rosservice call /srv ... |
ros2 service call /srv ... |
| 列出参数 | rosparam list |
ros2 param list |
| 获取参数 | rosparam get /p |
ros2 param get /node p |
| 列出功能包 | rospack list |
ros2 pkg list |
| 创建功能包 | catkin_create_pkg |
ros2 pkg create --build-type ament_python |
| 构建工作空间 | catkin_make |
colcon build |
| 启动文件 | roslaunch pkg file.launch |
ros2 launch pkg file.launch.py |
| 录制 bag | rosbag record -a |
ros2 bag record -a |
| 回放 bag | rosbag play file.bag |
ros2 bag play file/ |
6. ROS 2 发行版¶
| 发行版 | Ubuntu 版本 | 停止支持日期 | 说明 |
|---|---|---|---|
| Humble Hawksbill | 22.04 | 2027 年 5 月 | LTS(长期支持);截至 2025 年使用最广泛 |
| Iron Irwini | 22.04 | 2024 年 11 月 | 非 LTS;已停止支持 |
| Jazzy Jalisco | 24.04 | 2029 年 5 月 | LTS;最新稳定版 |
| Rolling | 最新版 | 无 | 持续交付;始终保持最新 |
建议:生产环境使用 Humble(Ubuntu 22.04)或 Jazzy(Ubuntu 24.04)。如需最新功能且能接受偶尔的兼容性问题,可使用 Rolling。
7. 用 ROS 2 Python 编写简单的发布者/订阅者¶
7.1 创建功能包¶
# 加载 ROS 2 环境
source /opt/ros/humble/setup.bash
# 创建工作空间
mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
# 创建 Python 功能包
ros2 pkg create --build-type ament_python my_pubsub --dependencies rclpy std_msgs
7.2 发布者(Publisher)(my_publisher.py)¶
创建文件 my_pubsub/my_pubsub/my_publisher.py:
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class MinimalPublisher(Node):
"""一个最小的 ROS 2 发布者,发送 'Hello World' 消息。"""
def __init__(self):
super().__init__('minimal_publisher')
# 在 'chatter' 话题上创建发布者,队列大小为 10
self.publisher_ = self.create_publisher(String, 'chatter', 10)
# 创建定时器,每 0.5 秒调用一次 publish_callback
self.timer = self.create_timer(0.5, self.publish_callback)
self.count = 0
def publish_callback(self):
msg = String()
msg.data = f'Hello World: {self.count}'
self.publisher_.publish(msg)
self.get_logger().info(f'Publishing: "{msg.data}"')
self.count += 1
def main(args=None):
rclpy.init(args=args)
node = MinimalPublisher()
try:
rclpy.spin(node) # 保持节点运行
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
7.3 订阅者(Subscriber)(my_subscriber.py)¶
创建文件 my_pubsub/my_pubsub/my_subscriber.py:
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class MinimalSubscriber(Node):
"""一个最小的 ROS 2 订阅者,监听 'chatter' 话题。"""
def __init__(self):
super().__init__('minimal_subscriber')
# 订阅 'chatter' 话题,每收到消息调用 listener_callback
self.subscription = self.create_subscription(
String,
'chatter',
self.listener_callback,
10 # QoS 队列深度
)
def listener_callback(self, msg: String):
self.get_logger().info(f'I heard: "{msg.data}"')
def main(args=None):
rclpy.init(args=args)
node = MinimalSubscriber()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
7.4 注册入口点¶
编辑 setup.py 添加控制台脚本入口点:
entry_points={
'console_scripts': [
'talker = my_pubsub.my_publisher:main',
'listener = my_pubsub.my_subscriber:main',
],
},
7.5 构建与运行¶
cd ~/ros2_ws
colcon build --packages-select my_pubsub
source install/setup.bash
# 终端 1:运行发布者
ros2 run my_pubsub talker
# 终端 2:运行订阅者
ros2 run my_pubsub listener
预期输出:
# 终端 1(talker)
[INFO] [minimal_publisher]: Publishing: "Hello World: 0"
[INFO] [minimal_publisher]: Publishing: "Hello World: 1"
...
# 终端 2(listener)
[INFO] [minimal_subscriber]: I heard: "Hello World: 0"
[INFO] [minimal_subscriber]: I heard: "Hello World: 1"
...
8. ROS 2 启动文件(基于 Python)¶
ROS 2 的启动文件使用 Python 编写。创建 my_pubsub/launch/demo.launch.py:
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='my_pubsub',
executable='talker',
name='my_talker',
output='screen',
parameters=[], # 参数 YAML 文件或字典列表
remappings=[], # 话题重映射
),
Node(
package='my_pubsub',
executable='listener',
name='my_listener',
output='screen',
),
])
运行启动文件:
高级启动特性¶
ROS 2 启动文件支持:
- 参数声明:
DeclareLaunchArgument('robot_name', default_value='robot1') - 条件执行:
IfCondition(LaunchConfiguration('use_camera')) - 变量替换:
${}风格的变量展开 - 包含其他启动文件:
IncludeLaunchDescription(...) - 分组动作:
GroupAction([...])支持命名空间和重映射
9. 从 ROS 1 迁移的建议¶
如果你要将现有的 ROS 1 代码迁移到 ROS 2,以下是实用指南:
-
消息类型基本相同:
.msg、.srv、.action文件语法一致。功能包名称可能不同(如geometry_msgs在两个版本中均可用)。 -
将
rospy替换为rclpy: rospy.init_node()→rclpy.init()+Node('name')rospy.Publisher()→node.create_publisher()rospy.Subscriber()→node.create_subscription()-
rospy.spin()→rclpy.spin(node) -
将
rosrun替换为ros2 run,roslaunch替换为ros2 launch。 -
将 catkin 替换为 colcon:
-
移植启动文件:将 XML 格式的
.launch转换为 Python 格式的.launch.py。虽然ros2 launch也支持旧格式,但 Python 是标准方式。 -
参数:ROS 2 的参数是每个节点独立的(没有全局参数服务器)。使用
ros2 param命令行工具或在代码中声明参数:self.declare_parameter('name', default_value)。 -
日志:将
rospy.loginfo()替换为self.get_logger().info()。 -
时间:将
rospy.Time.now()替换为node.get_clock().now()。 -
使用
ros1_bridge:在混合使用 ROS 1 和 ROS 2 的过渡期,ros1_bridge包可以桥接话题和服务。 -
渐进式迁移:不必一次性移植所有内容。使用
ros1_bridge连接新旧节点,逐步完成迁移。