← reports index
软件 · 2026-09-08

「每个键都是往上」——一个不是 bug、一个我改出来的、一个单元测试放过的

操作者报了三件事:右臂按什么键都只往上、键盘模式一进去手臂就水平前伸、以及不停归位。没有一条的结论和它听起来的样子一样。最后查清楚的版本是:「每个键都是往上」是准确描述,而且它有物理原因——shoulder_lift 对高度不单调,而 teleop 的起始姿势被另一条安全规则钉死在了反直觉的那一侧。中间我还改出过一个新问题,并且写了一个会放过真 bug 的单元测试。

最后查清楚的:为什么"每个键都是往上"

拿人录的示范当地面真值(54 集,3434 帧;夹爪闭合发生在腕高最低点、抓完腕高上升,方向可信):

手在杯口最低处   z=-0.040   lift=+22.9
抓着杯子举起来   z=+0.059   lift=-34.1

一、shoulder_lift 是反的——越负 = 手臂越高。

二、它对高度还不单调。y=0.10 时折返点在 z=0.110:

z -0.06 → +0.11   lift 一路变负(大臂抬)
z +0.11 → +0.22   lift 反而变正(大臂放下)

三、teleop 的 home 是 z=0.170,在折返点上方。在那一侧:

键标签大臂实际
K收回抬起 4.5°
O降低抬起 1.9°
U升高放下 2.5°(手却上去)
I前伸放下 4.8°

操作者盯着摄像头看的是大臂,而键位表描述的是手。这一段里两者是反的。

而且躲不掉。折返点在任何前伸量下都落在 z=0.088–0.115,而 robot_server 的开机自检要求 home 处爪尖离桌至少 100 mm(2026-08-16 起,因为每次启动都把杯子扫倒),把 home 的 z 锁在 0.161 以上。home 注定落在反直觉那一侧,换配置解决不了,是结构性的。

示范数据也印证:3434 帧的腕高是 −0.067 到 +0.113,全部在折返线以下——人从来没在这一段操作过,teleop 却一开机就待在这里。

解法:一个键跳出去

从 home 降到折返线以下要按 8 次 O,而那 8 次每一次都是反的——最容易让人放弃的地方。所以做成一个键:G(左臂)/ H(右臂)直接降到工作高度 z=0.095。

验收标准不是"键有反应",是按完之后控制是否变得可读。真机同一条 HTTP 路径实测:

高度按 U(升高)
z≈0.118(折返线以上)lift −2.5 → −2.4,大臂放下和标签相反
z≈0.095(折返线以下)lift −2.0 → −2.1,大臂抬起和标签一致

纯垂直下降,y/x 一动不动,不横扫桌面;走 PoseFilter 同一条限速平滑路径。但爪尖离桌只有 3 cm,比杯子矮——正下方有杯子会撞到,是操作者主动触发的动作,按之前要先看画面。

端到端测试抓到一个单元测试放过的真 bug。

第一次真机路径实测,按 H 只把 z 从 0.167 走到 0.157 就停了——正好是 max_step_m 的 10 mm,剩下 62 mm 再没走。手动控制的模型是"一次按键 = 调一次 filt.update()",8 mm 增量一两拍就到,但绝对目标差 72 mm,一拍只许走 10 mm,下一拍没输入就不再驱动。

而我的单元测试写了 for _ in range(200): f.update(tgt)——真实控制环不会对一次按键重复喂同一个目标。测试造了一个现实中不存在的条件,所以它通过了。改了两处:目标存成跨拍状态直到到位;测试改成驱动真实的 Session.run(),不再自己写循环。

① 右臂不管按 J / K / L 都只往上

操作者原话:「我测试了 不管JKL右臂都只有往上」

不是某个键坏了。日志里 J 和 L 是有反应的——问题是当时六个键全都只动了 1–3°。

先看当时的日志

操作者追问「右臂你到底改好没有」之后我回去翻了 2026-09-08 03:13 那次会话的记录。按 J 的那一段,y 和 z 完全不动,只有 x 在走:

x=-0.001  y=+0.146 z=+0.162   pan=-0.2  lift=-4.0  elbow=-14.4
x=-0.002  y=+0.146 z=+0.162   pan=-0.5  lift=-4.0  elbow=-14.4
x=-0.004  y=+0.146 z=+0.162   pan=-0.8  lift=-4.0  elbow=-14.4
x=-0.005  y=+0.146 z=+0.162   pan=-1.1  lift=-4.0  elbow=-14.4
x=-0.007  y=+0.146 z=+0.162   pan=-1.4  lift=-4.0  elbow=-14.4

后面 N 键的 roll 走 0 → −4 → −8 → −12,O 键的 elbow 走 −14.4 → −11.4,也都进去了。每个键都在工作。

但 J 一共只转了 1.2°。真机上 1.2° 落在旷量里,肉眼就是没动;而 elbow 那 3° 勉强看得见——于是就成了「只有往上」。

两个原因,不是一个

其一:标度失衡。x 走 shoulder_pan,换算写死在 RealArm.PAN_SCALE_DEG_PER_M = 200,一步 5 mm 只有 1.0°,经 smoothing 0.3 每拍落 0.3°;而 y/z 走 IK 出 shoulder_lift+elbow_flex,同样 5 mm 是 0.69–0.97°。差 3.2 倍,所以四个键看着在动 lift、两个看着没反应。

其二:绝对幅度整体太小。这一条是我第一版漏掉的——只修失衡的话,J/L 从 0.30° 提到 0.90°,仍然要按 26 下才转 18°。日志表明六个键当时全在 1° 上下,问题不只是它们互相差多少。

两处都改

ikjluo最快/最慢转 18° 要按
修前0.69°0.69°0.30°0.30°0.97°0.95°3.2×60 下
只修失衡0.69°0.69°0.90°0.90°0.97°0.95°1.4×26 下
现在(首拍)1.11°1.11°1.44°1.44°1.55°1.52°1.4×—
现在(稳定)6.09°5.32°4.80°4.80°6.93°6.02°1.4×4 下

口径更正:先前这里只列了"首拍"值(按下后第一个 33 ms 周期),把 J/L 说成 1.44°。一次按压最终会走到 4.80°(修前 1.0°)。数值由 test_manual_axis_balance.py 喂进真的 ManualControl + PoseFilter + IK 量出,不是推算。测试阈值 2.0× 也是量完才定的——先写的 4.0 判不出 3.2×,那就是个永远通过的空测试。测试里有一段专门验证「去掉修复后它确实会红」。

② 键盘模式一进去手臂就水平向前伸直

操作者原话:「确保遥操作键盘模式不强制让他水平向前伸直 不如一开始就 home 位置」

确认属实,而且这就是当晚过热的起点。旧 home 的 IK 解是 lift -0.1° / elbow -22.9°——上臂几乎水平前伸,是整条臂力臂最长的姿势之一。遥操作一进去就归到这里,之后不管按不按键都一直顶着。那次会话 6889 拍里 6821 拍(99%)是 holding position。

旧 home (0.150, 0.170)100%(0.120, 0.170) ← 08-13 被退回85%新 home (0.100, 0.170)73%新 park (0.080, 0.170)61% shoulder_lift 静态保持力矩(以旧 home 为 100%)
四个候选姿势的 shoulder_lift 静态保持力矩(上臂/前臂当均质杆 + 腕爪当端点质量,只看比值)。

我的第一版修法是错的,而且会带回一个已经修掉的 bug。

我先把 home 改成 (y 0.060, z 0.090),力矩能降到 45%。但 robot_server.py 的开机自检里有一条我没读到:home 处爪尖必须离桌至少 100 mm。来历是 2026-08-16——home 当时离桌 78 mm,比杯子还矮,每次启动都把桌上的杯子扫倒,因为一上电两条臂立刻扫向 home。我那个 z=0.090 只有 29 mm。

所以 z = 0.170 不是没人维护的遗留值,是被这条规则顶上去的。能动的只有 y——收 y 同样是在收 shoulder_lift 的力臂(力臂≈手的水平前伸量),而且完全不影响爪尖高度:表里三个候选的离桌距离都是 109 / 105 mm,一模一样。

拆成两个姿势

一个数值同时服务两个目的必然有一头将就,所以拆开:

这里有一条需要上机复核的冲突。2026-08-13 有人把 y 降到 0.12,当天就被退回,理由是姿势太蜷。这次定的 0.10 比那次被退回的还蜷。

还是这么改,是因为那次讲的是"操作起来顺不顺手",这次是"没人操作时会不会烧",而当晚已经真的烧到降额了——是新证据,不是忘了旧结论。觉得蜷就把 home_pose.y 调回 0.12(力矩 85%)甚至 0.15,idle_park_pose 不用动:省力矩的主要部分在停放那一档。

顺带修掉的一处过时注释

vr_teleop.DEFAULT_POSE 的注释写着"从 home 往上还有 8.1 cm",那是按代码默认 z=0.10 算的。而配置文件把 z 覆盖成 0.170 之后,实际余量是 sqrt(0.235² − 0.15²) − 0.17 = 1.1 cm——几乎贴死在 max_reach 弧上。往上推会被向内缩回,手感就是"手臂不跟手、还往回退"。收 y 顺手把它修了:新 home 的上行余量 4.3 cm,是原来的四倍。

另外把"扫杯规则"补成了 idle_park_pose 的自动校验——我差点犯的这个错,下次会被直接拦住,不用靠人读代码。

③ 不停归位

操作者原话:「然后解决那个不停归位的bug 不然还要过热」

说实话:我没能复现它的字面描述。怀疑最大的那条路——A 键归位——是上升沿触发的(a_pressed and not prev_rehome_btn),按住不放只会触发一次;而冻结的 VR 流给不出按键状态,a_pressed 恒为 False。从代码上走不通。

但同一类风险里有一条确实存在,而且更危险:归位靠 pose_near() 判到位,如果永远到不了位,homing 就永远是 True,手臂会被无限驱动、持续通电发热——正是烧舵机的那个场景。

到不了位的现实原因不止一个:目标被工作区包络钳住(我上面那个 z=0.090 就会)、静摩擦、舵机出力不足(当晚过热那颗就是)。所以不去猜是哪一个,直接封死这一整类:

MAX_HOMING_S = 25.0
# 归位受 max_step 0.02 m/拍限制,工作区对角线约 0.35 m,30 Hz 下满程约 0.6 s;
# 再算上 smoothing 0.3 的指数收敛尾巴,正常归位 3-5 s 走完。25 s 是它的 5 倍,不会误伤。

超时就放弃、大声报出来、手臂停在当前位置并保持力矩(卸了会掉)。

测试用一个盒子外的目标制造"永远到不了位",验证两件事:归位在上限后确实放弃;放弃之后指令位姿不再往那个够不到的目标爬——归位时 19.37 mm / 0.2 s,放弃后 0.0152 mm / 0.4 s,慢了 1275 倍。这里不能要求严格为 0(smoothing 是指数收敛,永远有尾巴),所以判据用的是速度比值,阈值自校准。

验收

全部不碰硬件。当前 17 颗舵机仍是断力矩状态,32–34 °C,按「先让他放凉 完全最好就不通电」没有通电。

cd ~/Robotic_challenge/03-software/scripts
python test_manual_axis_balance.py     # ① 每个键的实际角度 + 阈值回归
python test_idle_park.py               # ③ 归位超时(第 ⑦ 项)
python test_teleop_manual_persist.py   # 之前两个 teleop bug 不回归
python test_commands_consistency.py    # 面板按钮 ↔ 功能模块
python test_thermal_guard.py
python teleop_config.py                # 打印新 home 的上下余量
测试结果
test_manual_axis_balance.py全部通过(含"未修复会红"的回归验证)
test_idle_park.py全部通过(7 组)
test_teleop_manual_persist.py全部通过
test_commands_consistency.py全部通过
test_thermal_guard.py全部通过

还没上机。以上都是软件层验证。vr_teleop 当前没在跑,这些改动要重启 teleop 才生效;新 home 蜷不蜷、J/L 的手感够不够,都要操作者实际按一遍才算数。

这一页答不了什么

改动:configs/teleop_safety.yaml(home + idle_park)、configs/manual_control.json(axis_scale)、scripts/vr_teleop.py、scripts/teleop_config.py、新增 scripts/test_manual_axis_balance.py。

相关:过热事件全过程 · 给主办方的通报 · 带载梯度扫描工具