操作者报了三件事:右臂按什么键都只往上、键盘模式一进去手臂就水平前伸、以及不停归位。没有一条的结论和它听起来的样子一样。最后查清楚的版本是:「每个键都是往上」是准确描述,而且它有物理原因——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(),不再自己写循环。
操作者原话:「我测试了 不管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° 上下,问题不只是它们互相差多少。
manual_control.json 加 axis_scale: {"x": 3.0}——修失衡。放配置不写死:换臂长或改 PAN_SCALE 就该重调。step_m 0.005 → 0.008——修幅度。不取 0.010 是因为配置里既有的约定要求 step_m < max_step_m(0.010),留 20% 余量;实测 0.008 配 3.0 倍不触发削边。| i | k | j | l | u | o | 最快/最慢 | 转 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。
我的第一版修法是错的,而且会带回一个已经修掉的 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,一模一样。
一个数值同时服务两个目的必然有一头将就,所以拆开:
home_pose 已退回 (0.150, 0.170)——收到 0.10 是错的:它把 lift 从 −0.1° 推到 −25.3°,等于拿掉了"大臂水平"这个唯一的视觉参照,操作者立刻报「只会往上抬」。省力矩交给停放档就够了。idle_park_pose (0.080, 0.170)——无人操作 180 s 后缩回,保持力矩 61%。过热发生在无人看管时段,而这正是它覆盖的;有人操作时手臂本来就在动。这里有一条需要上机复核的冲突。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 的手感够不够,都要操作者实际按一遍才算数。