ros2高级篇之高可用启动文件及配置编写

Source

1.launch文件核心介绍

ros2的launch文件必须实现generate_launch_description() 函数,它是 ROS 2 启动文件的强制要求,必须实现且返回一个 LaunchDescription 对象。它的主要职责包括:

声明参数(通过 DeclareLaunchArgument)

配置节点(通过 Node)

组合多个组件(如包含其他启动文件、设置环境变量等)

定义启动顺序和依赖关系

一个简单的案例如下:

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    # 1. 声明参数
    arg1 = DeclareLaunchArgument('param1', default_value='value1')
    
    # 2. 配置节点
    node1 = Node(
        package='my_package',
        executable='my_node',
        parameters=[{
    
      'param1': LaunchConfiguration('param1')}]
    )
    
    # 3. 返回LaunchDescription对象
    return LaunchDescription([
        arg1,
        node1,
        # 其他Actions...
    ])

这样在以该启动脚本启动的时候就会自动拉起node1,并指定启动参数param1
需要注意的是,arg1中的param1和node1中的param1是同一个参数不同的使用阶段。
(1) arg1 声明参数

arg1 = DeclareLaunchArgument('param1', default_value='value1')
作用:向 ROS 2 启动系统注册一个名为 param1 的可配置参数,并设置默认值 'value1'。

效果:允许通过命令行或父启动文件覆盖该参数:
bash

ros2 launch my_package my_launch.py param1:=custom_value

(2) node1 使用参数

parameters=[{
    
      'param1': LaunchConfiguration('param1')}]
LaunchConfiguration('param1'):
动态引用已声明的 param1 参数值(由 arg1 声明)。

行为:在节点启动时,ROS 2 会将 param1 的当前值(可能是默认值或被覆盖的值)传递给节点。

(3)二者的关系

特性 arg1 的 param1 (声明阶段) node1 的 param1 (使用阶段)
目的 定义参数的存在性和默认值 引用参数的实际值
执行时机 在启动文件解析时注册 在节点启动时动态解析
是否可被覆盖 可通过命令行/父启动文件覆盖 自动继承解析后的值
数据类型 声明时不限制类型(默认为字符串) 实际传递给节点的类型(可转换为 int/float 等)

关键功能详解

(1) 参数声明与传递

声明参数:使用 DeclareLaunchArgument 定义可配置参数。

传递参数:通过 LaunchConfiguration 引用参数值。
arg_camera = DeclareLaunchArgument('camera_name', default_value='camera')
node = Node(
    name=LaunchConfiguration('camera_name'),  # 动态引用参数
    ...
)

(2) 节点配置

通过 launch_ros.actions.Node 配置节点属性:

    package:节点所属的功能包

    executable:可执行文件名称

    parameters:参数列表(支持YAML文件或字典)

    namespace:命名空间

    remappings:话题/服务重映射

(3) 组合多个组件

可以包含其他组件,如:

其他启动文件:IncludeLaunchDescription

环境变量:SetEnvironmentVariable

条件逻辑:ExecuteProcess(运行外部命令)
from launch.actions import IncludeLaunchDescription

other_launch = IncludeLaunchDescription(
    '/path/to/other_launch.py',
    launch_arguments={
    
      'arg1': 'value1'}.items()
)

(4)条件启动(高级用法)

通过 IfCondition 或 UnlessCondition 实现条件逻辑:

from launch.conditions import IfCondition
from launch.substitutions import PythonExpression

node = Node(
    condition=IfCondition(
        PythonExpression([LaunchConfiguration('enable_node'), ' == "true"'])
    ),
    ...
)

2 如何定义参数 – DeclareLaunchArgument 的核心作用

(1) 定义可配置参数

声明一个参数,并指定其名称、默认值和描述。

这些参数可以在启动时通过命令行、其他启动文件或工具(如 ros2 launch)进行覆盖。

(2) 提供参数默认值

如果用户不提供该参数的值,则使用默认值。

例如:
DeclareLaunchArgument(
    'camera_name', 
    default_value='camera',  # 默认值
    description='相机名称'
)

(3) 参数文档化

通过 description 字段提供参数说明,方便用户理解其用途。

可通过 ros2 launch --show-args 查看所有可配置参数及其描述。

(4) 参数验证

如果用户传递的参数不符合预期(如类型错误),ROS 2 会发出警告或报错。

典型应用场景
– 动态配置节点名称

DeclareLaunchArgument('node_name', default_value='camera_node')
Node(name=LaunchConfiguration('node_name'), ...)

– 选择是否启用某些功能

DeclareLaunchArgument('enable_depth', default_value='true')
Node(parameters=[{
    
      'enable_depth': LaunchConfiguration('enable_depth')}], ...)

– 加载不同的配置文件

DeclareLaunchArgument('config_file', default_value='default.yaml')
Node(parameters=[LaunchConfiguration('config_file')], ...)

(5) 几种对比:

方式 适用场景 灵活性 管理复杂度
ros2 run + 参数 简单临时测试 低(仅单个节点)
DeclareLaunchArgument + ros2 launch 复杂系统(多节点) 高(支持层级覆盖)
YAML 配置文件 固定配置 中(需修改文件)

如何引用参数 --LaunchConfiguration的作用

通过LaunchConfiguration可获取期望参数值,用法如下

from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration

# 声明参数
camera_arg = DeclareLaunchArgument(
    'camera_name',          # 参数名
    default_value='camera', # 默认值
    description='相机名称'   # 描述
)

# 在节点中使用参数
node = launch_ros.actions.Node(
    package='my_package',
    name=LaunchConfiguration('camera_name'),  # 引用参数
    executable='my_node'
)

高级用法 – 动态拼接

_config_file = LaunchConfiguration('config_file' + param_name_suffix).perform(context)
.perform(context)

    在启动过程中动态计算参数值,将其从LaunchConfiguration对象转换为实际字符串。

    context是启动系统的上下文对象,存储了当前参数、替换规则等信息。

'config_file' + param_name_suffix

    支持参数名的动态拼接,例如:

        如果param_name_suffix='' → 参数名为'config_file'

        如果param_name_suffix='_front' → 参数名为'config_file_front'

这样我门就能在启动时根据不同的参数名来获取参数值, 如:
假设启动文件被这样调用:
bash

ros2 launch realsense2_camera rs_launch.py config_file_front:=/config/front_camera.yaml

且代码中param_name_suffix='_front’时:

拼接参数名 → 'config_file_front'

从上下文获取值 → '/config/front_camera.yaml'

最终返回该路径字符串,供后续YAML解析使用。

常用于:

  • 延迟求值机制:ROS 2启动系统的参数可能在多个地方被覆盖(命令行、父启动文件等),必须在运行时才能确定最终值。

  • 兼容性处理:特别在以下情况需要:

    参数值包含其他LaunchConfiguration或替换规则(如PathJoinSubstitution)

    需要处理参数名动态拼接的情况(如多相机配置时param_name_suffix=‘_front’)

如何覆盖参数值?

(1) 通过命令行覆盖

ros2 launch my_package my_launch_file.py camera_name:=my_new_camera

(2) 通过其他启动文件包含
python

(2) 在另一个启动文件中覆盖参数

from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.substitutions import PathJoinSubstitution

def generate_launch_description():
    return LaunchDescription([
        IncludeLaunchDescription(
            PathJoinSubstitution(['my_package', 'launch', 'my_launch_file.py']),
            launch_arguments={
    
      
                'camera_name': 'custom_camera'  # 覆盖参数
            }.items()
        )
    ])

这里有几个要点:

1. IncludeLaunchDescription

作用

嵌套其他启动文件:允许当前启动文件调用另一个.launch.py文件,实现模块化设计。

参数传递:可以向被包含的启动文件传递或覆盖参数(如示例中的camera_name)。

关键参数

launch_description_source 必需 指定要包含的启动文件(通常配合PathJoinSubstitution使用)
launch_arguments 可选 向子启动文件传递参数的字典(键值对)

示例场景

IncludeLaunchDescription(
    # 指定要包含的启动文件路径
    PathJoinSubstitution(['my_package', 'launch', 'child_launch.py']),
    # 覆盖子启动文件的参数
    launch_arguments={
    
      
        'camera_name': 'custom_camera',  # 覆盖子文件的camera_name参数
        'enable_depth': 'true'           # 添加新参数
    }.items()
)

注意,这里是当前文件定义的参数,覆盖’child_launch.py‘指定的参数

2. PathJoinSubstitution

作用

动态构建路径:跨平台安全地拼接文件路径(自动处理/或\分隔符)。

延迟求值:路径在运行时解析,支持使用其他Substitution对象(如LaunchConfiguration)。

典型用法

from launch.substitutions import PathJoinSubstitution

# 拼接路径:<install_dir>/share/my_package/launch/child_launch.py
PathJoinSubstitution([
    'my_package',          # 包名
    'launch',              # launch目录
    'child_launch.py'      # 文件名
])
PathJoinSubstitution 在运行时计算出完整路径(如/opt/ros/humble/share/my_package/launch/my_launch_file.py)。

自动适应不同操作系统(Linux/macOS/Windows)的路径分隔符。

OpaqueFunction用法

在 ROS 2 的启动系统中,OpaqueFunction 是一个高级功能,用于延迟执行复杂的启动逻辑或动态生成启动动作。下面详细解析它在你的代码中的含义和作用:

  1. OpaqueFunction 的核心作用
    (1) 延迟执行

    将代码逻辑(如节点配置、参数计算)封装到一个函数中,在启动过程的后期才执行(而非在 generate_launch_description() 定义时立即执行)。

    允许访问运行时解析的参数值(通过 LaunchContext)。

(2) 动态生成动作

可以在函数内部根据条件或参数值动态创建 Node、IncludeLaunchDescription 等动作。

适用于需要灵活配置的场景(如多设备、参数化启动)。

如:

LaunchDescription([
    OpaqueFunction(
        function=launch_setup,  # 回调函数
        kwargs={
    
      'params': set_configurable_parameters(configurable_parameters)}  # 传递参数
    )
])

通过function指定了启动执行的函数launch_setup及该函数所需参数kwargs
功能分解

launch_setup 函数

    是实际执行启动逻辑的回调函数,接收 context 和 params 参数。

    内部可能包含节点创建、参数合并等操作(如你之前代码中的 yaml_to_dict 处理)。

kwargs 参数

    向 launch_setup 传递额外的数据(此处是解析后的参数字典)。

    注意:kwargs 中的值会在 generate_launch_description() 阶段计算(非延迟)。

执行时机

    在 ROS 2 启动系统处理完所有 DeclareLaunchArgument 后,才会调用 launch_setup。

什么场景需要该函数?

(1) 处理动态参数

你的 configurable_parameters 可能依赖其他参数(如 param_name_suffix),需要延迟到运行时解析。

例如:动态生成设备名称 camera_name_front。

(2) 避免提前求值

直接在 generate_launch_description() 中调用 launch_setup 会导致参数在未解析时就被计算。

OpaqueFunction 保证参数完全初始化后才执行逻辑。

(3) 支持条件分支

可在 launch_setup 中根据参数值决定是否启动某些节点:
python

def launch_setup(context, params):
    if params['enable_depth'] == 'true':
        return [depth_node]
    return []

注意事项

返回值要求

    launch_setup 必须返回 List[Action](动作列表),即使只有一个动作。

参数解析

    通过 context.perform_substitution() 获取动态参数的实际值。

调试技巧

    在 launch_setup 中打印 context.launch_configurations 查看所有已解析参数:
    python

print(context.launch_configurations)

与之搭配的,通常要定义一个启动设置函数launch_setup

一个完整案例

# Copyright 2023 Intel Corporation. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Launch realsense2_camera node."""
import os
import yaml
from launch import LaunchDescription
import launch_ros.actions
from launch.actions import DeclareLaunchArgument, OpaqueFunction
from launch.substitutions import LaunchConfiguration


configurable_parameters = [{
    
      'name': 'camera_name',                  'default': 'camera', 'description': 'camera unique name'},
                           {
    
      'name': 'camera_namespace',             'default': 'camera', 'description': 'namespace for camera'},
                           {
    
      'name': 'serial_no',                    'default': "''", 'description': 'choose device by serial number'},
                           {
    
      'name': 'usb_port_id',                  'default': "''", 'description': 'choose device by usb port id'},
                           {
    
      'name': 'device_type',                  'default': "''", 'description': 'choose device by type'},
                           {
    
      'name': 'config_file',                  'default': "''", 'description': 'yaml config file'},
                           {
    
      'name': 'json_file_path',               'default': "''", 'description': 'allows advanced configuration'},
                           {
    
      'name': 'initial_reset',                'default': 'false', 'description': "''"},
                           {
    
      'name': 'accelerate_gpu_with_glsl',     'default': "false", 'description': 'enable GPU acceleration with GLSL'},
                           {
    
      'name': 'rosbag_filename',              'default': "''", 'description': 'A realsense bagfile to run from as a device'},
                           {
    
      'name': 'log_level',                    'default': 'info', 'description': 'debug log level [DEBUG|INFO|WARN|ERROR|FATAL]'},
                           {
    
      'name': 'output',                       'default': 'screen', 'description': 'pipe node output [screen|log]'},
                           {
    
      'name': 'enable_color',                 'default': 'true', 'description': 'enable color stream'},
                           {
    
      'name': 'rgb_camera.color_profile',     'default': '0,0,0', 'description': 'color stream profile'},
                           {
    
      'name': 'rgb_camera.color_format',      'default': 'RGB8', 'description': 'color stream format'},
                           {
    
      'name': 'rgb_camera.enable_auto_exposure', 'default': 'true', 'description': 'enable/disable auto exposure for color image'},
                           {
    
      'name': 'enable_depth',                 'default': 'true', 'description': 'enable depth stream'},
                           {
    
      'name': 'enable_infra',                 'default': 'false', 'description': 'enable infra0 stream'},
                           {
    
      'name': 'enable_infra1',                'default': 'false', 'description': 'enable infra1 stream'},
                           {
    
      'name': 'enable_infra2',                'default': 'false', 'description': 'enable infra2 stream'},
                           {
    
      'name': 'depth_module.depth_profile',   'default': '0,0,0', 'description': 'depth stream profile'},
                           {
    
      'name': 'depth_module.depth_format',    'default': 'Z16', 'description': 'depth stream format'},
                           {
    
      'name': 'depth_module.infra_profile',   'default': '0,0,0', 'description': 'infra streams (0/1/2) profile'},
                           {
    
      'name': 'depth_module.infra_format',    'default': 'RGB8', 'description': 'infra0 stream format'},
                           {
    
      'name': 'depth_module.infra1_format',   'default': 'Y8', 'description': 'infra1 stream format'},
                           {
    
      'name': 'depth_module.infra2_format',   'default': 'Y8', 'description': 'infra2 stream format'},
                           {
    
      'name': 'depth_module.exposure',        'default': '8500', 'description': 'Depth module manual exposure value'},
                           {
    
      'name': 'depth_module.gain',            'default': '16', 'description': 'Depth module manual gain value'},
                           {
    
      'name': 'depth_module.hdr_enabled',     'default': 'false', 'description': 'Depth module hdr enablement flag. Used for hdr_merge filter'},
                           {
    
      'name': 'depth_module.enable_auto_exposure', 'default': 'true', 'description': 'enable/disable auto exposure for depth image'},
                           {
    
      'name': 'depth_module.exposure.1',      'default': '7500', 'description': 'Depth module first exposure value. Used for hdr_merge filter'},
                           {
    
      'name': 'depth_module.gain.1',          'default': '16', 'description': 'Depth module first gain value. Used for hdr_merge filter'},
                           {
    
      'name': 'depth_module.exposure.2',      'default': '1', 'description': 'Depth module second exposure value. Used for hdr_merge filter'},
                           {
    
      'name': 'depth_module.gain.2',          'default': '16', 'description': 'Depth module second gain value. Used for hdr_merge filter'},
                           {
    
      'name': 'enable_sync',                  'default': 'false', 'description': "'enable sync mode'"},
                           {
    
      'name': 'enable_rgbd',                  'default': 'false', 'description': "'enable rgbd topic'"},
                           {
    
      'name': 'enable_gyro',                  'default': 'false', 'description': "'enable gyro stream'"},
                           {
    
      'name': 'enable_accel',                 'default': 'false', 'description': "'enable accel stream'"},
                           {
    
      'name': 'gyro_fps',                     'default': '0', 'description': "''"},
                           {
    
      'name': 'accel_fps',                    'default': '0', 'description': "''"},
                           {
    
      'name': 'unite_imu_method',             'default': "0", 'description': '[0-None, 1-copy, 2-linear_interpolation]'},
                           {
    
      'name': 'clip_distance',                'default': '-2.', 'description': "''"},
                           {
    
      'name': 'angular_velocity_cov',         'default': '0.01', 'description': "''"},
                           {
    
      'name': 'linear_accel_cov',             'default': '0.01', 'description': "''"},
                           {
    
      'name': 'diagnostics_period',           'default': '0.0', 'description': 'Rate of publishing diagnostics. 0=Disabled'},
                           {
    
      'name': 'publish_tf',                   'default': 'true', 'description': '[bool] enable/disable publishing static & dynamic TF'},
                           {
    
      'name': 'tf_publish_rate',              'default': '0.0', 'description': '[double] rate in Hz for publishing dynamic TF'},
                           {
    
      'name': 'pointcloud.enable',            'default': 'false', 'description': ''},
                           {
    
      'name': 'pointcloud.stream_filter',     'default': '2', 'description': 'texture stream for pointcloud'},
                           {
    
      'name': 'pointcloud.stream_index_filter','default': '0', 'description': 'texture stream index for pointcloud'},
                           {
    
      'name': 'pointcloud.ordered_pc',        'default': 'false', 'description': ''},
                           {
    
      'name': 'pointcloud.allow_no_texture_points', 'default': 'false', 'description': "''"},
                           {
    
      'name': 'align_depth.enable',           'default': 'false', 'description': 'enable align depth filter'},
                           {
    
      'name': 'colorizer.enable',             'default': 'false', 'description': 'enable colorizer filter'},
                           {
    
      'name': 'decimation_filter.enable',     'default': 'false', 'description': 'enable_decimation_filter'},
                           {
    
      'name': 'spatial_filter.enable',        'default': 'false', 'description': 'enable_spatial_filter'},
                           {
    
      'name': 'temporal_filter.enable',       'default': 'false', 'description': 'enable_temporal_filter'},
                           {
    
      'name': 'disparity_filter.enable',      'default': 'false', 'description': 'enable_disparity_filter'},
                           {
    
      'name': 'hole_filling_filter.enable',   'default': 'false', 'description': 'enable_hole_filling_filter'},
                           {
    
      'name': 'hdr_merge.enable',             'default': 'false', 'description': 'hdr_merge filter enablement flag'},
                           {
    
      'name': 'wait_for_device_timeout',      'default': '-1.', 'description': 'Timeout for waiting for device to connect (Seconds)'},
                           {
    
      'name': 'reconnect_timeout',            'default': '6.', 'description': 'Timeout(seconds) between consequtive reconnection attempts'},
                          ]
# 转成DeclareLaunchArgument生成的对象列表
def declare_configurable_parameters(parameters):
    return [DeclareLaunchArgument(param['name'], default_value=param['default'], description=param['description']) for param in parameters]
# 生成启动参数字典
def set_configurable_parameters(parameters):
    return dict([(param['name'], LaunchConfiguration(param['name'])) for param in parameters])

def yaml_to_dict(path_to_yaml):
    with open(path_to_yaml, "r") as f:
        return yaml.load(f, Loader=yaml.SafeLoader)

def launch_setup(context, params, param_name_suffix=''):
    _config_file = LaunchConfiguration('config_file' + param_name_suffix).perform(context)
    params_from_file = {
    
      } if _config_file == "''" else yaml_to_dict(_config_file)

    _output = LaunchConfiguration('output' + param_name_suffix)
    if(os.getenv('ROS_DISTRO') == 'foxy'):
        # Foxy doesn't support output as substitution object (LaunchConfiguration object)
        # but supports it as string, so we fetch the string from this substitution object
        # see related PR that was merged for humble, iron, rolling: https://github.com/ros2/launch/pull/577
        _output = context.perform_substitution(_output)

    return [
        launch_ros.actions.Node(
            package='realsense2_camera',
            namespace=LaunchConfiguration('camera_namespace' + param_name_suffix),
            name=LaunchConfiguration('camera_name' + param_name_suffix),
            executable='realsense2_camera_node',
            parameters=[params, params_from_file],
            output=_output,
            arguments=['--ros-args', '--log-level', LaunchConfiguration('log_level' + param_name_suffix)],
            emulate_tty=True,
            )
    ]

def generate_launch_description():
    return LaunchDescription(declare_configurable_parameters(configurable_parameters) + [
        OpaqueFunction(function=launch_setup, kwargs = {
    
      'params' : set_configurable_parameters(configurable_parameters)})
    ])

值得注意的是,kwargs里面的键值顺序是要跟launch_setup里面的形参顺序保持一致的,可以少不能多,并且名称要一致。案例中使用yaml定义的参数补充了参数集,这里有一个问题,如果我yaml中和代码中同时定义了一个同名参数,甚至命令行启动时也指定了一个,是否会有冲突呢,是会报错,覆盖?答案是覆盖,并且有优先级,命令行 > yaml > 程序中编写。