← 返回报告索引
2026-09-03 · 工作日志

2026-09-03:深度终于录上了,以及为什么之前一直没有

这一天从「磁盘被写满、命令都跑不动」开始,查出深度旁路的四个缺陷, 修掉其中三个,把第四个(真正的根因)定位到 USB 2.0 带宽不够,换到 USB 3.0 后深度第一次录上。 中间还发现安全层的下限高了 8 cm,把训练示范拒掉 96.6%。 这一页是当天的完整账:做完的、发现但没修的、以及接下来谁做什么。

当天最重要的一条:深度相机从来没坏过。 它一直被 USB 2.0 的带宽卡着 —— 三路相机挤在嵌套的两层 USB 2.0 hub 上共享 480 Mbps, 深度分配不到带宽,于是「打开成功、一帧不给」。换到 USB 3.0(5000 Mbps)之后, 实测 valid 86%、中位距离 516 mm,当晚录到 3000+ 帧深度。

一、修完的

1 · 磁盘被写满(起因)

根分区被写到 0 字节,连开发环境的命令都执行不了。 根因:leader_follower_server._cmd_start 覆盖 self._depth 前不停旧的, 按两次 Start 就孤儿掉上一个录制器 —— 它跑了 13.7 小时、220,412 帧、27 GB。

2 · 摄像头句柄泄漏

v4l2_depth.DepthCamera.open() 先 os.open() 拿 fd,再做一串会抛异常的 ioctl; 失败时只把 _cam 置 None,fd 永远不关。实测累积 34 个泄漏句柄。

3 · 静默失败:0 帧不报

DepthRecorder 把「0 帧」当正常收尾写 meta.json,面板一个字都不说。 这是当天代价最大的缺陷 —— 9-02 一晚 102 集里 88 集丢了深度,当场无人察觉。

4 · ★ 真正的根因:USB 2.0 带宽 ★

拓扑查出来是这样:

Jetson
 └ Realtek 双总线 hub(USB2 侧 1-2 + USB3 侧 2-1,同一个物理 hub)
    ├ 口1  iTingSbc
    ├ 口2  串口(手臂总线)
    ├ 口3  ★ 第二个 hub —— 只支持 USB 2.0 ★
    │       ├ Orbbec Gemini 335   ← 深度 + 头部 RGB,两个接口
    │       ├ 腕相机
    │       ├ 腕相机
    │       └ 键盘
    └ 口4  串口(手臂总线)

三路相机全在那个 USB 2.0 hub 后面共享 480 Mbps。深度 640×480×16bit×30fps ≈ 147 Mbps, UVC 分配不到就一直超时。换到 Jetson 机身的 USB 3 口后链路 5000 Mbps,深度立刻正常。

被证伪的一个推断:9-02 的报告里写过「总线复位能清掉假死」(当时明确标注为未验证)。 9-03 直接做了实验:拔插一次,三次 probe 全部照样超时。所以那个推断是错的, 真正的变量是带宽,不是什么假死状态。
换口的连带代价(事前没预料到):换到不同物理口 = by-path 变了 (2.3.1 → 1.1),而仓库里 9 个文件、19 处写死了旧路径 —— 面板四路相机全找不到、LF 握着两个 (deleted) 句柄、预览画面定格。
已修:19 处全部更新;并新增 usb_cameras.py 按 VID:PID 动态解析 (Orbbec = 2bc5:0800,接口 0 = 深度、接口 4 = RGB),接进 robot_server.CAMERAS 和 depth_recorder.DEFAULT_DEPTH_NODE, 动态优先、写死兜底、找不到返回 None 绝不抛异常。以后再换口不用改代码。

5 · 安全层下限高了 8 cm

teleop_safety.yaml 右臂 z_min = 0.065(08-24 换夹爪时标定) 把训练集 0826-0042 全部 40 集 16,487 帧示范拒掉 96.6%,试跑第 1 帧即中止。

为什么错数值
z_min 的含义 = 桌面 + 爪长0.065 − 爪长 0.1179 = 桌面 −0.053
桌面实测(08-16 摇臂法)−0.137 / −0.142
差8 cm,而且新爪比旧爪短、下限却抬高了,方向都反了
推论:08-24 换夹爪之后所有「模型抓不到杯子」的实机结论都要重新审视 —— 模型很可能从来没被允许动第一帧。08-25 的记录里 2/4 次就是同一句 outside [0.065, 0.35] 中止。

6 · 面板上四个坏了很久的按钮

tctlRun / tctlSend / REC_TASKS 等 12 个函数 + 3 个变量 声明在 start() 函数内部(文件里没有缩进,看着像顶层), 而「重放选择器」和操作模式的按钮在外面调 → ReferenceError。 这几个按钮从来就没通过过,和当天的改动无关。用 AST 一次扫出全部,挂到 window 上修好。

顺手把这套检查固化成了技能 .claude/skills/self-debug/: HTML 标签配平、整块 JS 语法、作用域泄漏、服务端下发一致性,四类不用浏览器就能判定的错。

7 · leader-arm 录制从来不会自动上传

对比两条路径:VR/面板录制在作业结束时起线程调 _publish_dataset; leader-arm 的 _cmd_finish 里一行上传代码都没有。 所以 9-02、9-03 两批数据全靠人工补传,操作者以为录完就传了。

二、数据与账号

做了什么结果
数据集迁到组织33 个 → xlerobot-team/(显示名 Aalto-xlerobot-team)0 失败
垃圾集归拢12 个 TEST/soak 改名 trash-* + private,留在个人名下、一个没删
模型7 个不动,仍在 Suyang99/
owner 配置化bartender_tasks.HF_OWNER 一处改全局(含面板/VR/LF/traincalc 预填)
9-03 录制今晚 30 集已全部上 Hub(1824/1853/1906/1927),2015 还在录
磁盘从 0 字节 → 33 GB 可用
为什么 trash-* 没删:trash-soak-20260816-220415 / -220918 是 xlerobot_vr.py 注释里的实测证据,trash-xlerobot-TEST-* 是 VR 轴污染文档的证据。 删了那些结论以后没法复核。改名+私有达到了「从视野消失」的效果,且可逆。
清理造成的一个副作用:prune_uploaded_datasets.py 删掉了本地 xlerobot-cup-grasp-20260820-0230(Hub 上有完整副本,删得对),但 demo_coverage.py / grasp_sight_check.py / trace_vs_demo.py 硬编码了它的本地路径,现在会失败。数据没丢,从 Hub 拉回来即可。
教训:删本地副本前先 grep 有没有脚本硬编码它。

三、发现了但没修的

事项状态
z_floor(pitch) 俯仰补偿写好了但零个调用方安全层和 VR 用的都还是标量 z[0]。接上它才是正解(算爪尖不算腕),今天只是把 z_min 止血改成 −0.11
gripper_len_m = 0.1179 量了、存了、没有任何读者measure_table.py 只 print 不写 yaml;全靠人抄,而 08-24 抄错了
三个分析脚本指向已删的本地数据集要从 Hub 拉回 0826-0042(现在在组织名下、private)
两只腕相机 VID:PID 完全相同身份分不出左右,仍靠 by-path;对调线缆会静默互换
VR/面板录制路径的深度是整段会话一个文件夹不是逐集,所以「0 帧标红」只加在了 LF 路径
1853 的 episode-00000499 张残留 PNG 已清(是一次没录完的第 5 集,不属于任何已保存的集),4 集数据完好并已上传

四、深度要验收,需要做什么

深度现在能录了,但「能录」不等于「能用」。验收分两级:

第一级 · 每次录制当场看(现场,零成本)

  1. 按 Stop 之后看那行消息:会写 depth: N frames。
  2. 绿/正常色 + 帧数三位数 = 好。红色 depth: 0 frames = 立刻停,别再往下录。

这一条就能挡住 9-02 那种「一晚 88 集全空、第二天才发现」。

第二级 · 数据质量核对(远程,我做)

录完之后我跑这几项,全部只读:

指标怎么算合格线
每集帧数depth/<集>/ 里 PNG 数和 RGB 时长匹配(约 10 fps × 秒数)
有效像素率timestamps.jsonl 的 valid 中位数> 0.80(当晚实测 0.795–0.881)
时间覆盖深度起止 vs 该集 RGB 时长≥ 90%
收尾完整每个目录有 meta.json缺 = stop() 没被调用

现成工具:python 03-software/scripts/lf_depth_doctor.py(句柄数 / 线程数 / 逐集帧数 / 磁盘四项体检)。

第三级 · 对齐验证(现场 + 远程,做路线 A 之前必须过)

深度和 RGB 是两个独立的流,靠 timestamps.jsonl 的墙钟事后对齐。 这个对齐从来没被验证过。做法:现场在相机前快速挥一下手, 远程比对深度帧和 RGB 帧里手出现的时刻,量出偏移毫秒数。偏移大于一个帧间隔就得修。

五、路线 A:现在能预备什么、能直接执行什么

路线 A = 深度不进模型,在模型旁边算出杯子的 3D 坐标,用来 ①开跑前的门 ②每次试验记录杯位 ③失败诊断。不重训、不重采。 它直冲现在最大的未知:「抓不到」到底是没看见还是抓歪了 —— 今天的试验记录回答不了。

A0 · 现在就能执行的(远程,不需要任何人)

做什么为什么现在能做
写标定采集脚本纯代码。采点、存 (关节角, 像素, 深度) 三元组、留出验证集
写外参求解器Kabsch/Umeyama 求刚体变换 + 残差报告,不碰硬件
写 hand_eye.json 的读写内参 / 外参 / 头部预置位名 / 残差,一份配置
把 cup_locate_demo.py 改成可复用模块现在是 demo 脚本,抽成 locate_cup() -> (x,y,z)
离线跑通全链路拿 2015 已录的 3000+ 帧深度当输入,验证「找最近的一团 + 反投影」在真实数据上稳不稳
最后一条特别值:2015 这批深度数据可以当离线测试集用 —— 不需要机器人、不需要现场,就能把杯子定位算法调到能用,等标定做完直接接上去。

A1 · 需要现场的(现场,约 30 分钟)

  1. 存一个专用的头部预置位(叫 trial)。
    这一步是整条路线的地基,漏了后面全白做。 相机装在会动的头上,所以外参是「头在某个姿态时」的值,头一动全作废。 而 run_policy_trials.py 的 head_lock 是启动时读当前位置就锁在哪 (run_policy_trials.py:531),不是固定值。 必须:标定前、每次试跑前,都先把头移到 trial 这个预置位。
    现有预置位:test / cup-grasp-v0 / sky / justforfun / justforfun2 —— 没有一个是为试跑固定的
  2. 爪尖贴一个不反光的浅色记号(小块哑光胶带或小球)。反光和透明都会让深度失效。
  3. 按脚本提示摆 15–20 个手臂姿态,覆盖真实工作区(前后、左右、高低都要,别挤在一小块)。 每个姿态停稳后按一下确认,脚本自动记两套坐标。
  4. (可选,精度更高)打印一张棋盘格,9×6 内角点、格子 20–25 mm,贴硬板上; 打印后用尺子量一格实际边长 —— 打印机缩放会让标称值不准。

A2 · 采完之后(远程,我做)

  1. 解内参(有棋盘格时)/ 解外参(刚体变换)。
  2. 留出 5 个点不参与求解,用解出的变换预测它们,报残差中位数(毫米)。
  3. 验收线:< 10 mm 可以开始用;> 20 mm 重采(多半是采点太集中或记号找歪)。
  4. 接进试验流程:每次试跑前报一次杯子 3D 坐标,写进试验记录。
一条物理边界:透明玻璃杯的深度会失效(返回 0 / 空洞)。 不透明杯和磨砂杯可行,透明的要贴标记兜底。

六、按优先级的待办

#事项谁
1录完按 Finish,让 2015 落盘并上传现场
2重启 8781,让「Finish 自动上传」生效远程
3存 trial 头部预置位现场
4用 2015 的深度离线跑通杯子定位远程
5写标定采集脚本 + 求解器远程
6采标定点(15–20 个姿态)现场
7把 z_floor(pitch) 接进安全层(正解,替掉今天的止血)远程
8重做桌面标定,核对 z_min − 爪长 ≈ −0.14现场
9从 Hub 拉回 0826-0042,修好三个分析脚本远程

七、诚实边界

2026-09-03 · 代码出处以本机 03-software/scripts/ 为准 · 配套页:深度相机的调用链与缺陷 · 深度 + SmolVLA 运行架构 · 录制流程(含 Start 前固定动作)