第一次接触ROS2时,最让我头疼的就是每次调试都要手动开十几个终端窗口。想象一下这样的场景:你的移动机器人需要同时运行激光雷达驱动、视觉SLAM、导航规划、传感器融合等多个模块,每个模块都是一个独立节点。传统方式下,你得逐个执行ros2 run命令,不仅效率低下,还容易遗漏关键节点。
这就是launch文件存在的意义。它相当于一个"一键启动"脚本,能够:
在工业级应用中,我曾用launch文件实现过这样的工作流:早晨插上机器人电源→自动检测连接的传感器→根据环境变量选择仿真/实车模式→加载对应参数配置→完整启动所有功能节点。整个过程无需人工干预,极大提升了开发效率。
所有ROS2 launch文件都遵循相同的模板结构,就像Python的if __name__ == '__main__'约定。新建一个my_robot.launch.py文件:
python复制from launch import LaunchDescription
def generate_launch_description():
return LaunchDescription([
# 这里添加你的节点配置
])
这个骨架代码中,generate_launch_description()是ROS2约定的入口函数,必须返回一个LaunchDescription对象。我建议在项目初期就建立这样的规范:
launch目录功能描述.launch.py格式假设我们要启动一个激光雷达和底盘控制节点:
python复制from launch_ros.actions import Node
lidar_node = Node(
package='rplidar_ros',
executable='rplidar_node',
name='rplidar',
parameters=[{'serial_port': '/dev/ttyUSB0'}]
)
base_node = Node(
package='robot_base',
executable='base_controller',
name='base',
remappings=[('/cmd_vel', '/nav/cmd_vel')]
)
这里有几个关键细节需要注意:
parameters支持直接传递字典或YAML文件路径remappings解决话题命名冲突问题namespaceROS2提供了两种等效的节点添加语法:
方法一:直接列表填充
python复制return LaunchDescription([
lidar_node,
base_node
])
方法二:动态add_action
python复制ld = LaunchDescription()
ld.add_action(lidar_node)
ld.add_action(base_node)
return ld
我个人的经验法则是:简单场景用方法一,需要条件判断时用方法二。比如根据环境变量决定是否启动仿真节点:
python复制if os.getenv('SIMULATION') == 'true':
ld.add_action(sim_node)
当系统扩展到10+个节点时,一个常见的反模式是把所有配置堆在一个launch文件里。我曾接手过一个3000行的巨型启动文件,维护起来简直是噩梦。正确的做法是采用分层架构:
code复制launch/
├── core.launch.py # 硬件驱动层
├── perception.launch.py # 感知层
├── navigation.launch.py # 导航层
└── main.launch.py # 顶层调度
顶层文件通过IncludeLaunchDescription整合子模块:
python复制perception_launch = IncludeLaunchDescription(
PythonLaunchDescriptionSource([
get_package_share_directory('robot_bringup'),
'/launch/perception.launch.py'
]),
launch_arguments={'use_depth_cam': 'false'}.items()
)
动态参数传递:
python复制DeclareLaunchArgument(
'lidar_port',
default_value='/dev/ttyUSB0',
description='LiDAR device path'
)
Node(
package='rplidar_ros',
parameters=[{
'serial_port': LaunchConfiguration('lidar_port')
}]
)
YAML配置的最佳实践:
yaml复制/sensor:
ros__parameters:
scan_frequency: 10
resolution: 0.5
在launch中加载:
python复制config_path = os.path.join(
get_package_share_directory('config'),
'sensor_params.yaml'
)
Node(parameters=[config_path])
多机器人系统必须处理命名冲突问题。假设有两个AGV小车:
python复制GroupAction(
actions=[
PushRosNamespace('agv1'),
IncludeLaunchDescription(
PythonLaunchDescriptionSource('agv_base.launch.py')
)
]
)
重映射的典型应用场景:
python复制Node(
remappings=[
('/image_raw', '/camera/left/image_raw'),
('/camera_info', '/camera/left/camera_info')
]
)
通过ExecuteProcess实现硬件检测:
python复制ExecuteProcess(
cmd=['check_sensor', '--lidar'],
on_exit=[
LogInfo(msg='LiDAR detected, starting nodes...'),
lidar_node
]
)
python复制DeclareLaunchArgument(
'robot_id',
default_value=EnvironmentVariable('ROBOT_ID'),
description='Unique robot identifier'
)
python复制RegisterEventHandler(
OnProcessStart(
target_action=base_node,
on_start=[
LogInfo(msg='Base controller started'),
navigation_node
]
)
)
package和executable名称是否匹配ros2 param list验证ros2 topic list -t查看话题映射GroupAction配合scoped=TrueTimerAction控制时序SetParameter('use_sim_time', True)python复制GroupAction(
actions=[lidar_node, camera_node],
scoped=True,
launch_configurations={'delay': '2.0'}
)
一个工业级项目的标准布局:
code复制robot_bringup/
├── config/
│ ├── params.yaml
│ └── rviz/
├── launch/
│ ├── modules/
│ │ ├── sensors.launch.py
│ │ └── navigation.launch.py
│ └── main.launch.py
└── scripts/
└── health_check.py
配套的setup.py配置:
python复制data_files=[
(os.path.join('share', package_name, 'launch'), glob('launch/*.launch.py')),
(os.path.join('share', package_name, 'config'), glob('config/*.yaml'))
]
通过启动参数实现环境适配:
python复制def generate_launch_description():
sim_arg = DeclareLaunchArgument(
'use_sim',
default_value='false',
description='Use simulation clock'
)
if LaunchConfiguration('use_sim') == 'true':
return LaunchDescription([
sim_arg,
IncludeLaunchDescription('simulation.launch.py')
])
else:
return LaunchDescription([
sim_arg,
IncludeLaunchDescription('real_robot.launch.py')
])
启动时指定参数:
bash复制ros2 launch robot_bringup main.launch.py use_sim:=true
respawn=Trueoutput='screen'python复制Node(
respawn=True,
respawn_delay=5.0,
output='screen'
)
在真实项目中,我曾遇到过一个经典问题:两个节点因启动顺序导致话题丢失。解决方案是添加事件处理:
python复制RegisterEventHandler(
OnProcessExit(
target_action=driver_node,
on_exit=[processor_node]
)
)