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_link → laser_frame,z 方向抬高 8 厘米,
和我需要的变换一模一样。话题里确实有这条消息。
但 RViz 收不到,tf2_echo base_link laser_frame 也拿不到。
所以问题不是「数据对不对」,而是「数据有没有被送到」。
02答案藏在一行被我跳过的 WARN 里
回头翻启动日志,找到了它:
[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」这两条线索单独提出来讲。原始笔记里它们混在排查过程里, 其实它们是判断「这个发布者会不会自己消失」最快的依据。
留言
正在载入留言…