4、ROS2启动系统(Launch):Launch文件基础结构与语法、包含(Include)与合并(Merge)启动文件、参数化启动与命令行传参、条件逻辑与变量替换、启动文件调试与错误排查
说到ROS2的启动系统,我其实挺感慨的。早年在ROS1时代,我们启动一堆节点全靠roslaunch,虽然也能用,但说实话,那个XML语法写起来挺别扭的。到了ROS2,官方彻底重写了这套机制,用Python来写Launch文件——嗯,这个决定我个人非常喜欢。
为什么?因为Python本身就是编程语言,你可以在Launch文件里写循环、条件判断、甚至调用外部API。说白了,ROS2的Launch系统已经不是一个简单的「启动脚本」,而是一个真正的程序化启动框架。
4.1 Launch文件基础结构与语法
先看一个最基础的Launch文件长什么样。我习惯把它叫做「启动入口」。
# my_robot_launch.py
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='my_package',
executable='my_node',
name='my_node_instance',
output='screen'
)
])
注意看,核心就两个东西:LaunchDescription 和 Node。LaunchDescription 是一个列表,里面放着你想要启动的所有动作。而 Node 就是最常用的动作——启动一个ROS2节点。
这里有个细节:generate_launch_description 这个函数名是固定的,ROS2的launch系统会默认调用它。我曾经见过有人把函数名拼错成 generate_launch_descriptions(多了个s),结果启动时报错说找不到入口函数。嗯,这种坑踩过一次就记住了。
output='screen',这样节点的日志会直接打印到终端。调试阶段特别有用,省得你到处翻日志文件。
4.2 包含(Include)与合并(Merge)启动文件
实际项目中,一个机器人系统往往有几十个节点。你不可能把所有启动逻辑写在一个文件里——那会变成一团乱麻。这时候就需要「包含」和「合并」机制。
包含(Include) 就像C语言里的 #include,把另一个Launch文件的内容拉进来。
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from ament_index_python.packages import get_package_share_directory
def generate_launch_description():
return LaunchDescription([
IncludeLaunchDescription(
PythonLaunchDescriptionSource([
get_package_share_directory('my_bot_bringup'),
'/launch/sensors_launch.py'
])
),
IncludeLaunchDescription(
PythonLaunchDescriptionSource([
get_package_share_directory('my_bot_bringup'),
'/launch/navigation_launch.py'
])
),
])
你看,我把传感器启动和导航启动拆成了两个独立的Launch文件,然后在主文件里把它们包含进来。这样每个文件职责单一,维护起来也轻松。
合并(Merge) 稍微有点不同。它允许你把多个Launch描述合并成一个。我记得有一次做多机器人协同项目,每个机器人都有自己的Launch文件,我需要把它们合并到一个总控文件里。用 MergeLaunchDescription 就能搞定:
from launch.actions import MergeLaunchDescription
merged = MergeLaunchDescription([
launch_description_1,
launch_description_2
])
4.3 参数化启动与命令行传参
这是ROS2 Launch系统最强大的特性之一。你想想看,如果每次改个参数都要修改Launch文件,那得多麻烦?参数化启动就是让你在启动时动态传入参数。
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
def generate_launch_description():
return LaunchDescription([
DeclareLaunchArgument(
'use_sim_time',
default_value='false',
description='Use simulation time'
),
DeclareLaunchArgument(
'robot_namespace',
default_value='robot1',
description='Namespace for the robot'
),
Node(
package='my_package',
executable='my_node',
namespace=LaunchConfiguration('robot_namespace'),
parameters=[{
'use_sim_time': LaunchConfiguration('use_sim_time')
}]
)
])
启动时这样传参:
ros2 launch my_package my_launch.py use_sim_time:=true robot_namespace:=robot2
我个人习惯把常用的参数都声明成 DeclareLaunchArgument,并给一个合理的默认值。这样既可以在命令行覆盖,也可以直接用默认值启动。我曾经在调试一个导航系统时,需要频繁切换仿真和实车模式,靠的就是这个 use_sim_time 参数——一行命令搞定,不用改任何代码。
4.4 条件逻辑与变量替换
有时候你需要根据条件决定是否启动某个节点。比如,只有在仿真模式下才启动Gazebo,实车模式下就不启动。ROS2的Launch系统支持条件逻辑:
from launch.actions import IfCondition, GroupAction
from launch.substitutions import LaunchConfiguration, PythonExpression
def generate_launch_description():
use_sim = LaunchConfiguration('use_sim_time')
return LaunchDescription([
DeclareLaunchArgument('use_sim_time', default_value='false'),
GroupAction(
condition=IfCondition(use_sim),
actions=[
Node(
package='gazebo_ros',
executable='spawn_entity.py',
arguments=['-topic', 'robot_description']
)
]
),
GroupAction(
condition=IfCondition(PythonExpression(['not ', use_sim])),
actions=[
Node(
package='my_robot',
executable='motor_driver'
)
]
)
])
这里用到了 IfCondition 和 PythonExpression。说白了,就是Python的 if 语句,只不过包装成了Launch的语法。你甚至可以写更复杂的表达式,比如 PythonExpression(['"', LaunchConfiguration('mode'), '" == "debug"'])。
变量替换 也是日常高频使用的功能。除了 LaunchConfiguration,还有 FindPackageShare、PathJoinSubstitution 等:
from launch.substitutions import FindPackageShare, PathJoinSubstitution
config_path = PathJoinSubstitution([
FindPackageShare('my_package'),
'config',
'params.yaml'
])
Node(
package='my_package',
executable='my_node',
parameters=[config_path]
)
这样写的好处是:路径是动态解析的,不管你的工作空间安装在哪里,都能正确找到文件。我见过太多人硬编码绝对路径,结果换台机器就崩了。
4.5 启动文件调试与错误排查
Launch文件写多了,难免遇到问题。我总结了一套排查流程,分享给你:
- 先检查语法:用
ros2 launch --show-args your_launch.py看看能不能正确解析参数列表。如果这一步就报错,说明Python语法有问题。 - 查看完整日志:启动时加上
--log-level debug,ROS2会打印详细的Launch执行过程。比如:ros2 launch my_launch.py --log-level debug - 检查节点是否真的启动了:用
ros2 node list查看当前运行的节点列表。如果节点没出现,可能是Launch文件里的executable名字写错了。 - 参数传递验证:用
ros2 param list /node_name和ros2 param get /node_name param_name检查参数是否被正确设置。
我曾经踩过的一个坑: 有一次我写了一个Launch文件,里面包含了一个子Launch文件,但子Launch文件里又声明了相同的参数名。结果启动时参数冲突,节点一直报错。排查了半天才发现是参数名重复了。所以我现在有个习惯:在包含子Launch文件时,尽量使用 launch_arguments 显式传递参数,避免隐式继承导致的冲突。
还有一个实用技巧:如果你不确定Launch文件里某个变量的值是什么,可以在文件里加一行 print() 输出调试信息。因为Launch文件本质上是Python脚本,所以 print 是有效的。不过记得调试完删掉,不然每次启动都会打印一堆东西。
嗯,关于ROS2 Launch系统的基础内容,今天就聊到这里。下一章我们会深入探讨更高级的启动技巧,比如事件驱动和生命周期节点管理。到时候再跟你分享更多实战经验。