ROS2中Launch启动文件配置介绍

一. launch简介

回忆下之前博客中所构造的文件

ROS工作空间、功能包、节点创建_创建自己的功能包与节点-CSDN博客

ROS2话题、服务、动作通讯-CSDN博客

ROS2自定义接口消息、参数服务案例_ros2自定义通信借口-CSDN博客

ROS2质量服务策略Qos & 时间相关API-CSDN博客

编译后,想要启动包里的含有节点的可执行程序,则要命令行一条条执行,比如执行如下几个:

复制代码
ros2 run pythonpackagedemo1 topic_demo1_publisher_side
ros2 run pythonpackagedemo1 topic_demo1_subscriber_side
ros2 run pythonpackagedemo1 pythonexecute_onenode 

但这样太麻烦了,我们需要寻找一种批量打开各程序的方法,那就是Launch启动文件,它是ROS系统中多节点启动与配置的一种脚本。

ROS2 Launch 用于批量启动节点、设置参数、配置命名空间、加载硬件驱动,替代多条独立ros2 run命令,支持多种文件格式:

Python launch 文件(最灵活,可写逻辑判断)

XML launch 文件(简洁,适合简单启动流程)

YAML launch 文件(轻量化,仅基础启动)

二. launch文件编写(py文件)

拿pythonpackagedemo1包做实验,在该包下新建一个文件夹launch, 然后在launch文件夹内新建两个py文件testlaunch.py和sub_launch.py。testlaunch.py文件里会嵌套启用另一个sub_launch.py。

testlaunch.py文件中代码如下:

python 复制代码
from launch import LaunchDescription
from launch_ros.actions import Node
import os
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from ament_index_python.packages import get_package_share_directory
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration

def generate_launch_description():
###################################   本launch文件运行程序所用参数       ##############################################################################################

    # 1. 先声明本launch参数
    arg_name = DeclareLaunchArgument("arg_name", default_value="to_onenode")
    # 2. 创建本launch参数占位对象
    name = LaunchConfiguration("arg_name")

##################################    子launch文件运行程序所用参数         ##################################################################################################

    # 先声明子launch参数
    arg_publish_name = DeclareLaunchArgument("arg_publish_name", default_value="to_publish_node", description="")
    arg_subscribe_name = DeclareLaunchArgument("arg_subscribe_name", default_value="to_subscribe_node",  description="")

    # 创建子launch参数占位对象
    publish_name = LaunchConfiguration("arg_publish_name")      #由于后面调子节点时要用,这边要加载
    subscribe_name = LaunchConfiguration("arg_subscribe_name")


   #嵌套子launch,并把参数传递给子launch ==========
    child_launch = IncludeLaunchDescription(PythonLaunchDescriptionSource(
        [os.path.join(get_package_share_directory('pythonpackagedemo1'), 'launch'),
         '/sub_launch.py']),
        # 将当前launch的参数转发给子launch
        launch_arguments={
            "arg_publish_name": publish_name,     #使用时用占位符名字
            "arg_subscribe_name": subscribe_name  #使用时用占位符名字
        }.items()
    )
######################################################################################################################################################################

    #当前launch加载的可执行程序
    node1 = Node(
        package='pythonpackagedemo1',   # 包名
        executable='pythonexecute_onenode',  # 可执行程序文件名
        name = "tmpnodename",  # 节点重命名
        output='screen',   # 日志打印到终端
        # 把参数作为ROS参数传入节点内部
        parameters=[
            {"arg_name": name},
        ],
    )

    return LaunchDescription([
                              arg_name,                         #调用程序时要传参
                              arg_publish_name,
                              arg_subscribe_name,
                              node1,
                              child_launch
                              ])

sub_launch.py中代码如下:

python 复制代码
from launch import LaunchDescription
from launch_ros.actions import Node
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration

#子launch文件

def generate_launch_description():
    # 接收父launch传递的两个参数
    # 1. 先声明参数
    arg_publish_name = DeclareLaunchArgument("arg_publish_name", default_value="to_publish_node", description="")
    arg_subscribe_name = DeclareLaunchArgument("arg_subscribe_name", default_value="to_subscribe_node",  description="")

    # 2. 创建参数占位对象
    publish_name = LaunchConfiguration("arg_publish_name")
    subscribe_name = LaunchConfiguration("arg_subscribe_name")

    #接下来使用,第1可以赋值给节点名称、命名空间
    # Node(
    #     name=publish_name,
    #     namespace=subscribe_name
    # )

    #接下来使用,第2拼接动态话题名(不能用 f-string,必须列表拼接)
    # remappings = [
    #     ["/topic_demo1", "/robot_", topic_suffix]

    # 接下来使用,第3作为 ROS 参数传入节点内部
    # parameters = [
    #     {"robot_name": robot_name},
    #     {"suffix": topic_suffix}
    # ]
    #

    # 接下来使用,第4传递给子 launch 文件
    # IncludeLaunchDescription(
    #     PythonLaunchDescriptionSource(sub_launch_path),
    #     launch_arguments={
    #         "robot_name": robot_name
    #     }.items()
    # )

    nod_pub = Node(
        package='pythonpackagedemo1',   # 包名
        executable='topic_demo1_publisher_side',  # 可执行程序文件名
        output='screen',   # 日志打印到终端
        parameters=[{"arg_publish_name": publish_name}], #使用时用占位符名字
        remappings=[("/topic_demo1", "/topic_changed")]
    )

    nod_sub = Node(
        package='pythonpackagedemo1',  # 包名
        executable='topic_demo1_subscriber_side',  # 可执行程序文件名
        output='screen',  # 日志打印到终端
        parameters=[{"arg_subscribe_name": subscribe_name}], #使用时用占位符名字
        remappings=[("/topic_demo1", "/topic_changed")]
    )

    return LaunchDescription([
                              arg_publish_name,     #调用应用程序,传参
                              arg_subscribe_name,
                              nod_pub,
                              nod_sub
                              ])

launch文件中的一些格式等要求,在上面的代码中都有注释说明

我们再去修改下启动的py文件中的代码

第1个是pythonexecute1.py, 增加了打印launch方式传进来的参数:

python 复制代码
import rclpy                                     # ROS2 Python接口库
from rclpy.node import Node                      # ROS2 节点类
import time
import threading
from rclpy.time import Time
from rclpy.duration import Duration

"""
创建一个py_hello节点
"""
class pyHelloNode1(Node):
    def __init__(self, name):
        super().__init__(name)

        # 声明需要读取的参数(必须声明,否则读不到)
        # 参数名 和 launch 里的 key 保持一致,第二个是默认值
        self.declare_parameter("arg_name", "hi,pub")
        # 读取参数值
        # 字符串参数 .string_value
        show = self.get_parameter("arg_name").value

        #show = self.get_parameter("arg_name").get_parameter_value().string_value
        # # 整数
        # int_val = self.get_parameter("num").get_parameter_value().integer_value
        # # 浮点数
        # float_val = self.get_parameter("rate").get_parameter_value().double_value
        # # 布尔
        # bool_val = self.get_parameter("enable").get_parameter_value().bool_value

        # 打印验证
        self.get_logger().info(f"读取到launch传入参数arg_name: {show}")

        # 计数器,用于演示定时器执行次数
        self.counter = 0

        # 创建定时器:每1秒执行一次callback函数
        self.timer = self.create_timer(2.0, self.timer_callback)

        # 创建一个更快的定时器:每2秒执行一次
        self.fast_timer = self.create_timer(1.0, self.fast_timer_callback)

        self.get_logger().info("定时器节点已启动")

    def get_time(self):
       return self.get_clock().now()

    def timer_callback(self):
        """2秒定时器回调函数"""
        self.counter += 1
        current_time = self.get_clock().now()

        # 打印当前时间和计数器值
        self.get_logger().info(
            f"[2秒定时器] 第 {self.counter} 次执行,当前时间: {current_time.seconds_nanoseconds()}")

    def fast_timer_callback(self):
        """1秒定时器回调函数"""
        # 打印当前时间戳(纳秒)
        self.get_logger().info(
            f"[1秒定时器] 当前时间戳: {self.get_clock().now().nanoseconds}")

        # ROS2节点父类初始化
        # while rclpy.ok():                          # ROS2系统是否正常运行
        #     self.get_logger().info("Hello python node1")  # ROS2日志输出
        #     time.sleep(0.5)                        # 休眠控制循环时间

    def run_loop(self):
        # 使用节点的create_rate()创建2Hz的Rate
        rate = self.create_rate(2)

        count = 0
        try:
            while rclpy.ok():
                self.get_logger().info(f"循环执行 {count} 次")
                count += 1
                rate.sleep()  # 休眠到下一个周期(0.5秒)
        except KeyboardInterrupt:
            self.get_logger().info("循环被中断")

def main(args=None):
    # ROS2节点主入口main函数
    rclpy.init(args=args)                          # ROS2 Python接口初始化
    node = pyHelloNode1("Hello_python_node1")            # 创建ROS2节点对象并进行初始化

    # time类的使用方法,创建'时间点、时刻'
    time1_demo = Time(seconds=1)
    current_time = node.get_time()
    time.sleep(5)
    duration1 =  node.get_time() - current_time
    t2 = time1_demo  + duration1

    node.get_logger().info(f'duration1: {duration1}')
    node.get_logger().info("t2 = %d" % t2.nanoseconds)

    # 创建线程运行循环(避免阻塞主线程)
    loop_thread = threading.Thread(target=node.run_loop)
    loop_thread.start()

    # 主线程执行spin,维持ROS 2节点运行
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        loop_thread.join()  # 等待线程结束
        node.destroy_node()
        rclpy.shutdown()

第2个是topic_demo1_publisher_side.py,里面的代码如下:

python 复制代码
# 导入rclpy库
import rclpy
from rclpy.node import Node
# 导入String字符串消息
from std_msgs.msg import String
# 导入QoS相关类
from rclpy.qos import QoSProfile, QoSReliabilityPolicy, QoSHistoryPolicy

# 创建一个继承于Node基类的子类
class Topic_Pub(Node):
    def __init__(self, name):
        super().__init__(name)
        # 声明需要读取的参数(必须声明,否则读不到)
        # 参数名 和 launch 里的 key 保持一致,第二个是默认值
        self.declare_parameter("arg_publish_name", "hi,pub")
        # 读取参数值
        # 字符串参数 .string_value
        show = self.get_parameter("arg_publish_name").value
        # 打印验证
        self.get_logger().info(f"读取到launch传入参数 rg_publish_name: {show}")


        # 1. 配置QoS策略:可靠传输,保留最后1条历史数据
        self.qos_profile = QoSProfile(
            reliability=QoSReliabilityPolicy.BEST_EFFORT,  # 可靠传输(重传丢失数据)
            history=QoSHistoryPolicy.KEEP_LAST,  # 保留最后N条数据
            depth=1  # 保留1条历史数据
        )
        # 参数含义: 消息类型, "话题名", 消息队列长度
        self.pub = self.create_publisher(String, "/topic_demo1", self.qos_profile)

        #self.create_timer(1.0, self.timer_callback)
        #意思:每隔1秒执行一次timer_callback
        self.timer = self.create_timer(1, self.pub_msg)

    def pub_msg(self):
        msg = String()  # 创建一个String类型的变量msg
        msg.data = "Hi,I publish a message."  # 给msg里边的data赋值
        self.pub.publish(msg)  # 发布话题数据


# 主函数
def main(args=None):
    rclpy.init()  # 初始化
    pub_demo = Topic_Pub("topic_publisher_node")
    rclpy.spin(pub_demo)
    pub_demo.destroy_node()  # 销毁节点对象
    rclpy.shutdown()  # 关闭ROS2 Python接口

第3个是topic_demo1_subscriber_side.py文件

python 复制代码
#导入相关的库
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
from rclpy.qos import QoSProfile, QoSReliabilityPolicy, QoSHistoryPolicy

class Topic_Sub(Node):
    def __init__(self,name):
        super().__init__(name)
        #self.create_subscription(
        #    msg_type,  # 1. 消息类型(必须)
        #    topic,  # 2. 话题名称(必须)
        #    callback,  # 3. 收到消息后的回调函数(必须)
        #    qos_profile,  # 4. 队列长度(必须)
        #    callback_group=None,  # 5. 回调组(可选)
        #)

        # 4个必须参数(你写代码必写)
        # msg_type 消息类型,如String、Image、LaserScan
        # topic 话题名字字符串,如"my_topic"
        # callback 收到消息后自动调用的函数
        # qos_profile队列长度,一般写10

        # 声明需要读取的参数(必须声明,否则读不到)
        # 参数名 和 launch 里的 key 保持一致,第二个是默认值
        self.declare_parameter("arg_subscribe_name", "hi,sub")
        # 绑定参数变更回调函数
        self.add_on_set_parameters_callback(self.param_callback)

        # 读取参数值
        # 字符串参数 .string_value
        show = self.get_parameter("arg_subscribe_name").value
        # 打印验证
        self.get_logger().info(f"读取到launch传入参数 rg_publish_name: {show}")

        # 1. 配置与发布者兼容的QoS策略
        self.qos_profile = QoSProfile(
            reliability=QoSReliabilityPolicy.BEST_EFFORT,
            history=QoSHistoryPolicy.KEEP_LAST,
            depth=1
        )
        self.sub = self.create_subscription(String,"/topic_demo1",self.sub_callback,self.qos_profile)

    def param_callback(self, params):
        for param in params:
            if param.name == "arg_subscribe_name":
                self.get_logger().info(f"参数更新:{param.string_value}")
        return rclpy.parameter.ParameterSetResult.SUCCESS

    def sub_callback(self,msg):
        # print(msg.data,flush=True)
        self.get_logger().info(msg.data)

def main(args=None):
    rclpy.init()
    sub_demo = Topic_Sub("topic_subscriber_node") # 创建对象并进行初始化
    rclpy.spin(sub_demo)
    sub_demo.destroy_node()  #销毁节点对象
    rclpy.shutdown()         #关闭ROS2 Python接口

三. 2种运行方式

1. 使用默认参数启动

由于launch.py文件中,都有写默认参数,故可直接运行

python 复制代码
ros2 launch pythonpackagedemo1 testlaunch.py

可看到三个可执行程序运行起来了,同时如下默认参数传进来了

2. 命令行自定义传参

python 复制代码
ros2 launch pythonpackagedemo1 testlaunch.py arg_name:=myparent arg_publish_name:=my_child_publishparam arg_subscribe_name:=my_child_subscribeshparam

可看到三个可执行程序运行起来了,同时命令行设置的参数传进来了

**注1:**查看所有可传入参数说明,可使用如下语句

python 复制代码
ros2 launch pythonpackagedemo1 testlaunch.py --show-args

注2: 确认节点是否重命名了**(** 在testlaunch.py中对节点名字进行了修改,见代码注释,本来是Hello_python_node1**)**

注3: 确认话题名字是否重映射了**(** 在子sub_launch.py中对话题做了名字映射**)**

至于使用xml文件和yaml文件方式实现批量运行可执行程序的方式,博主这边就不再介绍了。