ROS2质量服务策略Qos & 时间相关API

一. ROS2质量服务策略Qos

1. 我们在之前博客 基础上做一些实验,首先不修改Qos策略,运行发布端和订阅段的程序,效果如下:

我们改用命令行来发布话题数据

复制代码
ros2 topic pub -r 2 /topic_demo1 std_msgs/msg/String "{data: \"hello world\"}"

命令行订阅话题数据

复制代码
 ros2 topic echo /topic_demo1

效果如下(因为发布端程序和命令行方式都在发送话题,所以订阅端,这两种话题数据都会收到):

如果运行订阅段程序,则效果如下(因为发布端程序和命令行方式都在发送话题,所以订阅端,这两种话题数据都会收到):

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)

        # 1. 配置QoS策略:可靠传输,保留最后1条历史数据
        self.qos_profile = QoSProfile(
            reliability=QoSReliabilityPolicy.RELIABLE,  # 可靠传输(重传丢失数据)
            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():
    rclpy.init()  # 初始化
    pub_demo = Topic_Pub("topic_publisher_node")
    rclpy.spin(pub_demo)
    pub_demo.destroy_node()  # 销毁节点对象
    rclpy.shutdown()  # 关闭ROS2 Python接口

修改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

        # 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 sub_callback(self,msg):
        # print(msg.data,flush=True)
        self.get_logger().info(msg.data)

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

然后执行colcon build,去运行发布端和订阅端程序

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

执行效果如下:

执行命令查看话题详细 QoS 协商结果:

python 复制代码
 ros2 topic info /topic_demo1 --verbose

可看到整条链路降级为 BEST_EFFORT了,丢包不重传;

我们再接着将发布端reliability参数值改为QoSReliabilityPolicy.BEST_EFFORT,订阅端改为RELIABLE,更改完毕后,再编译,执行两个端的可执行程序。

发布端

订阅端

可看到,订阅端是收不到发布的数据的且有提示消息,如下,得修改代码,做到两者保持一致

注:也可以用命令行语句调用时,去做指定修改

命令行修改发布端和订阅端保持一致

python 复制代码
ros2 topic pub -r 2 /topic_demo1 std_msgs/msg/String "{data: \"Hi,I publish a message.\"}" --qos-reliability reliable

或命令行修改订阅端和发布端保持一致

python 复制代码
 ros2 topic echo /topic_demo1 --qos-reliability best_effort

3. QoSProfile 完整可配置字段一共有 10 个:

  1. reliability
  2. history
  3. depth
  4. durability
  5. lifespan
  6. liveliness
  7. deadline
  8. liveliness_lease_duration
  9. max_blocking_time
  10. partition

1)核心枚举策略类参数(4 个,决定通信核心规则)

reliability 可靠性策略 ,控制消息的丢包重传规则 ,是决定通信是否可靠的核心参数,发布 / 订阅两端的取值会直接影响通信兼容性。可选值介绍参考AI工具里介绍

可选值 详细含义 适用场景
QoSReliabilityPolicy.RELIABLE 可靠传输模式:发布方发送消息后,必须等待订阅方返回 ACK 确认包;网络丢包、延迟时会自动重发,保证订阅端 100% 收到消息 你的机器人控制指令(/robot_cmd)、导航目标点、启停命令、紧急停止等绝对不能丢失的关键消息
QoSReliabilityPolicy.BEST_EFFORT 尽力传输模式:无 ACK 确认机制,发布方发完消息就结束,网络差直接丢包不重传,延迟更低但不保证送达 相机图像、激光雷达、IMU 等高频传感器数据流,允许少量丢包,优先保证低延迟

兼容规则如下:

发布 = RELIABLE, 订阅 = BEST_EFFORT:可建立通信,但整条链路降级为 BEST_EFFORT,丢包不重传;

发布 = BEST_EFFORT,订阅 = RELIABLE:完全匹配失败,订阅端收不到任何消息

history 消息缓策略,控制发布 / 订阅端的消息缓冲区如何存放未处理的消息 ,必须搭配depth参数使用,决定了消息的覆盖规则。可选值介绍参考AI工具里介绍

可选值 详细含义 适用场景
QoSHistoryPolicy.KEEP_LAST 仅保留最新的depth条消息,新消息进入缓冲区后,会直接覆盖最旧的消息,内存占用完全可控 你的机器人控制指令、实时传感器数据等只关心最新消息,过时消息无意义的场景,是 90% 场景的首选
QoSHistoryPolicy.KEEP_ALL 缓存所有历史消息,无数量上限,只要缓冲区没满就不会丢弃任何消息 极少使用,仅适合日志、全量数据回放等必须保留所有历史数据的场景,风险是消息处理慢会导致内存持续暴涨

durability 消息持久化策略,控制后上线的订阅节点 ,能否收到发布节点在订阅启动前发送的历史消息,也就是常说的「晚加入者(Late Joiner)」处理规则。可选值介绍参考AI工具里介绍

可选值 详细含义 适用场景
QoSDurabilityPolicy.VOLATILE 非持久化模式:订阅节点启动前发布的所有消息全部丢弃,订阅端只能接收上线后新发布的消息 你的机器人实时运动控制指令、高频传感器数据等旧指令无执行意义的场景,是系统默认值
QoSDurabilityPolicy.TRANSIENT_LOCAL 本地持久化模式:发布节点会在本地缓存历史消息,新订阅节点连接后,会自动补发缓存内的所有消息 静态地图、参数配置、界面状态、固定初始参数等新节点上线需要先获取当前最新状态的场景

liveliness 节点活性保活策略,用于检测发布节点是否在线、崩溃、卡死 ,订阅端可以通过该策略的回调,及时感知发布节点的离线状态,做异常处理。可选值介绍参考AI工具里介绍

可选值 详细含义 适用场景
QoSLivelinessPolicy.AUTOMATIC 自动保活模式:DDS 底层会自动维护心跳包,无需用户代码干预,自动检测节点是否在线 绝大多数常规场景的首选,你的机器人控制指令场景用这个就足够
QoSLivelinessPolicy.MANUAL_BY_TOPIC 手动保活模式:需要用户代码主动调用publish()发送消息,才能维持节点活性;长时间不发送消息,就会被判定为离线 仅适合需要手动控制节点生命周期的进阶场景,比如低功耗设备、按需唤醒的节点

2)数值类参数(1 个,控制缓冲区大小)

depth 消息队列深度,配合history=KEEP_LAST使用,定义消息缓冲区的最大存储消息条数 ,是控制内存占用的核心参数。类型:int 正整数,取值≥1;当history=KEEP_ALL时,该参数完全无效,不会限制队列大小;取值越大,能缓存的历史消息越多,内存占用也越高。

适用场景:你的机器人控制指令:depth=1 最优,只保留最新 1 条指令,旧指令直接被覆盖,避免执行滞后的错误指令;传感器数据:depth=5~10 即可,平衡内存占用和数据容错。

lifespan 消息生命周期,定义单条消息的有效期 ,消息从发布时刻开始计时,超过该时长未被订阅端处理,会直接被丢弃,不会再执行。类型:Duration,取值≥0;取值为 0 时,消息永远不会过期,直到被处理或覆盖。适用场景:你的机器人控制指令:建议设为Duration(seconds=2.0),2 秒前的旧指令自动作废,避免卡顿后执行过时指令;

deadline 消息周期时限,约定该话题的最大消息发布间隔 ,用于检测发布节点是否卡死、断流:发布端:两次发布消息的间隔不能超过该时长;订阅端:超过该时长未收到新消息,会触发「消息丢失」事件,可通过回调捕获。类型:Duration,取值 > 0;取值越小,对发布端的实时性要求越高。适用场景:你的机器人控制指令:如果要求 1 秒必须发一次指令,设为Duration(seconds=1.0),超时未收到可触发紧急停止;传感器数据:比如 IMU 要求 100Hz 发布,设为Duration(milliseconds=10),超时可判定传感器掉线。

liveliness_lease_duration 活性超时时间, 配合liveliness策略使用,定义多久没检测到发布者的心跳,就判定发布节点离线 。类型:Duration,取值 > 0;必须和liveliness策略搭配使用,单独设置无意义。适用场景:你的机器人控制指令:设为Duration(seconds=3.0),3 秒没收到发布者心跳,就判定控制节点离线,可触发紧急停止;常规场景:建议设为发布周期的 2~3 倍,避免误判。

max_blocking_time 最大阻塞等待时间,定义发布缓冲区满时,publish () 方法最多阻塞等待的时长 ,超时仍无法写入缓冲区,就会发布失败,消息直接丢弃。类型:Duration,取值≥0;取值为 0 时,缓冲区满就直接丢弃消息,不阻塞。适用场景:高频发布场景:比如相机图像高频发布,缓冲区偶尔满时,可设为Duration(milliseconds=50),给一点等待时间,避免频繁丢包;你的机器人控制指令:用默认值即可,控制指令发布频率低,缓冲区几乎不会满。

3)分组隔离类参数(1 个,实现多节点通信隔离)

partition 话题分区标签,实现同一话题下的多组隔离通信 ,只有发布端和订阅端的partition列表中存在相同的字符串,才能建立通信,互不干扰。类型:list[str],字符串列表,可填多个分区标签;空列表[]为默认值,属于默认公共分区,所有同话题节点都能互通。适用场景:多机器人集群场景:比如机器人 A 的partition=["robot_01"],机器人 B 的partition=["robot_02"],同是/robot_cmd话题,两个机器人不会收到对方的指令;多模块隔离:比如导航模块和底盘模块用不同分区,避免指令串扰。

4. 常见注意事项:

QoS 匹配是核心 :发布端和订阅端的所有枚举参数必须完全一致,否则会出现协商降级、甚至收不到消息的问题;

depth 和 history 必须搭配 :只有history=KEEP_LAST时,depth 才有效,不要设了 KEEP_ALL 还改 depth;

时间参数不要设的过短:比如 deadline 设的比发布周期还短,会频繁触发断流误判;

分区不要乱填:一旦填了 partition,订阅端必须有相同的标签才能收到消息,否则会完全收不到。

二. 时间相关API

我们拿如下博客中的例子来做实验

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

编辑pythonexecute1.py文件代码如下:

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)

        # 计数器,用于演示定时器执行次数
        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()

然后重新colcon build, 执行该可执行程序,效果如下:

python 复制代码
ros2 run pythonpackagedemo1 pythonexecute_onenode 

此篇到此结束,我们再继续深入!