Skip to content

Wiki

开发问题与排查

About 26 min read

An English translation is not available yet. The latest Simplified Chinese source is shown without triggering paid translation.

1.开发问题与排查

1.1.SSH 连接 ysu.local 时解析到 MID360 网口地址

1.1.1.原因

现象是:电脑与工控机连接同一个 Wi-Fi,直接 ping 工控机的 Wi-Fi 地址 192.168.2.183 正常,也能通过这个地址 SSH 登录;但使用 ssh ysu@ysu.local 时,主机名却解析到 192.168.1.50,提示 Connection closed by 192.168.1.50 port 22。 此前拔掉 MID360 网线并重启工控机后,主机名又能解析到 Wi-Fi 地址,但保留两张网卡时问题再次出现。

工控机同时使用 Wi-Fi 和有线网口,两张网卡的用途不同:

接口或设备 本次地址 用途
工控机 Wi-Fi wlp4s0 192.168.2.183/24 SSH、局域网通信和联网,地址由 DHCP 分配
工控机雷达有线口 enp3s0 192.168.1.50/24 接收 MID360 点云和 IMU 数据
MID360 192.168.1.107 向配置的上位机地址发送数据

**192.168.1.50 是雷达数据的接收端地址,不是 MID360 的默认网关。 **该地址填写在驱动配置的 host_net_info 中,雷达自身地址填写在 lidar_configs 中。 Wi-Fi 和雷达使用不同的 /24 网段,本次故障不是这两个网段重叠造成的。

2026-10-19 保留两张网卡复现后,抓包和进程跟踪确认:工控机上的 NoMachine 9.7.3 在 Wi-Fi 上错误发布了 ysu.local → 192.168.1.50,与 Avahi 发布的正确地址冲突。

发布进程 在 Wi-Fi 上返回的 A 记录 记录有效期 TTL
avahi-daemon,PID 735 ysu.local → 192.168.2.183 120 秒
nxserver.bin,PID 3955,发送线程 4400 ysu.local → 192.168.1.50 600 秒

在工控机 wlp4s0 上抓到的响应均来自 192.168.2.183:5353,但记录内容分别为 .183 和 .50。进一步跟踪系统调用,确认 NoMachine 的发送线程将包含 .50 的记录从 Wi-Fi socket 发出,而 Avahi 发出的是 .183。因此不是单凭“哪个进程监听了 5353 端口”推测原因,已经定位到错误记录的实际发送进程。

两个服务使用同一个主机名给出不同的地址,电脑收到并缓存错误记录后,就会把 SSH 发往雷达网段。 600 秒表示记录有效期,并不是地址优先级;实际解析结果还受响应先后和缓存更新影响,所以会出现有时正常、有时指向 .50 的现象。

本次电脑去往 .50 的实际路由还进入了 FlClash TUN,去往 .183 则直接经过 Wi-Fi。 错误地址来自工控机 NoMachine 的 mDNS 发布;FlClash 接管去往错误地址的连接,是后续路径问题。 不能仅凭 Connection closed 判断工控机的 SSH 服务故障。

Avahi 当时已正确回答 Wi-Fi 地址,systemd-resolved 的 mDNS 未启用,两端检查的 hosts 文件中也没有静态配置 ysu.local → .50。先关闭 NoMachine 自带的局域网发现后,主机名立即恢复正常,验证了故障来源;单独重启 Avahi 或限制其接口,无法阻止 NoMachine 继续发送错误记录。随后进一步通过 NoMachine 自身的 NXDListenAddress 配置绑定 Wi-Fi 地址,恢复原生自动发现,并补上 DHCP 地址变化时的同步。下面先记录临时方案,再说明本机最终采用的完整修复。

1.1.2.临时方案:关闭 NoMachine 自动发现

1. 临时连接:使用工控机当前的 Wi-Fi 地址。(前提是你得知道它的 IP 地址)

以下命令在电脑端执行,本次地址为:

ssh ysu@192.168.2.183

Wi-Fi 地址可能随 DHCP 租约变化,可以在工控机本地或路由器客户端列表中查询。工控机上也可以执行:

nmcli -f DEVICE,TYPE,STATE,CONNECTION device status
ip -br -4 addr

本次工控机 Wi-Fi 接口为 wlp4s0,雷达有线接口为 enp3s0。排查其他机器时,以实际输出为准。

2. 工控机:确认 NoMachine 也在使用 mDNS。

以下操作在工控机上执行,不是在用于远程连接的电脑上执行:

sudo ss -ulpn 'sport = :5353'
sudo /etc/NX/nxserver --version

修复前,5353 UDP 端口同时由 avahi-daemon 和 nxserver.bin 使用。本次 NoMachine 版本为 9.7.3。多个进程监听同一端口本身不代表一定故障,应结合实际回答的地址判断;本次已通过抓包和进程跟踪确认 NoMachine 发布错误记录。

3. 工控机:关闭 NoMachine 的局域网自动发现。

先备份配置。下面的备份文件名用于本次排查;再次操作时换一个文件名,保留已有备份:

sudo cp -a /usr/NX/etc/server.cfg /usr/NX/etc/server.cfg.before-mdns-fix-20261019
sudoedit /usr/NX/etc/server.cfg

找到本次安装版本中的配置项:

#EnableLocalNetworkBroadcast 1

去掉开头的 # 并改为:

EnableLocalNetworkBroadcast 0

这会关闭 NoMachine 的局域网自动发现,其他电脑不再通过它的广播自动发现该工控机;仍可通过 IP 或能够正常解析的主机名手动连接 NoMachine。 无需卸载 NoMachine,也无需停掉 Avahi。NoMachine 官方端口与配置说明

不同版本的配置项可能不同,旧资料中还会出现 EnableNetworkBroadcast。应以安装版本的 server.cfg 注释和对应文档为准,不能把旧名称直接添加到本次 9.7.3 的配置中。

4. 工控机:重启 NoMachine,使配置生效。

先结束正在使用的 NoMachine 远程桌面会话,再执行;重启会暂时中断该服务:

sudo /etc/NX/nxserver --restart
sudo /etc/NX/nxserver --status

随后检查:

sudo ss -ulpn 'sport = :5353'
sudo ss -ltnp 'sport = :4000'

临时方案生效后,5353 端口只剩 Avahi,NoMachine 的 nxserver、nxnode 和 nxd 服务恢复运行,4000 TCP 端口继续监听。配置保存在文件中,系统重启后仍会读取;本次没有修改 Avahi 配置,也没有修改 Wi-Fi、雷达 IP 或路由。

5. 电脑端:验证主机名和 SSH。

getent ahostsv4 ysu.local
avahi-resolve-host-name -4 ysu.local
ssh ysu@ysu.local

预期解析到工控机当前的 Wi-Fi 地址,本次为 192.168.2.183,并且可以正常登录。已经缓存的错误记录可能需要等待过期或被正确记录更新;若仍返回 .50,应继续抓包确认是否还有错误发布源,不能仅凭服务状态认定修复完成。

如需抓包,在工控机执行以下命令,将接口名换成实际 Wi-Fi 接口:

sudo tcpdump -ni wlp4s0 -vv 'udp port 5353'

观察 ysu.local 的 A 记录内容,而不只是数据包的源 IP。本次错误响应的数据包源 IP 也是 .183,真正错误的是其中的 A 记录指向了 .50。

1.1.3.临时方案验证结果

2026-10-19 完成上述修改并重启 NoMachine 后:

检查项目 结果
电脑端三次名称查询 均得到 192.168.2.183
工控机 Wi-Fi 上的三次 mDNS 查询响应 均为 Avahi 返回的 .183,该轮抓包未再出现 .50
ssh ysu@ysu.local 密码登录成功,远端 hostname 返回 ysu
NoMachine 状态 服务正常,4000 TCP 端口继续监听
电脑断开并重新连接原 Wi-Fi 后 主机名仍解析到 .183

临时方案只将 /usr/NX/etc/server.cfg 中的 EnableLocalNetworkBroadcast 设为 0,原配置备份在同目录的 server.cfg.before-mdns-fix-20261019。后续已恢复自动发现,最终状态以下面的完整修复为准。

1.1.4.完整修复:让 NoMachine 自动跟随 Wi-Fi 当前 IP

这里固定的是用于远程连接的 Wi-Fi 接口,不是某一个 IP。NXDListenAddress 本身需要填写具体地址,不能靠写一行配置就自动追踪 DHCP。动态效果来自下面的同步程序:每次读取 Wi-Fi 当前地址,再把它写入 NoMachine 配置。

例如当前地址为 .183,程序就写入 .183;以后 DHCP 分配了其他地址,程序会用新地址替换它。不要手动把 NXDListenAddress 永久写成 192.168.2.183,也不要把 ysu.local 填进这个字段来解析自己;电脑端连接时才使用 ysu.local。NoMachine 监听和广播地址配置说明

本机已完成下面的部署,现有工控机无需重复安装。下面给出完整步骤,方便在其他机器复用;首次部署会重启 NoMachine,应先结束正在使用的远程桌面会话。

1. 工控机:确认需要跟随的 Wi-Fi 接口。

nmcli -f DEVICE,TYPE,STATE,CONNECTION device status
ip -br -4 addr

本机 Wi-Fi 为 wlp4s0,雷达网口为 enp3s0。后面的 Python 脚本和 NetworkManager 事件脚本中各有一处接口设置,其他机器应把两处 wlp4s0 都换成实际 Wi-Fi 接口名。Wi-Fi 继续使用 DHCP,无需固定 IP,也无需修改雷达网口。

2. 工控机:安装读取当前 IP 的同步程序。

创建文件:

sudoedit /usr/local/sbin/nomachine-wifi-sync

填入下面的内容。脚本中只指定接口名 INTERFACE,没有写死 Wi-Fi IP;PORT 对应本机 NoMachine 默认的 4000 端口。

#!/usr/bin/python3
"""Keep NoMachine bound to the robot's management Wi-Fi IPv4 address."""

import fcntl
import json
import os
from pathlib import Path
import re
import stat
import subprocess
import tempfile


INTERFACE = "wlp4s0"
CONFIG = Path("/usr/NX/etc/server.cfg")
PORT = 4000


def run(*args):
    return subprocess.check_output(args, text=True, timeout=10)


def replace_config(content):
    metadata = CONFIG.stat()
    temporary = None
    try:
        with tempfile.NamedTemporaryFile(mode="w", dir=CONFIG.parent, delete=False) as output:
            temporary = Path(output.name)
            output.write(content)
            output.flush()
            os.fchown(output.fileno(), metadata.st_uid, metadata.st_gid)
            os.fchmod(output.fileno(), stat.S_IMODE(metadata.st_mode))
            os.fsync(output.fileno())
        os.replace(temporary, CONFIG)
    finally:
        if temporary is not None and temporary.exists():
            temporary.unlink()


def main():
    with open("/run/nomachine-wifi-sync.lock", "w") as lock:
        fcntl.flock(lock, fcntl.LOCK_EX)
        interfaces = json.loads(run("/usr/sbin/ip", "-j", "-4", "address", "show", "dev", INTERFACE))
        addresses = [
            address["local"]
            for interface in interfaces
            for address in interface.get("addr_info", [])
            if address.get("scope") == "global" and address.get("valid_life_time") != 0
        ]
        if not addresses:
            print(f"{INTERFACE} has no IPv4 address; leaving configuration unchanged.", flush=True)
            return
        if len(addresses) != 1:
            raise RuntimeError(f"Expected one Wi-Fi IPv4 address; got {addresses!r}")
        address = addresses[0]
        old = CONFIG.read_text()
        updated, count = re.subn(
            r"(?m)^[ \t]*#?NXDListenAddress[^\S\n]+[^\n]*$",
            f'NXDListenAddress "{address}"',
            old,
        )
        if count != 1:
            raise RuntimeError(f"Expected one NXDListenAddress entry; got {count}")
        listeners = run("/usr/bin/ss", "-H", "-ltn", f"sport = :{PORT}")
        bound = any(line.split()[3] == f"{address}:{PORT}" for line in listeners.splitlines())
        if updated == old and bound:
            print(f"NoMachine already bound to {INTERFACE} {address}:{PORT}; no restart.", flush=True)
            return
        if updated != old:
            replace_config(updated)
        print(f"Synchronizing NoMachine to {INTERFACE} {address}:{PORT}.", flush=True)
        subprocess.run(["/etc/NX/nxserver", "--restart"], check=True, timeout=75)
        listeners = run("/usr/bin/ss", "-H", "-ltn", f"sport = :{PORT}")
        if not any(line.split()[3] == f"{address}:{PORT}" for line in listeners.splitlines()):
            raise RuntimeError(f"NoMachine is not listening on {address}:{PORT} after restart")
        print(f"Verified NoMachine listener {address}:{PORT}.", flush=True)


if __name__ == "__main__":
    main()

这个程序读取接口当前的唯一全局 IPv4,更新现有的 NXDListenAddress 配置项。当前地址和监听都正确时直接退出;地址改变或监听未恢复时,才重启 NoMachine 并检查结果。接口暂时没有 IPv4 时不修改配置,存在多个 IPv4 时明确报错,避免随意选择地址。

3. 工控机:设置开机同步和网络变化触发。

创建一次性同步服务:

sudoedit /etc/systemd/system/nomachine-wifi-sync.service

填入:

[Unit]
Description=Bind NoMachine to the current management Wi-Fi IPv4 address
Wants=network-online.target
After=network-online.target nxserver.service

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/nomachine-wifi-sync
TimeoutStartSec=100

[Install]
WantedBy=multi-user.target

再创建 NetworkManager 事件脚本:

sudoedit /etc/NetworkManager/dispatcher.d/90-nomachine-wifi

填入:

#!/bin/sh
# NetworkManager runs this as root after connection/address changes.
case "${1:-}:${2:-}" in
    wlp4s0:up|wlp4s0:dhcp4-change|wlp4s0:reapply)
        /usr/bin/systemctl --no-block start nomachine-wifi-sync.service
        ;;
esac

设置文件所有者和执行权限:

sudo chown root:root /usr/local/sbin/nomachine-wifi-sync /etc/systemd/system/nomachine-wifi-sync.service /etc/NetworkManager/dispatcher.d/90-nomachine-wifi
sudo chmod 755 /usr/local/sbin/nomachine-wifi-sync /etc/NetworkManager/dispatcher.d/90-nomachine-wifi
sudo chmod 644 /etc/systemd/system/nomachine-wifi-sync.service

这样,Wi-Fi 连接、DHCPv4 租约变化或重新应用连接配置时,系统都会触发同步。NetworkManager 官方事件说明

上述三个文件是本次部署的辅助配置,不是 NoMachine 自带功能;只修改 server.cfg、没有安装事件脚本,地址就不会自动跟随 DHCP。

4. 工控机:启用同步,再恢复 NoMachine 自动发现。

先加载并启用同步服务,让程序根据当前 Wi-Fi 地址生成 NXDListenAddress:

sudo systemctl daemon-reload
sudo systemctl enable --now nomachine-wifi-sync.service

然后编辑 NoMachine 配置:

sudoedit /usr/NX/etc/server.cfg

将局域网自动发现配置项改为:

EnableLocalNetworkBroadcast 1

**NXDListenAddress 这一行交给同步程序维护,不手动填写 .183。**首次打开自动发现后,重启 NoMachine,使该开关生效:

sudo /etc/NX/nxserver --restart

后续 IP 变化由同步服务自动处理,无需再手动修改配置或重启;地址发生变化时的服务重启会中断原有 NoMachine 会话。

可以检查程序生成的地址和日志:

sudo grep '^NXDListenAddress' /usr/NX/etc/server.cfg
systemctl is-enabled nomachine-wifi-sync.service
sudo journalctl -u nomachine-wifi-sync.service -n 30 --no-pager

这里看到的是程序最后一次同步的具体地址,应与 wlp4s0 当前 IPv4 一致,不代表给网卡设置了静态 IP。服务是一次性任务,执行成功后显示 inactive 属于正常情况,应看日志和 Result,不要求它一直处于 active。

需要手动重新同步时执行:

sudo systemctl start nomachine-wifi-sync.service

NoMachine 恢复自动发现后仍会使用 5353 UDP 端口。此时不能把“看到 nxserver.bin 监听 5353”直接当作故障,应检查它发布的地址是否与当前 Wi-Fi IP 一致。

5. 电脑端:始终使用主机名连接。

avahi-browse -rt _nomachine._tcp
getent ahostsv4 ysu.local
ssh ysu@ysu.local

自动发现的 A 记录和 TXT 中的 ip= 应等于工控机此时的 Wi-Fi IPv4,而不是永远等于本次排查时的 .183。本次 NoMachine 的 SRV 记录端口为 0,实际 NX 连接端口由 TXT 中的 service=nx:4000 给出,不能据此误判服务没有监听。

手动创建 NoMachine 连接时使用主机名:

协议:NX
主机:ysu.local
端口:4000
用户名:ysu

电脑仍需与工控机处于能够进行局域网发现和互访的网络中。IP 变化时,由 Avahi 更新主机名记录、同步程序更新 NoMachine 绑定地址,电脑端继续使用 ysu.local。

1.1.5.完整修复验证结果

检查项目 结果
原生 NoMachine 自动发现 已恢复,能发现 ysu,A 记录和 TXT 的 IP 均为 .183
Wi-Fi 抓包 Avahi 和 NoMachine 都回答 .183,该轮未再出现 .50
4000 TCP 端口 绑定 192.168.2.183:4000,电脑端实际 TCP 连接成功
SSH 主机名连接 ssh ysu@ysu.local hostname 密码登录成功,返回 ysu
配置残留旧 IP 的模拟 将保存地址临时改成 .254 后触发 DHCP 变化处理,同步任务自动恢复 .183、重启服务并验证监听成功
地址未变化的事件 同步任务直接退出,不重启 NoMachine
开机同步 服务已启用

完整修复前的配置备份为 /usr/NX/etc/server.cfg.before-wifi-binding-20261019。本次没有改变工控机实际 Wi-Fi IP,也没有做整机重启或图形桌面登录测试;模拟旧配置验证了同步处理流程,真实换 IP 和完整图形会话仍应在实际使用时确认。

1.2.ROS 2 远端查看 MID360 点云导致降频、延迟和 RViz 不显示

1.2.1.原因

现象是:电脑通过 Wi-Fi 打开 RViz,或者仅运行远端 ros2 topic hz,工控机上的 MID360 点云和 IMU 就明显降频,连工控机本地测频也受到影响;关闭远端订阅后恢复。切换手机热点和其他 Wi-Fi 都未直接解决问题。

本次排查发现了两个独立问题:

  1. 电脑端 DDS 公布了不适合本次跨机通信的地址。

    电脑端 DDS 同时公布了 Wi-Fi 地址、Docker 网桥地址 172.17.0.1 和 Clash TUN 虚拟网卡地址 198.20.0.1。工控机根据这些地址发送数据,其中发往 Clash TUN 地址的数据被路由到雷达网口的网关 192.168.1.1,邻居解析未完成。实测部分 UDP 发送调用阻塞约 250~330 毫秒,发送阻塞拖慢发布,点云和 IMU 出现积压。

    本次定位到的明显阻塞与 Clash TUN 地址有关,没有单独验证 Docker 地址是否造成同样阻塞。不过,电脑 Docker 网桥地址也不适合作为本次跨机接收地址,应一并排除。选择接口的原则是:只使用 ROS 对端实际可达、并且本次通信确实需要的接口。

  2. 电脑端 UDP 接收缓冲不足以承受本次点云突发流量。

    修正接口并重建电脑端 daemon 后,工控机已恢复点云约 20 Hz、IMU 约 200 Hz,但电脑端点云仍只有约 12.8 Hz,接收 socket 仍有缓冲丢包。此时默认接收缓冲约为 208 KiB;将请求值增大到 4 MiB 后,电脑端点云恢复约 20 Hz,测试期间该 socket 的缓冲丢包计数为零。

    因此,缓冲丢包不能全部归因于选错网卡。大点云的分片集中到达、接收线程短暂来不及处理时,正确接口也可能出现缓冲溢出。增大接收缓冲是按数据量选择的优化;本次 MID360 接收环境有实测依据,建议保留 4 MiB。Fast DDS 大数据传输说明

ros2 topic hz 会订阅完整消息来统计频率,同样需要接收整帧点云,因此仅开启远端测频也可能触发上述问题。ROS 2 Humble 测频实现

点云延迟到十几秒甚至几十秒后,点云时间戳可能早于 RViz 保存的 TF 历史。即使 TF 树连通、最新变换正常,RViz 仍无法完成点云时间戳对应的坐标变换,表现为不显示点云。本次将 Fixed Frame 临时改为点云自身的 front_mid360 后出现点云,验证了这个方向;解决传输积压后,就可以使用正常的显示坐标系。

1.2.2.解决方法

1. 工控机:建议保留异步发布。

在实际运行 ROS 的用户的 ~/.bashrc 中加入。

export RMW_FASTRTPS_PUBLICATION_MODE=ASYNCHRONOUS

修改后执行:

source ~/.bashrc

然后在加载了该环境的终端中重新启动 MID360 驱动,使新进程继承设置。

异步发布将实际发送交给后台线程,可以减轻网络发送对驱动发布线程的直接影响;单独设置它未能解决本次问题,仍需配合下面的电脑端配置。Fast DDS 在 ROS 2 Humble 中的发布模式说明

2. 电脑端:通过 source 加载工程内的 dds_env.bash。

先查询自己网络网卡:

ip -br -4 addr

看ip就能看出来,只有10.xx.xx.xx和192.168.xx.xx这种才是NAT4下发的ip。 所以这个wlp0s20f3才是你和ROS用于局域网通信的网卡。

也可以点击WIFI按钮确认一下ip,和上方说的一模一样:

当前脚本位于工程根目录:

/ysu2026_rm_sentry_nav/dds_env.bash

现在初始化当前终端后即可正常使用 rviz2 和 ros2 命令。

这份配置应首先加载到电脑端,因为接收地址由电脑 DDS 公布,接收缓冲也属于电脑上的接收进程。工控机使用电脑公布的地址发送点云;增大工控机的接收缓冲不能替代电脑端设置。

脚本读取所选接口当前的 IPv4,生成 XML,并导出 FASTRTPS_DEFAULT_PROFILES_FILE。UDP 使用所选地址和本机回环地址 127.0.0.1,同时保留本机 SHM 通信。XML 保存在工程根目录,文件名为 .dds-env-<用户ID>-<配置摘要>.xml,已加入 Git 忽略规则。Fast DDS 接口白名单

以下命令在电脑的 ROS 终端执行。两端 ROS_DOMAIN_ID 必须一致,本次为 6;网卡名 wlp0s20f3 应按实际接口修改:

source /opt/ros/humble/setup.bash
export ROS_DOMAIN_ID=6

export DDS_INTERFACE=wlp0s20f3
export DDS_RECV_BUFFER=4MiB
source /你的存放工程的路径/ysu2026_rm_sentry_nav/dds_env.bash

比如:source /home/tungchiahui/UserFolder/MySource/ysu2026_rm_sentry/ysu2026_rm_sentry_nav/dds_env.bash

两个参数可以分别配置:

参数 用法
DDS_INTERFACE 指定网卡名称,支持逗号分隔的多个接口;未设置时自动选择唯一处于 UP 状态且有 IPv4 的物理网卡,多个候选时返回错误,要求明确选择
DDS_RECV_BUFFER 不设置或 0 表示系统默认;纯整数表示字节;支持 B、KB、MB、GB、KiB、MiB、GiB 单位,数值须为非负整数

4MiB 等于 4194304 字节,4MB 等于 4000000 字节。缓冲参数是脚本定义的选项,最终会转换为字节写入 Fast DDS XML。本次接收 MID360 点云建议使用已验证有效的 4MiB。请求值还受系统 net.core.rmem_max 上限约束,超出上限时脚本会提示,但不会自动修改系统参数。

脚本成功加载时不打印,出错或系统上限不足时给出提示。它仅配置 Fast DDS,不改变 ROS_DOMAIN_ID、消息 QoS 或当前 shell 的选项。

我电脑容器使用 host 网络,可以访问宿主 Wi-Fi 接口。其他 Docker 网络模式应选择容器实际可见且对端可达的接口; 接口白名单不能替代 bridge 网络所需的 NAT 或发现配置。跨容器使用 SHM 还取决于 IPC 配置。(一劳永逸,直接host即可)

3. 首次启用或切换网络后,重建电脑端 daemon。

先关闭旧的 RViz 和远端测频进程,在已加载上述 DDS 配置的终端执行:

ros2 daemon stop
ros2 daemon start

daemon 本身也会创建 DDS Participant,应继承相同的接口配置,避免保留原先的虚拟网卡地址。本次排查中,重建 daemon 是恢复正常通信的一步;不必每次测频都重启它。

4. 直接打开 RViz 和测频。

在已加载配置的电脑终端中打开 RViz:

rviz2

另一个终端也先加载第 2 步相同的 ROS 和 DDS 环境,再运行:

ros2 topic hz /livox/lidar/pointcloud --window 40

--window 40 只是缩短统计窗口,方便观察近期变化;真正起作用的是网卡白名单和接收缓冲设置。

5. 日常使用:可加入电脑 ROS 用户的 .bashrc。

将以下三行放在已有 ROS 环境加载语句之后。当前电脑容器使用 root,对应 /root/.bashrc:

export DDS_INTERFACE=wlp0s20f3
export DDS_RECV_BUFFER=4MiB
source /你的存放工程的路径/ysu2026_rm_sentry_nav/dds_env.bash

比如:source /home/tungchiahui/UserFolder/MySource/ysu2026_rm_sentry/ysu2026_rm_sentry_nav/dds_env.bash

这样,新打开并读取 .bashrc 的终端会自动初始化 DDS。通过服务或非交互方式启动的 ROS 程序,需要在其启动环境中主动加载配置。脚本位于个人工程中,加载现有 /opt/ros/humble/setup.bash 不会修改 ROS 安装文件。

切换 Wi-Fi、改变所选接口或接收缓冲后,应重新 source 脚本,重建 daemon,并重新启动相关 ROS 程序;已经运行的进程不会自动读取新的环境变量或 XML。脚本本身不会重启这些进程。

1.2.3.本次验证结果

完成接口限制、daemon 重建和 4 MiB 接收缓冲配置后的现场远端实测结果如下:

测量位置 点云频率 IMU 频率
工控机本地 约 20 Hz 约 200 Hz
电脑端 约 20 Hz 约 197~199 Hz

放大缓冲后的测试节点接收缓冲丢包计数为零。打开 RViz 后,工控机仍保持正常频率,点云持续显示,延迟不再增长到几十秒。这是本次测试期间的结果。

上表来自整理新脚本之前的现场远端测试。后续新增 dds_env.bash,在 ysu_ros_humble 中验证了 XML 加载、接收缓冲生效、本机约 262 KB 点云接收 20/20 帧,以及单位转换、静默加载和参数错误时保留原环境。新版脚本的验证范围包含这些项目;上述远端频率仍引用此前现场实测。

配置整理在工程中新增了 DDS 环境脚本和生成 XML 的 Git 忽略规则;本次排查未修改 MID360 驱动、SLAM 或控制节点的业务代码。

1.3.点云和 TF 正常但二维地图不更新

1.3.1.现象与排查入口

2026-10-20,工控机 ysu.local 在容器 ysu_rm_ros_humble 中进行实车建图,启动参数包含 slam:=True use_sim_time:=False。现场表现为点云持续刷新,odom 到底盘的 TF 和位姿输出正常,但二维地图没有继续生成。

当前二维建图链路为:

Point-LIO → cloud_registered
    → loam_interface → registered_scan(odom系点云)
    → 地形分析 → terrain_map_ext
    → pointcloud_to_laserscan → obstacle_scan
    → slam_toolbox → /map

先检查点云有没有变成二维扫描,再检查扫描有没有进入地图。 点云刷新只说明三维处理链有输出;ros2 node list 能看到转换节点、ros2 topic list 能看到话题,也不代表该节点正在发布有效数据。

以下检查在工控机实际运行建图的 ROS 容器内执行,先加载对应环境。工程路径和 ROS_DOMAIN_ID 按现场修改,本次为:

source /opt/ros/humble/setup.bash
source /home/ysu/robomaster/ysu2026_rm_sentry/install/setup.bash
export ROS_DOMAIN_ID=6

# 分别观察二维转换的直接输入和输出,每项最多运行10秒。
timeout 10s ros2 topic hz /terrain_map_ext --window 40
timeout 10s ros2 topic hz /obstacle_scan --window 40

# 查看是否有发布端和订阅端;有连接仍需结合实际收帧判断。
ros2 topic info /obstacle_scan --verbose

上面的 timeout 到时结束属于预期行为;在没有消息时,测频命令可能一直没有频率输出。本次已存在 slam_toolbox 订阅,转换节点仍然没有输出,不能解释为“没人订阅,所以节点没有接收点云”。

1.3.2.现场证据

本次在工控机本地连续订阅采样 10 秒,结果如下:

话题 接收数量 说明
/cloud_registered 199 Point-LIO 点云持续更新
/registered_scan 197 转到 odom 系的点云持续更新
/terrain_map 199 近场地形持续更新
/terrain_map_ext 195 二维转换的直接输入持续更新,约20 Hz
/odometry 196 位姿输出持续更新
/obstacle_scan 0 二维转换的输出断流
/map 20 仍在发布旧地图,消息时间约落后405秒

/terrain_map_ext 最后一帧有 2700 个点,其中 1204 个点满足配置的 intensity=[0.1,2.0] 门槛。这里的 intensity 是地形节点计算的相对地面高差,单位为 m。该结果说明输入并非全部被这项门槛筛掉,但其他几何条件仍需单独检查;本次确定死锁依靠下面的线程和锁证据。

slam_toolbox 的参数服务正常返回,use_sim_time=false,scan_topic=obstacle_scan;转换节点的参数请求在采样期间没有返回。这与转换节点的主执行线程被阻塞相符。

单独看到 /map 的时间戳较旧,不能直接断言死锁:机器人静止或新扫描未达到入图门槛时,地图内容也可能不变。本次同时确认了输入持续更新、输出完全断流、参数请求不返回,以及两个线程互相等待锁。

1.3.3.原因:转换节点内部的 TF 等待死锁

现场安装版本为:

ros-humble-tf2             0.25.23-1jammy.20260907.213559
ros-humble-tf2-ros         0.25.23-1jammy.20260907.224752
ros-humble-tf2-sensor-msgs 0.25.23-1jammy.20260907.225802

转换节点使用 tf2_ros::MessageFilter,等待把 odom 系地形点云转换到 base_footprint 所需的 TF。这个版本的异步等待路径存在锁顺序反转:点云处理线程先持有一把锁、再等另一把锁,TF 接收线程按相反顺序持锁,可能形成永久的相互等待。Humble 上游修复说明

本次转换节点容器 PID 为 2902,抓取线程堆栈后立即脱离。两条关键调用链为:

主线程2902:
pthread_mutex_lock(0x5dad8dbf3840)
  → tf2::BufferCore::addTransformableRequest
  → tf2_ros::Buffer::waitForTransform
  → tf2_ros::MessageFilter<PointCloud2>::add

TF接收线程3006:
pthread_mutex_lock(0x5dad8dbf38f0)
  → tf2_ros::Buffer的transform-ready回调
  → tf2::BufferCore::testTransformableRequests
  → tf2::BufferCore::setTransformImpl
  → tf2_ros::TransformListener::subscription_callback

读取两把锁的所有者,得到:

锁地址 持有该锁的线程 等待该锁的线程
0x5dad8dbf3840 3006 2902
0x5dad8dbf38f0 2902 3006

线程2902和3006各持有对方需要的锁,已经形成死锁。这里的线程编号和地址只描述本次现场,重新启动后会变化。

外部 TF 正常与这个死锁可以同时存在。 每个转换进程有自己的 TF Buffer 和处理线程;本次卡住的是二维转换进程内部的等待路径,其他节点仍能接收 TF、处理点云和输出位姿。转换节点进程保持存在,却停止处理新点云,slam_toolbox 收不到新的 obstacle_scan,于是只能继续发布旧地图。相同版本与调用链的上游报告 #992

1.3.4.修复版本与时间

Humble 已有对应修复:

日期 上游进展
2026-09-10 修复锁顺序反转的补丁合入 Humble 分支,见PR #990
2026-09-15 tf2/tf2_ros 0.25.24 的版本记录列出这项死锁修复,见tf2版本记录、tf2_ros版本记录

2026-10-20 直接检查工控机所配置的清华 ROS 软件源在线索引,确认已有以下 amd64 包:

ros-humble-tf2     0.25.24-1jammy.20260915.203746
ros-humble-tf2-ros 0.25.24-1jammy.20260915.204410

但容器当时的 apt-cache policy 仍把 0.25.23 列为候选,本地 ROS 软件源索引停在2026-10-02。因此需要先刷新索引,再检查候选版本;“上游有修复”“本地安装了修复版”“运行中的进程已经加载修复版”是三个不同状态。

1.3.5.处理方法

以下为修复操作步骤,本次诊断时尚未执行。

1. 在运行建图的 ROS 容器内刷新索引,核对候选版本。

以下以容器中的 root 用户为例;普通用户执行软件包管理命令时需使用 sudo:

apt update
apt-cache policy ros-humble-tf2 ros-humble-tf2-ros

确认两个包的 Candidate 均为 0.25.24 或包含该修复的后续 Humble 版本。如果仍为 0.25.23,先检查索引刷新是否成功及镜像是否同步,再进行升级。

2. 结束本轮建图,升级相关包并核对安装结果。

apt install --only-upgrade ros-humble-tf2 ros-humble-tf2-ros
dpkg-query -W -f='${Package} ${Version}\n' ros-humble-tf2 ros-humble-tf2-ros

由包管理器处理依赖,检查命令是否成功以及实际安装版本。单独重启转换节点有机会暂时恢复,但继续使用 0.25.23 仍可能再次触发死锁。

3. 重新启动建图,检查扫描与地图恢复。

使用原来的实车建图启动方式重新运行,随后检查:

  • /terrain_map_ext 持续输入,/obstacle_scan 恢复连续输出,且扫描中存在有效距离值。
  • 转换节点的参数请求能够正常返回。
  • 在满足扫描入图条件的实际移动后,/map 内容继续更新。不要只凭静止时地图不变判断修复失败。

升级磁盘上的库不会解除旧进程已经形成的死锁,必须重启使用旧库的建图进程。修改高度、强度、TF容差或RViz显示参数,也无法解除这次已经形成的锁等待环。

1.3.6.本次验证状态

已完成: 现场话题采样、参数服务检查、线程堆栈和锁所有者核对;确认二维转换节点死锁;核实上游修复版本及当前配置镜像中修复包可用。

待完成: 容器软件包升级、建图重启,以及升级后二维扫描与地图恢复的实车验证。截至2026-10-20本次记录,尚未执行这些修复操作,不能将上游修复结论写成已经完成本机恢复验证。