ROS2系列教程:launch文件与多节点启动(参数/条件/复用)
本文摘要
本篇文章系统掌握了 launch:最简多节点启动、参数传递(yaml/字典/launch 参数)、话题重映射、命名空间、条件控制、子 launch 包含、组与外部进程、事件处理器,以及调试技巧…
本文是 ROS2 系列教程的第 11 篇
本文是 ROS2 系列教程的第 11 篇:launch 文件与多节点启动。一个真实的机器人系统有几十个节点,手动开几十个终端逐个
ros2 run既不现实也不可维护。本篇文章系统讲解 launch——用 Python 编写 launch 文件实现一键启动多节点、传递参数、设置条件与复用、包含其他 launch 文件,掌握ros2 launch命令与调试技巧。学完你就能用一条命令启动整个机器人系统。
一、为什么需要 launch
1.1 手动启动的痛点
# 手动启动一个导航系统:要开十几个终端ros2 run nav2_amcl amcl &ros2 run nav2_bt_navigator bt_navigator &ros2 run nav2_controller controller_server &...痛点显而易见:
- 命令又多又长,容易出错。
- 节点间的启动顺序、参数传递无法管理。
- 无法“一键启停”、无法复用配置。
1.2 launch 能做什么
launch 文件用 Python 编写(ROS2 用 Python 而非 ROS1 的 XML),描述“启动什么、怎么启动、按什么顺序”:
启动多个节点 → Node(package=..., executable=...)设置节点参数 → parameters=[...]设置命名空间/重映射 → namespace=..., remappings=[...]条件控制 → IfCondition/UnlessCondition复用其他 launch → IncludeLaunchDescription启动外部进程 → ExecuteProcess事件与回调 → RegisterEventHandler(复杂场景)一句话:launch 是 ROS2 的系统编排层。
二、launch 文件基础
2.1 最简 launch 文件
from launch import LaunchDescriptionfrom launch_ros.actions import Node
def generate_launch_description(): return LaunchDescription([ Node( package='py_demo', executable='py_talker', name='talker', # 覆盖节点名 output='screen', # 输出到屏幕(替代终端日志) ), Node( package='py_demo', executable='py_subscriber', name='listener', output='screen', ), ])运行:
ros2 launch py_demo demo.launch.py注意:
- 函数名固定为
generate_launch_description()。 package+executable定位节点(executable 是 entry_points/CMake 里定义的命令名)。output='screen'把节点日志打到终端(不设置则静默)。- launch 文件放在包的
launch/目录,并在setup.py/CMakeLists.txt安装。
2.2 launch 文件的安装
Python 包(setup.py 里已加过):
data_files=[ ... (os.path.join('share', package_name, 'launch'), glob('launch/*.launch.py')),]C++ 包(CMakeLists.txt):
install(DIRECTORY launch DESTINATION share/${PROJECT_NAME})三、参数与重映射
3.1 传递参数
# launch/params.launch.py —— 参数传递from launch import LaunchDescriptionfrom launch_ros.actions import Node
def generate_launch_description(): return LaunchDescription([ Node( package='py_demo', executable='configurable_sensor', name='sensor', output='screen', # 方式一:parameters 传 yaml 文件 parameters=['config/params.yaml'], ), Node( package='py_demo', executable='param_node', name='param_node', output='screen', # 方式二:直接传参数对 parameters=[{'robot_name': 'my_rover', 'max_speed': 3.0}], ), ])3.2 从命令行传参(launch 参数)
# launch/args.launch.py —— 带命令行参数的 launchfrom launch import LaunchDescriptionfrom launch.actions import DeclareLaunchArgumentfrom launch.substitutions import LaunchConfigurationfrom launch_ros.actions import Node
def generate_launch_description(): # 声明 launch 参数(默认值) speed_arg = DeclareLaunchArgument( 'max_speed', default_value='2.5', description='最大速度')
# 用 LaunchConfiguration 读取 max_speed = LaunchConfiguration('max_speed')
return LaunchDescription([ speed_arg, Node( package='py_demo', executable='param_node', name='param_node', output='screen', parameters=[{'max_speed': max_speed}], # 引用 launch 参数 ), ])运行:
# 默认值 2.5ros2 launch py_demo args.launch.py
# 自定义 5.0ros2 launch py_demo args.launch.py max_speed:=5.03.3 话题重映射
重映射解决“同一个包换个话题名用”的场景(如双机器人,一个用 /robot1/cmd_vel):
Node( package='py_demo', executable='py_talker', name='talker_1', output='screen', remappings=[ ('chatter', 'robot1/chatter'), # 原话题 → 新话题 ],)重映射对话题、服务、动作名都生效,是“多实例复用”的关键技术。
四、命名空间
4.1 设置命名空间
# launch/namespace.launch.py —— 命名空间from launch import LaunchDescriptionfrom launch_ros.actions import Node
def generate_launch_description(): return LaunchDescription([ # 两个机器人,各自命名空间 Node( package='py_demo', executable='py_talker', name='talker', namespace='robot1', output='screen', ), Node( package='py_demo', executable='py_talker', name='talker', namespace='robot2', output='screen', ), ])启动后 ros2 node list 显示:
/robot1/talker/robot2/talker同名节点 + 不同命名空间互不冲突——这就是命名空间的价值:一套代码,多机器人实例。
4.2 相对与绝对话题
设置命名空间后,话题名会跟着“前缀”:
代码里写 'chatter'(相对名)→ 命名空间 robot1 下实际为 /robot1/chatter代码里写 '/chatter'(绝对名,以 / 开头)→ 仍是 /chatter,不受命名空间影响实践建议:默认用相对话题名(随命名空间走),需要跨命名空间共享的数据用绝对名。
五、条件与组合
5.1 条件启动
# launch/conditional.launch.py —— 条件控制from launch import LaunchDescriptionfrom launch.actions import DeclareLaunchArgumentfrom launch.substitutions import LaunchConfigurationfrom launch.conditions import IfCondition, UnlessConditionfrom launch_ros.actions import Node
def generate_launch_description(): use_sim = LaunchConfiguration('use_sim') # 'true' / 'false'
return LaunchDescription([ DeclareLaunchArgument('use_sim', default_value='false'), # use_sim=true 时启动仿真节点 Node( package='gazebo_ros', executable='spawn_entity.py', condition=IfCondition(use_sim), arguments=['-entity', 'robot', '-file', 'robot.urdf'], ), # use_sim=false 时启动实体驱动 Node( package='py_demo', executable='robot_driver', condition=UnlessCondition(use_sim), output='screen', ), ])运行:
ros2 launch py_demo conditional.launch.py use_sim:=true # 仿真ros2 launch py_demo conditional.launch.py use_sim:=false # 实体5.2 包含其他 launch 文件
大型系统拆分成多个 launch 子文件,用 IncludeLaunchDescription 组合:
# launch/main.launch.py —— 包含子 launchimport osfrom ament_index_python.packages import get_package_share_directoryfrom launch import LaunchDescriptionfrom launch.actions import IncludeLaunchDescriptionfrom launch.launch_description_sources import PythonLaunchDescriptionSourcefrom launch_ros.actions import Node
def generate_launch_description(): py_demo_dir = get_package_share_directory('py_demo')
return LaunchDescription([ # 包含同包内的 sensor.launch.py IncludeLaunchDescription( PythonLaunchDescriptionSource( os.path.join(py_demo_dir, 'launch', 'sensor.launch.py')), launch_arguments={'rate': '20.0'}.items(), # 传参给子 launch ), # 包含其他包的 launch(如 Nav2 官方 launch) IncludeLaunchDescription( PythonLaunchDescriptionSource( os.path.join( get_package_share_directory('nav2_bringup'), 'launch', 'bringup_launch.py')), ), Node( package='py_demo', executable='display_node', output='screen', ), ])分层设计模式:底层每个模块一个 launch(sensor.launch.py、nav.launch.py、arm.launch.py),顶层 main.launch.py 组合全部——类似“模块化 + 总装配”。
5.3 组(GroupAction)
用 GroupAction 给一组节点统一设置条件/命名空间:
from launch.actions import GroupAction
def generate_launch_description(): return LaunchDescription([ GroupAction( # 组内所有节点共享命名空间 scoped=True, namespace='robot1', actions=[ Node(package='py_demo', executable='py_talker', name='talker'), Node(package='py_demo', executable='py_subscriber', name='listener'), ], ), ])六、外部进程与事件
6.1 启动外部进程
launch 也可以启动非 ROS 命令(如打开 rviz2、启动仿真器):
from launch import LaunchDescriptionfrom launch.actions import ExecuteProcessfrom launch_ros.actions import Node
def generate_launch_description(): return LaunchDescription([ Node( package='py_demo', executable='sensor_node', output='screen', ), # 启动 rviz2(非 ROS 节点进程) ExecuteProcess( cmd=['rviz2'], output='screen', ), ])6.2 事件处理器(高级)
用 RegisterEventHandler 实现“节点启动后执行动作”:
from launch import LaunchDescriptionfrom launch.actions import RegisterEventHandler, ExecuteProcessfrom launch.event_handlers import OnProcessStartfrom launch_ros.actions import Node
def generate_launch_description(): sensor = Node(package='py_demo', executable='sensor_node', output='screen')
# sensor 启动后,再启动显示节点(保证顺序) start_display_after_sensor = RegisterEventHandler( OnProcessStart( target_action=sensor, on_start=[ Node(package='py_demo', executable='display_node', output='screen'), ], ) )
return LaunchDescription([ sensor, start_display_after_sensor, ])七、调试 launch
7.1 常用命令
# 打印 launch 文件展开后的完整描述(不实际启动)ros2 launch py_demo demo.launch.py --show-argsros2 launch py_demo demo.launch.py --show-description
# 正常启动(Ctrl+C 可优雅关闭所有节点)ros2 launch py_demo demo.launch.py
# 打印 launch 日志(debug 级别,排查"节点为什么没启动")ros2 launch py_demo demo.launch.py --log-level debug7.2 常见问题排查
| 现象 | 原因 | 解决 |
|---|---|---|
Package 'xxx' not found |
launch 文件没安装 | 重新 colcon build 并 source |
| 节点启动但无输出 | 忘了 output='screen' |
加上 output='screen' |
LaunchConfiguration 未定义 |
未先 DeclareLaunchArgument | 先声明再引用 |
| 参数没生效 | 参数名/节点名不匹配 | 检查 yaml 顶层节点名与 name= 一致 |
| launch 报语法错误 | Python 语法或 import 缺失 | 本地 python3 xxx.launch.py 先测语法 |
| Ctrl+C 后进程残留 | 未优雅退出 | launch 一般自动清理;残留用 pkill -f 节点名 |
7.3 launch 文件写法的工程建议
- 一个模块一个 launch,顶层用 IncludeLaunchDescription 组合。
- 参数全部走 launch 参数(DeclareLaunchArgument),不硬编码。
- 命名空间 + 重映射支持多实例复用。
- 用
--show-description验证 launch 展开结果再实际启动。 - 文件命名统一
xxx.launch.py,放launch/目录。
八、实战:一键启动机器人演示系统
8.1 系统组成
模拟一个小型机器人演示系统:
sensor_node → 发布 /odom、/scan(模拟传感器)robot_controller → 订阅 /scan,发布 /cmd_vel(简单避障逻辑)display_node → 订阅 /odom、/cmd_vel,打印状态rviz2 → 可视化(外部进程)8.2 传感器节点
import rclpy, mathfrom rclpy.node import Nodefrom nav_msgs.msg import Odometryfrom sensor_msgs.msg import LaserScan
class SensorNode(Node): def __init__(self): super().__init__('sensor_node') self.odom_pub = self.create_publisher(Odometry, 'odom', 50) self.scan_pub = self.create_publisher(LaserScan, 'scan', 10) self.create_timer(0.02, self.pub_odom) # 50Hz self.create_timer(0.1, self.pub_scan) # 10Hz self.t = 0.0
def pub_odom(self): self.t += 0.02 msg = Odometry() msg.header.stamp = self.get_clock().now().to_msg() msg.header.frame_id = 'odom' msg.child_frame_id = 'base_link' msg.pose.pose.position.x = math.sin(self.t) msg.pose.pose.position.y = math.cos(self.t) self.odom_pub.publish(msg)
def pub_scan(self): msg = LaserScan() msg.header.stamp = self.get_clock().now().to_msg() msg.header.frame_id = 'laser' msg.angle_min = -3.14; msg.angle_max = 3.14 msg.angle_increment = 0.01745 msg.range_min = 0.1; msg.range_max = 10.0 msg.ranges = [1.0 + 0.1*math.sin(i*0.3) for i in range(360)] self.scan_pub.publish(msg)
def main(): rclpy.init() node = SensorNode() rclpy.spin(node) node.destroy_node() rclpy.shutdown()
if __name__ == '__main__': main()8.3 控制器节点
# py_demo/robot_controller.py —— 简单避障控制器import rclpyfrom rclpy.node import Nodefrom sensor_msgs.msg import LaserScanfrom geometry_msgs.msg import Twist
class RobotController(Node): def __init__(self): super().__init__('robot_controller') self.cmd_pub = self.create_publisher(Twist, 'cmd_vel', 10) self.scan_sub = self.create_subscription( LaserScan, 'scan', self.on_scan, 10) self.declare_parameter('avoid_distance', 0.5)
def on_scan(self, msg): # 计算前方最近障碍 n = len(msg.ranges) front = msg.ranges[n//2] if n > 0 else 10.0 avoid = self.get_parameter('avoid_distance').value cmd = Twist() if front < avoid: cmd.linear.x = 0.0 cmd.angular.z = 0.5 # 前方有障碍:原地转弯 else: cmd.linear.x = 0.3 # 安全:直行 self.cmd_pub.publish(cmd)
def main(): rclpy.init() node = RobotController() rclpy.spin(node) node.destroy_node() rclpy.shutdown()
if __name__ == '__main__': main()8.4 显示节点
import rclpyfrom rclpy.node import Nodefrom nav_msgs.msg import Odometryfrom geometry_msgs.msg import Twist
class DisplayNode(Node): def __init__(self): super().__init__('display_node') self.odom_sub = self.create_subscription(Odometry, 'odom', self.on_odom, 50) self.cmd_sub = self.create_subscription(Twist, 'cmd_vel', self.on_cmd, 10)
def on_odom(self, msg): self.get_logger().info( f'位姿: x={msg.pose.pose.position.x:.2f} y={msg.pose.pose.position.y:.2f}', throttle_duration_sec=1.0) # 每秒最多打一条,防刷屏
def on_cmd(self, msg): self.get_logger().info(f'指令: vx={msg.linear.x:.2f} wz={msg.angular.z:.2f}', throttle_duration_sec=1.0)
def main(): rclpy.init() node = DisplayNode() rclpy.spin(node) node.destroy_node() rclpy.shutdown()
if __name__ == '__main__': main()8.5 主 launch 文件
from launch import LaunchDescriptionfrom launch.actions import DeclareLaunchArgument, ExecuteProcessfrom launch.substitutions import LaunchConfigurationfrom launch_ros.actions import Node
def generate_launch_description(): avoid_distance = LaunchConfiguration('avoid_distance')
return LaunchDescription([ DeclareLaunchArgument('avoid_distance', default_value='0.5'), Node( package='py_demo', executable='sensor_node', name='sensor_node', output='screen', ), Node( package='py_demo', executable='robot_controller', name='robot_controller', output='screen', parameters=[{'avoid_distance': avoid_distance}], ), Node( package='py_demo', executable='display_node', name='display_node', output='screen', ), ExecuteProcess(cmd=['rviz2'], output='screen'), ])8.6 一键启动
# 一条命令启动整个系统ros2 launch py_demo robot_demo.launch.py
# 调整参数ros2 launch py_demo robot_demo.launch.py avoid_distance:=1.0
# 另一个终端观察ros2 node list # 应看到 3 个节点ros2 topic hz /odom # 50Hzros2 topic hz /scan # 10Hz预期结果:一条命令拉起 3 个 ROS 节点 + rviz2;Ctrl+C 一次全部优雅关闭。这就是 launch 的工程价值——系统编排、一键启停、参数化复用。
九、总结
本篇文章系统掌握了 launch:最简多节点启动、参数传递(yaml/字典/launch 参数)、话题重映射、命名空间、条件控制、子 launch 包含、组与外部进程、事件处理器,以及调试技巧。实战中一键启动了完整的“传感器-控制-显示”机器人演示系统。
关键要点回顾:
- launch 文件 = Python 函数
generate_launch_description()返回LaunchDescription。 Node(package, executable, name, output)是核心动作;别忘了output='screen'。- 参数三方式:parameters=[yaml]、parameters=[dict]、launch 参数(DeclareLaunchArgument + LaunchConfiguration)。
- 重映射改话题名、命名空间隔离多实例,两者是复用的关键。
IfCondition/UnlessCondition条件启动;IncludeLaunchDescription组合子 launch。- launch 文件要安装(setup.py/CMakeLists),并用
--show-description调试。
下一篇预告
下一篇进入 常用工具详解:系统掌握 ros2 CLI 全家桶(node/topic/service/action/param/interface/pkg/daemon)、可视化工具 rqt(rqt_graph/rqt_console/rqt_tf_tree/rqt_reconfigure)、RViz2 基础使用、ros2 bag 数据录制回放,以及 tf2 工具基础。这些工具是 ROS2 开发调试的“眼睛和双手”,学完你将具备独立排障能力。
ROS2系列教程:launch文件与多节点启动(参数/条件/复用)
作者:Wang Shijie
本文链接:https://wsj-blog.pages.dev/posts/ros2-xi-lie-jiao-cheng-launch-wen-jian-yu-duo-jie-dian-qi/
本文采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
留言评论
评论资源加载失败,请重试。