launch 文件不是“机器人控制代码”,它是“启动脚本”:负责一次性启动多个 ROS 节点、传参数、选择真机/仿真、加载控制器。
1.最小的ROS2 lauch长什么样子
from launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): node = Node( package="包名", executable="可执行程序名", name="节点名", output="screen", ) return LaunchDescripition([Node])generate_Launch_description()是ROS2launch的入口函数
LaunchDescription(【】)里面放的是要执行的动作
Node()表示启动一个ROS节点
2.下面是我的一段工程代码,接下来我根据这份代码来进行讲解
import os import sys from ament_index_python.packages import get_package_share_directory from launch import LaunchDescription from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription from launch.conditions import IfCondition, UnlessCondition from launch.launch_description_sources import PythonLaunchDescriptionSource from launch.substitutions import ( Command, FindExecutable, LaunchConfiguration, PathJoinSubstitution, PythonExpression, ) from launch_ros.actions import Node from launch_ros.parameter_descriptions import ParameterValue from launch_ros.substitutions import FindPackageShare sys.path.append(os.path.join(get_package_share_directory("tide_ctrl_bringup"), "launch")) from controller import engineer_controllers def generate_launch_description(): bringup_dir = get_package_share_directory("tide_ctrl_bringup") sim_mode = LaunchConfiguration("sim_mode") declare_sim_mode = DeclareLaunchArgument( "sim_mode", default_value="true", description="true: MuJoCo simulation, false: real robot via STM32 serial bridge", ) mujoco_headless = LaunchConfiguration("mujoco_headless") declare_mujoco_headless = DeclareLaunchArgument( "mujoco_headless", default_value="false", description="Run MuJoCo without visualization window if true", ) remote_control = LaunchConfiguration("remote_control") declare_remote_control = DeclareLaunchArgument( "remote_control", default_value="true", description="Start VT13 remote control mapper on the real robot", ) use_sim_time = LaunchConfiguration("use_sim_time") declare_use_sim_time = DeclareLaunchArgument( "use_sim_time", default_value=sim_mode, description="Use simulation clock if true", ) real_xacro_file = PathJoinSubstitution( [bringup_dir, "description", "engineer", "engineer_real.xacro"] ) mujoco_xacro_file = PathJoinSubstitution( [bringup_dir, "description", "engineer", "engineer_mujoco.xacro"] ) real_robot_description = ParameterValue( Command([FindExecutable(name="xacro"), " ", real_xacro_file]), value_type=str, ) mujoco_robot_description = ParameterValue( Command( [ FindExecutable(name="xacro"), " ", mujoco_xacro_file, " headless:=", mujoco_headless, ] ), value_type=str, ) real_controller_config = PathJoinSubstitution( [bringup_dir, "config", "engineer", "engineer_real.yaml"] ) mujoco_controller_config = PathJoinSubstitution( [bringup_dir, "config", "engineer", "engineer_mujoco.yaml"] ) remote_control_config = PathJoinSubstitution( [bringup_dir, "config", "engineer", "remote_control.yaml"] ) real_state_publisher = Node( package="robot_state_publisher", executable="robot_state_publisher", name="robot_state_publisher", output="screen", parameters=[ {"use_sim_time": use_sim_time, "robot_description": real_robot_description} ], condition=UnlessCondition(sim_mode), ) mujoco_state_publisher = Node( package="robot_state_publisher", executable="robot_state_publisher", name="robot_state_publisher", output="screen", parameters=[ {"use_sim_time": use_sim_time, "robot_description": mujoco_robot_description} ], condition=IfCondition(sim_mode), ) real_control_node = Node( package="controller_manager", executable="ros2_control_node", parameters=[ { "use_sim_time": use_sim_time, "robot_description": real_robot_description, }, real_controller_config, ], output="screen", condition=UnlessCondition(sim_mode), ) mujoco = IncludeLaunchDescription( PythonLaunchDescriptionSource( [ PathJoinSubstitution( [ FindPackageShare("tide_mujoco"), "launch", "mujoco_start.launch.py", ] ) ] ), launch_arguments={ "use_sim_time": use_sim_time, "controller_config": mujoco_controller_config, }.items(), condition=IfCondition(sim_mode), ) vt13_remote_control = Node( package="tide_hw_interface", executable="vt13_chassis_remote_node", name="vt13_chassis_remote_node", output="screen", parameters=[remote_control_config], condition=IfCondition( PythonExpression(["'", remote_control, "' == 'true' and '", sim_mode, "' == 'false'"]) ), ) return LaunchDescription( [ declare_sim_mode, declare_mujoco_headless, declare_remote_control, declare_use_sim_time, real_state_publisher, mujoco_state_publisher, real_control_node, mujoco, engineer_controllers(), vt13_remote_control, ] )整体逻辑是:
读取 launch 参数
找到 xacro / yaml 配置文件
根据 sim_mode 判断启动真机还是仿真
启动 robot_state_publisher
启动 ros2_control_node 或 MuJoCo
启动 controller spawner
如果是真机且启用遥控,启动 VT13 遥控节点
最关键入口:
def generate_launch_description():
ROS执行
ros2 launch tide_ctrl_bringup tide_ctrl_bringup.launch.py时会调用这个函数。
3.launch参数:运行时的开关
sim_mode = LaunchConfiguration("sim_mode")意思是:我要读取一个叫sim_mode的launch参数
DeclareLaunchArgument(...)意思是:声明这个参数,并给默认值。
可以这样启动仿真和真机:
ros2 launch tide_ctrl_bringup tide_ctrl_bringup.launch.py sim_mode:=true ros2 launch tide_ctrl_bringup tide_ctrl_bringup.launch.py sim_mode:=false4.找文件路径:PathJoinSubstitution
real_controller_config = PathJoinSubstitution( [bringup_dir, "config", "engineer", "engineer_real.yaml"] )本质等价于拼接路径
tide_ctrl_bringup/config/engineer/engineer_real.yamlROS launch 推荐用PathJoinSubstitution,因为它可以和 launch 参数、包路径一起动态解析。
5.xacro转robot_description
real_robot_description = ParameterValue( Command([FindExecutable(name="xacro"), " ", real_xacro_file]), value_type=str, )意思是:执行xacro enginner_real.xacro
把生成结构作为字符串参数 robot_description
ROS里面很多节点都需要知道机器人模型,比如robot_state_publisher,ros2_control_node
它们都需要robot_description
所以后面会传进去:
parameters=[ {"use_sim_time": use_sim_time, "robot_description": real_robot_description} ]这里的robot_description可以理解成机器人完整的urdf模型文本
6.启动节点
比如真机的robot_state_publisher:
real_state_publisher = Node( package="robot_state_publisher", executable="robot_state_publisher", name="robot_state_publisher", output="screen", parameters=[ {"use_sim_time": use_sim_time, "robot_description": real_robot_description} ], condition=UnlessCondition(sim_mode), )package:节点来自哪个ROS包
executable:启动哪个可执行程序
name:节点名字
output="screen":日志打印到终端
parameters:传入ROS参数
conditon:满足条件才启动
condition=UnlessCondition(sim_mode)这句话的意思是sim_mode为false时启动,所以这个是真机专用
7.真机和仿真的分支
真机路径:
real_state_publisher
real_control_node
vt13_remote_control
engineer_controllers()
仿真路径:
mujoco_state_publisher
mujoco
engineer_controllers()
engineer_controllers()目前是无条件执行的,他会在真机和仿真中都启动这些controller:
joint_state_broadcaster chassis_controller arm_controller gripper_controller8.IncludeLaunchDescription:包含另一个launch
mujoco = IncludeLaunchDescription( PythonLaunchDescriptionSource( [ PathJoinSubstitution( [ FindPackageShare("tide_mujoco"), "launch", "mujoco_start.launch.py", ] ) ] ), launch_arguments={ "use_sim_time": use_sim_time, "controller_config": mujoco_controller_config, }.items(), condition=IfCondition(sim_mode), )这段的意思是:
如果sim_mode=true,就去启动tide_mujoco包里的mujoco_start.laucnh.py
并传入两个参数进去:
"use_sim_time": use_sim_time
"controller_config": mujoco_controller_config
被包含的文件的接收方式是:
controller_config = LaunchConfiguration("controller_config") use_sim_time = LaunchConfiguration("use_sim_time")9.controller.py
def _spawner(name): return Node( package="controller_manager", executable="spawner", arguments=[ name, "--controller-manager", "/controller_manager", ], )这不是普通控制节点,而是执行命令
ros2 run controller_manager spawner chassis_controller --controller-manager /controller_manager它的作用是:让controller_manager加载并启动controller
def engineer_controllers(): return GroupAction( [ _spawner("joint_state_broadcaster"), _spawner("chassis_controller"), _spawner("arm_controller"), _spawner("gripper_controller"), ] )然后这里一次启动了四个
10.YAML和spawner的关系
YAML配置了controller
spawner启动了controller
例如
controller_manager: ros__parameters: chassis_controller: type: "mecanum_drive_controller/MecanumDriveController"这只是告诉controller_manager:
我有一个 controller 叫 chassis_controller,它的类型是 MecanumDriveController但它不会自动启动。
真正启动它的是:
_spawner("chassis_controller")所以完整流程是:
ros2_control_node 读取 YAML controller_manager 知道有哪些 controller spawner 请求 controller_manager 启动 controller11.以后需要新增一个节点
my_node = Node( package="my_package", executable="my_node", name="my_node", output="screen", parameters=[ {"use_sim_time": use_sim_time} ], )然后一定要放进:
return LaunchDescription( [ ... my_node, ] )否则你只是定义了变量,没有真正启动
12.以后需要新增一个YAML配置
模板:
my_config = PathJoinSubstitution( [bringup_dir, "config", "engineer", "my_config.yaml"] ) my_node = Node( package="my_package", executable="my_node", parameters=[my_config], )对应 YAML 结构通常是:
my_node: ros__parameters: param_a: 1 param_b: "hello"注意:最外层my_node要和节点名匹配。
remote_control.yaml就是这样:
vt13_chassis_remote_node: ros__parameters: joy_topic: "/astra/remote/joy"因为 launch 里节点名是:
name="vt13_chassis_remote_node"13. 你以后要新增一个 controller
分三步。
第一步,在 YAML 里声明 controller:
controller_manager: ros__parameters: my_controller: type: "some_controller/SomeController"第二步,给它写参数:
my_controller: ros__parameters: some_param: 123第三步,在controller.py里加 spawner:
_spawner("my_controller")这就是 controller 的标准套路
结语:
第一次写这么长的东西,本人目前也还在学习当中,如果有哪里讲的不对,还请各位大佬们指出