ROS2与launch入门教程-使用ROS2 launch文件启动大型项目
ROS2与launch入门教程-使用ROS2 launch文件启动大型项目
说明:
- 学习使用ROS 2 launch文件管理大型项目的最佳实践
背景:
- 本教程描述了为大型项目编写启动文件的一些技巧。
- 重点是如何构建launch文件,以便在不同情况下尽可能多地重用它们。
- 此外,它还涵盖了不同 ROS 2 启动工具的使用示例,例如参数、YAML 文件、重新映射、命名空间、默认参数和 RViz 配置。
介绍:
- 机器人上的大型应用程序通常涉及多个相互连接的节点,每个节点都可以有许多参数。
- 在海龟模拟器中模拟多只海龟就是一个很好的例子。
- 海龟模拟由多个海龟节点、world配置以及 TF 广播器和侦听器节点组成。
- 在所有节点之间,存在大量影响这些节点行为和外观的 ROS 参数。
- ROS 2 launch文件允许我们在一个地方启动所有节点并设置相应的参数。
- 在教程结束时,您将在 launch_tutorial 包中构建 launch_turtlesim.launch.py 启动文件。
- 这个启动文件会调出不同的节点,负责模拟两个turtlesim模拟,启动TF广播器和监听器,加载参数,启动一个RViz配置。
- 在本教程中,我们将介绍这个启动文件和使用的所有相关功能。
顶层结构
- 编写启动文件过程中的目标之一应该是使它们尽可能可重用。
- 这可以通过将相关节点和配置集群到单独的启动文件中来完成。
- 之后,可以编写专用于特定配置的顶级启动文件。
- 这将允许在完全不更改启动文件的情况下在相同的机器人之间移动。
- 即使是从真正的机器人移动到模拟机器人这样的变化,也只需进行少量更改即可完成。
- 我们现在将讨论使这成为可能的顶级启动文件结构。
- 首先,我们将创建一个launch文件,该文件将调用单独的启动文件。
- 为此,让我们在 launch_tutorial 包的 /launch 文件夹中创建一个 launch_turtlesim.launch.py 文件。
cd ~/launch_ws/src/launch_tutorial/launch
vim launch_turtlesim.launch.py
- 内容如下:
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
def generate_launch_description():
turtlesim_world_1 = IncludeLaunchDescription(
PythonLaunchDescriptionSource([os.path.join(
get_package_share_directory('launch_tutorial'), 'launch'),
'/turtlesim_world_1.launch.py'])
)
turtlesim_world_2 = IncludeLaunchDescription(
PythonLaunchDescriptionSource([os.path.join(
get_package_share_directory('launch_tutorial'), 'launch'),
'/turtlesim_world_2.launch.py'])
)
broadcaster_listener_nodes = IncludeLaunchDescription(
PythonLaunchDescriptionSource([os.path.join(
get_package_share_directory('launch_tutorial'), 'launch'),
'/broadcaster_listener.launch.py']),
launch_arguments={'target_frame': 'carrot1'}.items(),
)
mimic_node = IncludeLaunchDescription(
PythonLaunchDescriptionSource([os.path.join(
get_package_share_directory('launch_tutorial'), 'launch'),
'/mimic.launch.py'])
)
fixed_frame_node = IncludeLaunchDescription(
PythonLaunchDescriptionSource([os.path.join(
get_package_share_directory('launch_tutorial'), 'launch'),
'/fixed_broadcaster.launch.py'])
)
rviz_node = IncludeLaunchDescription(
PythonLaunchDescriptionSource([os.path.join(
get_package_share_directory('launch_tutorial'), 'launch'),
'/turtlesim_rviz.launch.py'])
)
return LaunchDescription([
turtlesim_world_1,
turtlesim_world_2,
broadcaster_listener_nodes,
mimic_node,
fixed_frame_node,
rviz_node
])
- 此启动文件包括一组其他启动文件。
- 这些包含的启动文件中的每一个都包含节点、参数和可能的嵌套包含,它们与系统的一个部分有关。
- 确切地说,我们推出了两个turtlesim 模拟世界,TF 广播器、TF 监听器、模仿器、固定帧广播器和 RViz 节点。
- 设计提示:顶级启动文件应该很短,包含与应用程序的子组件相对应的其他文件的包含,以及经常更改的参数。
- 用以下方式编写启动文件可以很容易地换出系统的一部分,我们稍后会看到。
- 但是,由于性能和使用原因,某些节点或启动文件必须单独启动。
- 设计提示:在决定您的应用程序需要多少顶级启动文件时,请注意权衡取舍。
参数:
- 我们将首先编写一个启动文件,该文件将启动我们的第一个 turtlesim 模拟。
- 1.首先,创建一个名为 turtlesim_world_1.launch.py 的新文件
cd ~/launch_ws/src/launch_tutorial/launch
vim turtlesim_world_1.launch.py
- 内容如下:
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration, TextSubstitution
from launch_ros.actions import Node
def generate_launch_description():
background_r_launch_arg = DeclareLaunchArgument(
'background_r', default_value=TextSubstitution(text='0')
)
background_g_launch_arg = DeclareLaunchArgument(
'background_g', default_value=TextSubstitution(text='84')
)
background_b_launch_arg = DeclareLaunchArgument(
'background_b', default_value=TextSubstitution(text='122')
)
return LaunchDescription([
background_r_launch_arg,
background_g_launch_arg,
background_b_launch_arg,
Node(
package='turtlesim',
executable='turtlesim_node',
name='sim',
parameters=[{
'background_r': LaunchConfiguration('background_r'),
'background_g': LaunchConfiguration('background_g'),
'background_b': LaunchConfiguration('background_b'),
}]
),
])
- 此启动文件启动 turtlesim_node 节点,该节点启动 turtlesim 模拟,模拟配置参数已定义并传递给节点。
- 2.从YAML 文件加载参数
- 在第二次启动中,我们将使用不同的配置启动第二次 turtlesim 模拟。
- 现在创建一个 turtlesim_world_2.launch.py 文件。
cd ~/launch_ws/src/launch_tutorial/launch
vim turtlesim_world_2.launch.py
- 内容如下:
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
config = os.path.join(
get_package_share_directory('launch_tutorial'),
'config',
'turtlesim.yaml'
)
return LaunchDescription([
Node(
package='turtlesim',
executable='turtlesim_node',
namespace='turtlesim2',
name='sim',
parameters=[config]
)
])
- 此启动文件将使用直接从 YAML 配置文件加载的参数值启动相同的 turtlesim_node。
- 在 YAML 文件中定义参数和参数可以轻松存储和加载大量变量。
- 此外,YAML 文件可以很容易地从当前的 ros2 参数列表中导出。
- 要了解如何执行此操作,请参阅了解 ROS 2 参数教程。
- 现在让我们在包的 /config 文件夹中创建一个配置文件,turtlesim.yaml,它将由我们的启动文件加载。
mkdir -p ~/launch_ws/src/launch_tutorial/config
cd ~/launch_ws/src/launch_tutorial/config
vim turtlesim.yaml
- 内容如下:
/turtlesim2/sim:
ros__parameters:
background_b: 255
background_g: 86
background_r: 150
如果我们现在启动 turtlesim_world_2.launch.py 启动文件,我们将使用预配置的背景颜色启动 turtlesim_node。
要了解有关使用参数和使用 YAML 文件的更多信息,请查看了解 ROS 2 参数教程。
3.在 YAML 文件中使用通配符
有时我们想在多个节点中设置相同的参数。
这些节点可能具有不同的命名空间或名称,但仍具有相同的参数。
定义显式定义命名空间和节点名称的单独 YAML 文件效率不高。
一种解决方案是使用通配符,作为文本值中未知字符的替换,将参数应用于几个不同的节点。
现在让我们创建一个类似于turtlesim_world_2.launch.py 的新turtlesim_world_3.launch.py 文件,以包含更多的turtlesim_node 节点。
cd ~/launch_ws/src/launch_tutorial/launch
vim turtlesim_world_3.launch.py
- 增加如下内容
...
Node(
package='turtlesim',
executable='turtlesim_node',
namespace='turtlesim3',
name='sim',
parameters=[config]
)
- 然而,加载相同的 YAML 文件不会影响第三个 turtlesim 世界的外观。
- 原因是它的参数存储在另一个命名空间下,如下所示:
/turtlesim3/sim:
background_b
background_g
background_r
- 因此,我们可以使用通配符语法,而不是为使用相同参数的同一节点创建新配置。
- /** 将分配每个节点中的所有参数,尽管节点名称和命名空间不同。
- 我们现在将按照以下方式更新 /config 文件夹中的 turtlesim.yaml:
/**:
ros__parameters:
background_b: 255
background_g: 86
background_r: 150
- 现在在我们的主启动文件中包含 turtlesim_world_3.launch.py 启动描述。
- 在我们的启动描述中使用该配置文件会将 background_b、background_g 和 background_r 参数分配给 turtlesim3/sim 和 turtlesim2/sim 节点中的指定值。
命名空间
- 您可能已经注意到,我们在 turtlesim_world_2.launch.py 文件中定义了 turlesim 世界的命名空间。
- 唯一的命名空间允许系统启动两个相似的节点,而不会出现节点名或主题名冲突。
namespace='turtlesim2',
但是,如果启动文件包含大量节点,则为每个节点定义命名空间可能会变得乏味。
为了解决这个问题,可以使用 PushRosNamespace 操作为每个启动文件描述定义全局命名空间。
每个嵌套节点都会自动继承该命名空间。
为此,首先,我们需要从 turtlesim_world_2.launch.py 文件中删除 namespace='turtlesim2' 行。
之后,我们需要更新 launch_turtlesim.launch.py 以包含以下行:
from launch.actions import GroupAction
from launch_ros.actions import PushRosNamespace
...
turtlesim_world_2 = IncludeLaunchDescription(
PythonLaunchDescriptionSource([os.path.join(
get_package_share_directory('launch_tutorial'), 'launch'),
'/turtlesim_world_2.launch.py'])
)
turtlesim_world_2_with_namespace = GroupAction(
actions=[
PushRosNamespace('turtlesim2'),
turtlesim_world_2,
]
)
- 最后,我们在 return LaunchDescription 语句中将 turtlesim_world_2 替换为 turtlesim_world_2_with_namespace。
- 因此,turtlesim_world_2.launch.py 启动描述中的每个节点都会有一个 turtlesim2 命名空间。
重用节点
- 现在创建一个 broadcaster_listener.launch.py 文件。
cd ~/launch_ws/src/launch_tutorial/launch
vim broadcaster_listener.launch.py
- 内容如下:
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
DeclareLaunchArgument(
'target_frame', default_value='turtle1',
description='Target frame name.'
),
Node(
package='turtle_tf2_py',
executable='turtle_tf2_broadcaster',
name='broadcaster1',
parameters=[
{'turtlename': 'turtle1'}
]
),
Node(
package='turtle_tf2_py',
executable='turtle_tf2_broadcaster',
name='broadcaster2',
parameters=[
{'turtlename': 'turtle2'}
]
),
Node(
package='turtle_tf2_py',
executable='turtle_tf2_listener',
name='listener',
parameters=[
{'target_frame': LaunchConfiguration('target_frame')}
]
),
])
- 在这个文件中,我们用默认值turtle1 声明了target_frame 启动参数。
- 默认值意味着启动文件可以接收一个参数转发给它的节点,或者如果没有提供参数,它会将默认值传递给它的节点。
- 之后,我们在启动期间使用不同的名称和参数两次使用 turtle_tf2_broadcaster 节点。
- 这允许我们复制同一个节点而不会发生冲突。
- 我们还启动了一个 turtle_tf2_listener 节点并设置了我们在上面声明和获取的 target_frame 参数。
参数覆盖
- 回想一下,我们在顶级启动文件中调用了 broadcaster_listener.launch.py 文件。
- 除此之外,我们还向它传递了 target_frame 启动参数,如下所示:
broadcaster_listener_nodes = IncludeLaunchDescription(
PythonLaunchDescriptionSource([os.path.join(
get_package_share_directory('launch_tutorial'), 'launch'),
'/broadcaster_listener.launch.py']),
launch_arguments={'target_frame': 'carrot1'}.items(),
)
- 此语法允许我们将默认目标目标框架更改为carrot1。
- 如果您希望turtle2 跟随turtle1 而不是carrot1,只需删除定义launch_arguments 的行。
- 这将为 target_frame 分配其默认值,即 turtle1。
重新映射
- 现在创建一个mimic.launch.py 文件
cd ~/launch_ws/src/launch_tutorial/launch
vim mimic.launch.py
- 内容如下:
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='turtlesim',
executable='mimic',
name='mimic',
remappings=[
('/input/pose', '/turtle2/pose'),
('/output/cmd_vel', '/turtlesim2/turtle1/cmd_vel'),
]
)
])
- 该启动文件将启动模拟节点,该节点将向一个turtlesim 发出命令以跟随另一个。
- 该节点旨在接收话题/输入/姿势上的目标姿势。
- 在我们的例子中,我们想要从 /turtle2/pose 话题重新映射目标姿势。
- 最后,我们将 /output/cmd_vel 话题重新映射到 /turtlesim2/turtle1/cmd_vel。
- 这样,我们的turtlesim2 模拟世界中的turtle1 将跟随我们最初的turtlesim 世界中的turtle2。
配置文件
- 现在让我们创建一个名为 turtlesim_rviz.launch.py 的文件
cd ~/launch_ws/src/launch_tutorial/launch
vim turtlesim_rviz.launch.py
- 内容如下:
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
rviz_config = os.path.join(
get_package_share_directory('turtle_tf2_py'),
'rviz',
'turtle_rviz.rviz'
)
return LaunchDescription([
Node(
package='rviz2',
executable='rviz2',
name='rviz2',
arguments=['-d', rviz_config]
)
])
- 此启动文件将使用在 turtle_tf2_py 包中定义的配置文件启动 RViz。
- 此 RViz 配置将设置世界框架,启用 TF 可视化,并以自上而下的视图启动 RViz。
环境变量
- 现在让我们在包中创建最后一个名为 fixed_broadcaster.launch.py 的启动文件
cd ~/launch_ws/src/launch_tutorial/launch
vim fixed_broadcaster.launch.py
- 内容如下:
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import EnvironmentVariable, LaunchConfiguration
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
DeclareLaunchArgument(
'node_prefix',
default_value=[EnvironmentVariable('USER'), '_'],
description='prefix for node name'
),
Node(
package='turtle_tf2_py',
executable='fixed_frame_tf2_broadcaster',
name=[LaunchConfiguration('node_prefix'), 'fixed_broadcaster'],
),
])
- 该启动文件显示了在启动文件中可以调用环境变量的方式。
- 环境变量可用于定义或推送命名空间,以区分不同计算机或机器人上的节点。
重新编译:
- 打开 setup.py 并添加以下行,以便安装 launch/ 文件夹中的启动文件和 config/ 中的配置文件。
cd ~/launch_ws/src/launch_tutorial/
vim setup.py
- data_files 字段现在应该如下所示:
data_files=[
...
(os.path.join('share', package_name, 'launch'),
glob(os.path.join('launch', '*.launch.py'))),
(os.path.join('share', package_name, 'config'),
glob(os.path.join('config', '*.yaml'))),
],
- 指定构建launch_tutorial 包,并使用symlink的方式
cd ~/launch_ws/
colcon build --symlink-install --packages-select launch_tutorial
- 加载工作空间
. ~/launch_ws/install/local_setup.bash
- 要最终查看我们的代码的结果,请构建包并使用以下命令启动顶级启动文件:
ros2 launch launch_tutorial launch_turtlesim.launch.py
- 您现在将看到两个turtlesim 模拟已启动。
- 第一个有两只乌龟,第二个有一只。
- 在第一个模拟中,turtle2 生成在世界的左下角。 它的目标是到达相对于turtle1 框架在x 轴上5 米远的carrot1 框架。
- 第二个中的turtlesim2/turtle1 旨在模仿turtle2 的行为。
- 如果要控制turtle1,请运行teleop 节点。
ros2 run turtlesim turtle_teleop_key
- 效果图:
- 除此之外,RViz 应该已经启动。
- 它将显示相对于世界框架的所有海龟框架,其原点位于左下角。
获取最新文章: 扫一扫右上角的二维码加入“创客智造”公众号