4. 启动文件解析:nav2_bringup 启动流程、launch 文件参数传递、多机器人配置
好,我们进入第四章。这一章我打算把 nav2_bringup 的启动流程彻底拆开给你看。
很多朋友拿到 Nav2 第一件事就是 ros2 launch nav2_bringup navigation_launch.py,跑起来了就以为万事大吉。但一旦要改参数、加机器人、或者换传感器,就懵了。说白了,你不懂 launch 文件怎么组织,后面调参就是瞎蒙。
我个人习惯,拿到一个新系统,第一件事就是读它的 launch 文件。这比读管用多了。
4.1 nav2_bringup 的整体启动流程
先看一张图,把整个启动流程串起来。
整个流程其实就六步。但每一步都有坑,我一个个说。
4.2 launch 文件的结构与参数传递
Nav2 的 launch 文件用了 Python launch 格式。为什么不用 XML?因为 Python 可以写逻辑,可以条件判断,可以循环。你想想看,多机器人场景下,你难道要手写 10 个 XML 文件?
来看核心的 navigation_launch.py,我简化一下关键部分:
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
def generate_launch_description():
# 1. 声明可配置参数
declared_arguments = []
declared_arguments.append(
DeclareLaunchArgument('use_sim_time', default_value='false',
description='Use simulation time')
)
declared_arguments.append(
DeclareLaunchArgument('params_file', default_value='nav2_params.yaml',
description='Full path to params file')
)
# 2. 加载参数文件
params_file = LaunchConfiguration('params_file')
# 3. 启动核心节点
lifecycle_nodes = ['controller_server', 'planner_server',
'recoveries_server', 'bt_navigator',
'waypoint_follower', 'velocity_smoother']
# 4. 启动生命周期管理器
start_lifecycle_manager = Node(
package='nav2_lifecycle_manager',
executable='lifecycle_manager',
name='lifecycle_manager_navigation',
parameters=[{'use_sim_time': use_sim_time,
'autostart': True,
'node_names': lifecycle_nodes}]
)
return LaunchDescription(declared_arguments + [start_lifecycle_manager])
DeclareLaunchArgument 暴露出来,这样换场景只需要改命令行参数,不用改代码。
参数传递的路径是这样的:
| 层级 | 传递方式 | 典型示例 |
|---|---|---|
| 命令行 → launch | LaunchConfiguration |
params_file:=~/my_params.yaml |
| launch → 节点 | Node(parameters=[...]) |
直接传入字典或 YAML 路径 |
| launch → 子 launch | IncludeLaunchDescription(launch_arguments={...}) |
传递 namespace 或 robot_name |
| YAML 文件 → 节点 | ROS2 参数机制 | 节点启动时自动加载 |
这里有个容易翻车的地方:参数覆盖顺序。ROS2 的参数优先级是:命令行 > launch 文件内传入 > YAML 文件。我曾经遇到一个情况,在 YAML 里配好了 transform_tolerance: 0.5,但 launch 文件里又传了个 0.3,结果怎么调都不对。排查了半天才发现是 launch 文件里的参数把 YAML 的覆盖了。
parameters=[{'key': 'value'}] 这种方式传参,它会完全覆盖 YAML 文件里的同名参数。不是合并,是覆盖。我建议要么全用 YAML,要么全用 launch 传参,不要混用。
4.3 多机器人配置实战
多机器人导航,说白了就是「一套代码,多个命名空间」。每个机器人要有独立的 namespace,独立的 TF 树,独立的参数。
我做过一个项目,三台机器人在同一个仓库里跑。一开始我天真地以为,把 launch 文件复制三份,改改名字就行了。结果 TF 树打架,topic 冲突,AMCL 互相抢数据。后来我总结了一套标准做法。
核心思路:用 namespace 隔离一切。
# multi_robot_launch.py
from launch import LaunchDescription
from launch.actions import GroupAction
from launch_ros.actions import PushRosNamespace, Node
from launch.substitutions import LaunchConfiguration
def generate_launch_description():
robots = [
{'name': 'robot1', 'namespace': 'r1', 'x': 0.0, 'y': 0.0},
{'name': 'robot2', 'namespace': 'r2', 'x': 2.0, 'y': 1.0},
{'name': 'robot3', 'namespace': 'r3', 'x': -1.0, 'y': -0.5},
]
groups = []
for robot in robots:
# 每个机器人一个 GroupAction,带独立 namespace
group = GroupAction([
PushRosNamespace(robot['namespace']),
# 启动该机器人的导航栈
IncludeLaunchDescription(
'nav2_bringup/launch/navigation_launch.py',
launch_arguments={
'namespace': robot['namespace'],
'params_file': f'params_{robot["name"]}.yaml',
'use_sim_time': 'true',
'autostart': 'true'
}.items()
),
# 启动该机器人的 TF 发布
Node(
package='robot_state_publisher',
executable='robot_state_publisher',
name='robot_state_publisher',
parameters=[{'robot_description': robot_description}]
)
])
groups.append(group)
return LaunchDescription(groups)
这里有几个关键点:
- 命名空间隔离:每个机器人的所有 topic 都会带上 namespace 前缀,比如
/r1/odom、/r2/odom,不会冲突。 - 独立参数文件:每个机器人有自己的 YAML 参数,可以单独调优。比如 robot1 是差速底盘,robot2 是阿克曼底盘,参数完全不同。
- 独立 TF 树:每个机器人的 TF 树在各自的 namespace 下,通过
tf2_ros::Buffer的setUsingDedicatedThread()隔离。
| 参数项 | robot1(差速) | robot2(阿克曼) | 说明 |
|---|---|---|---|
base_frame_id |
r1/base_link |
r2/base_link |
必须带 namespace 前缀 |
odom_frame_id |
r1/odom |
r2/odom |
同上 |
transform_tolerance |
0.2 | 0.5 | 阿克曼底盘延迟大,容忍度要高 |
planner_server.robot_base_frame |
r1/base_link |
r2/base_link |
规划器也要知道自己的 frame |
local_costmap.global_frame |
r1/odom |
r2/odom |
代价地图的全局坐标系 |
嗯,这里要注意一个细节:多机器人共享地图的问题。如果多个机器人在同一张地图上导航,你需要一个全局的 map_server,而且所有机器人的 global_frame 都要指向同一个 map 坐标系。但 odom 坐标系必须是独立的,否则里程计会互相干扰。
我当时的做法是:
- 启动一个全局的
map_server,不带 namespace,发布/maptopic。 - 每个机器人的
global_costmap设置global_frame: map,订阅全局/map。 - 每个机器人的
local_costmap设置global_frame: r1/odom,用自己独立的里程计。 - TF 树里,
map→r1/odom的变换由 AMCL 发布,每个机器人独立维护。
这样既共享了全局地图,又保持了局部控制的独立性。说白了就是「全局统一,局部自治」。
ros2 topic list | grep -E "/(r1|r2|r3)/" 检查 topic 是否隔离。再用 ros2 run tf2_tools view_frames 看 TF 树,确保每个机器人的 frame 都在自己的 namespace 下。
最后说一个我踩过的坑:生命周期节点的同步问题。多机器人场景下,每个机器人都有自己的生命周期管理器。如果所有机器人同时启动,生命周期管理器会同时尝试激活所有节点,导致 CPU 瞬间飙高,甚至节点超时。我的解决方案是在 launch 文件里加一个 startup_delay 参数,每个机器人错开 2-3 秒启动:
from launch.actions import TimerAction
for i, robot in enumerate(robots):
delayed_group = TimerAction(
period=2.0 * i, # 每个机器人延迟 2 秒
actions=[group]
)
groups.append(delayed_group)
这个改动虽然简单,但效果立竿见影。从那以后,我再也没有遇到过多机器人启动时节点崩溃的问题。