RViz 里激光点云画得好好的,我随手把那个终端关掉了,点云还在。

这不对。RViz 里的东西都有来源:要么来自某个正在跑的节点,要么来自它自己缓存的一份。 我把发布者杀了,画面却纹丝不动——说明我看到的那份数据,根本不是我以为的那个发布者给的。

顺着这条线查下去,我撞上了 ROS 2 里最容易忽略、也最容易把人困住的一块: QoS(服务质量策略)。它造成的问题有个共同特征—— 数据明明在那儿,你的订阅者就是收不到,而且全程几乎不报错

01先出现的怪事:能看到,却用不上

更早的时候就有征兆。RViz 一直报某个 frame 不存在,我以为是 TF 树没建好, 于是直接去翻话题:

终端
$ ros2 topic echo /tf_static --once
    transforms:
    - header:
        stamp:
          sec: 0
          nanosec: 0
        frame_id: base_link
      child_frame_id: laser_frame
      transform:
        translation: {x: 0.0, y: 0.0, z: 0.08}
        rotation: {x: 0.0, y: 0.0, z: 0.0, w: 1.0}
    ---

内容完全正确:base_linklaser_frame,z 方向抬高 8 厘米, 和我需要的变换一模一样。话题里确实有这条消息

但 RViz 收不到,tf2_echo base_link laser_frame 也拿不到。 所以问题不是「数据对不对」,而是「数据有没有被送到」。

02答案藏在一行被我跳过的 WARN 里

回头翻启动日志,找到了它:

rviz2 启动日志
[WARN] [...] New publisher discovered on topic '/tf_static',
           offering incompatible QoS. No messages will be sent to it.
           Last incompatible policy: DURABILITY_QOS_POLICY

这行警告说得很直白,只是我当时没读懂:「发现了一个新的发布者,但它提供的 QoS 不兼容, 不会向它发送任何消息。不匹配的策略是 DURABILITY。」

这里的措辞有点绕,但结果是清楚的:RViz 不是「没收到」,而是主动拒绝接收

03Durability:这两台设备对「历史」的看法不同

Durability(持久性)解决的是一个问题:订阅者来晚了,还能不能拿到之前发过的消息?

策略 含义 谁在用
volatile 只收「我订阅之后」发出的消息,不看历史 RViz、tf2_echo 的默认值
transient_local 消息会「存」在话题里,后来的订阅者能补收到 我那个雷达驱动发布的 /tf_static

ROS 2 的规则很硬:发布者和订阅者的 Durability 必须完全一致,才允许通信。 一深一浅,就不匹配,消息根本不会发出去。所以哪怕话题里躺着那条变换, RViz 的 TF 缓存里也永远不会有它。

04第二个因素:发布者「打一枪就走了」

只到这一步还不够——我又去量了一下那个话题的发布频率,发现了第二件事:

频率与时间戳
average rate: 0.35        # 大约 3 秒一次
    stamp: sec: 0, nanosec: 0 # 时间戳是 0

0.35 Hz,而且时间戳是 0。这两个特征凑在一起通常意味着: 发布者并不是一个常驻节点,而是某个驱动在启动时发了一次静态变换,然后就退出了 (或者进了休眠)。

静态变换按定义只发一次就够了——前提是订阅者用 transient_local 能补收到。 可一旦遇到 volatile 的订阅者,这次发布就彻底错过了:发布时你还没来,你来时它已经走了

而 RViz 是在驱动之后才启动的。两件事凑齐,就成了死局。

05为什么手敲一行命令就好了

手动发布静态变换
ros2 run tf2_ros static_transform_publisher \
      --x 0 --y 0 --z 0.08 --roll 0 --pitch 0 --yaw 0 \
      --frame-id base_link --child-frame-id laser_frame

这行命令一跑,RViz 立刻正常。它成功的原因,正好是前面两个失败原因的镜像:

  • 它用默认的 volatile 策略——和 RViz、tf2_echo 完全匹配, 「语言」相通,消息才发得出去。
  • 它一直在运行,大约每秒重发一次。无论 RViz 什么时候打开,都能等到下一次推送, 不存在「来晚了」的问题。

把两条路并排放一下,问题就非常清楚了:

驱动发的 /tf_static 手动命令
Durability transient_local → 不匹配 volatile → 匹配
发布行为 0.35 Hz,发完就走 持续约 1 Hz
RViz 能收到吗 拒绝接收 正常接收
结果 frame does not exist 点云正常显示

06如果一定要用原来那个发布者

把订阅端的 QoS 调成 transient_local,就能和它对上:

让订阅端去迁就
ros2 run tf2_ros tf2_echo base_link laser_frame --qos-durability transient_local

RViz 也可以在话题配置里把 Durability 改掉。但如果那个发布者是别人写好的驱动, 改自己这边更省事。反过来,如果两边都能改, 标准做法还是用 static_transform_publisher 持续发布—— 它对任何订阅者都友好,不要求对方懂你的策略。

一个判断规则:静态变换只用发一次,是建立在「订阅者能补收到历史」这个前提上的。 只要链路上有任何一个订阅者是 volatile,这个前提就不成立。 与其去猜别人用什么策略,不如老老实实持续发。

07这件事教我的

  • 「话题里有消息」和「我收得到」是两件事。 ros2 topic echo 能用,只证明能和发布者对上, 不证明 RViz 或你的代码也能。这是我当时最想不通的地方。
  • WARN 要读,尤其是提到 QoS 的。 那行 offering incompatible QoS 安静地躺在启动日志里, 它其实已经把答案说完了。我跳过了它,于是多花了几个小时。
  • 时间戳是 0 的静态变换,是个信号。 它说明这条消息被设计成「发一次就够」。看到这种设计, 就要立刻问一句:那我的订阅者能不能补收到?
关于本文
整理自 2026 年 8 月 30 日的排查记录。当时的现象是停掉手动发布的 TF 之后, 点云画面依旧存在——顺着这条线才把 QoS 不匹配的问题挖出来。
2026.09.20 整理
把「0.35 Hz + 时间戳为 0」这两条线索单独提出来讲。原始笔记里它们混在排查过程里, 其实它们是判断「这个发布者会不会自己消失」最快的依据。