为什么
------
应用层只认 arm_msgs/srv/GripperExecute(开口毫米),真机与仿真必须同一套接口。
但 GN01 的 URDF 里 <mimic> 是**线性近似**:直接按线性系数下发 finger_width_joint,
"命令开 20 mm"在 Gazebo 里量出来是 21.75 mm —— 抓不住工件,而且看起来像算法问题。
实测(2026-09-24,Gazebo 内量两指尖间距):
线性下发 20 mm → 实际 21.74 mm(偏差 1.74 mm,超 2 mm 抓取容差的前半步)
走 FK 反解 → 实际 20.00 mm(≤0.5 mm)
怎么改
------
- 新增 sim_gripper_kin.py:解析 URDF 正运动学,算出"主关节值 ↔ 真实开口 mm"的
单调曲线,反解用二分。曲线非单调/零行程时**主动报错**,不静默退化成线性。
- sim_gripper.py:开口换算全部走该曲线;容差统一改成毫米。
- sim_gripper_adapter.py:反馈改取**从动关节**(见下);is_gripped 按毫米判定。
- 新增 check_sim_gripper.py:三层验收(服务/开口一致/指尖真实几何行程 + 对称性)。
踩到的坑
--------
- finger_width_joint 挂在无质量的 unused_finger_width_link 上,与手指**没有机械
连接**:命令到 0.100,它的 /joint_states 恒为 1.4e-08,6 个从动关节才准到
0.7854。反馈若取它,/gripper/status 会永远报"开口 0 mm"。
- 残留进程会伪造出"代码写错了"的现场:旧 sim_gripper_adapter 与新实例抢话题,
验收读到"指尖间距 0.03 mm"(真实 127 mm);孤儿 static_transform_publisher
用 latched /tf_static 把旧相机外参按在新仿真上。
→ 新增 scripts/lib/sim_procs.sh;run_sim_gz.sh 用 setsid 独立进程组,
PGID 记进 log_sim/sim.pgid,stop 时整组 kill;启动前预检拒绝带病启动。
- 验收脚本自己也错过两次(把 TF 的米当毫米比 → 127 mm 显示成 0.13 mm;
用会转动指垫的中点漂移判对称 → 必然位移 16 mm)。判据改成"两侧各走多远",
单位只在 _dist_mm 里换算一次。
验证
----
python3 -m pytest src/arm_sim_gazebo/test/ -q → 307 passed
python3 scripts/check_sim_gripper.py → 三层全过
第 2 层:请求/反馈开口最大偏差 < 0.5 mm
第 3 层:期望行程 vs 实际行程、左右行程差,均 < 1.5 mm
bash scripts/stop_sim_gz.sh --list → 整组列出(含 static tf)
文档
----
- 仿真手册:新增 §4.4(开口 mm 怎么换算)、坑 12(宽度关节不动、
反馈必须取从动关节);停止一节改写成进程组语义。
- FRAMEWORK_RULES:新增 R4.11(残留进程会伪造现场)、R4.12(验收判据要挑
与实现无关的物理量);R4.9 表补夹爪;清单补 13/14 两条。
- HANDBOOK:新增 §3.4「在仿真里开合夹爪」,补 §4.4 的两条新教训。
Co-authored-by: Cursor <cursoragent@cursor.com>
31 KiB
框架规则(docs/FRAMEWORK_RULES.md)
这份文档给两类读者看:
- 改这个仓库的 AI:开工前先读,逐条遵守。这些不是风格建议,是硬规矩 —— 违反它们的改动会在真机上以"不报错但结果是错的"方式失败,而这正是本仓库 花最多代价在防的事。
- review 人:每条例规都有编号(
R1…),可以直接引用,例如 "这条违反 R7.2"。配套:
.cursor/rules/*.mdc是给 Cursor 的常驻规则(自动注入),内容是本文件的 压缩版;本文件是唯一权威,两者冲突时以本文件为准。规则是可以改的 —— 但必须改这里,而不是在代码里偷偷绕开。改规则时写清为什么。
0. 三条元规则
其余所有规则都是从这三条推出来的。拿不准某件事该怎么做时,回到这三条。
M1. 静默失效是头号敌人
本仓库遇到的大部分问题都不报错:话题在、图像有、TF 查得到、日志干净,结果是错的。 所以:
- 任何"检查 / 校验 / 验收"都必须能真的失败。写完先问一句:
"我故意把它弄坏,这条检查会红吗?" 不会红 = 这条检查等于没有(见
R5.2)。 - 宁可当场报错,也不要"宽容降级"。降级会把"没检查"伪装成"检查通过"。
真实案例:位姿解析函数格式对不上,于是永远返回空、调用方按设计"跳过位姿核对",
结果位姿检查从来没跑过,而日志只说"跳过"(见
R5.4)。 - 校验代码自己也会静默失效。凡是"检查 X 是否为空"的地方,都要问:
"它会不会被填成一个假值?"(真实案例:
sdformat用__default__/__default_topic__填缺失字段,见R4.6)
M2. 单一数据源
同一个事实只允许写一处,其余全部派生。
- 能算出来的不要抄(内参由视场与图幅算、相机位姿从 world 文件解析)。
- 必须抄的(如仿真侧要提前知道像素门槛)必须有人守着:
用脚本交叉核对真机配置,不一致就判失败(
R2.6)。 - 同一份事实出现两处时,一定有一处会先过期。过期的那处不会报错。
M3. 口径必须显式
单位、坐标系、符号、参考基准必须写下来,并且只在一个地方换算。
- 毫米还是米?相对零还是相对基线?压下去为正还是为负? 这些都不写清就会"数值看起来合理、语义是错的"。
- 换算只做一次,放在适配层(相机适配器、触觉适配器),下游不许再猜。
1. 配置与数据
R1.1 分层优先级固定:命令行 > site.yaml > 包内默认
config/site.yaml 是站点数据(SN/IP/相机序列号/锁高/权重路径),不进 git;
包内 config/*.yaml 是默认值,被 site 覆盖。
禁止在代码里硬编码站点值。真实反例:15 处硬编码的 Rizon4-063989
(见 R8 的清单),换一台臂就会静默连错机器。
R1.1.1 含臂名的一级配置分两类,别混
Flexiv 官方把 SN 编进了关节名(Rizon4-063989_joint1),所以"含 SN 的文件"
天然分成两类。新增文件时必须先归类,归错类就是下一颗哑弹:
| 类 | 特征 | 换臂时 | 例子 |
|---|---|---|---|
| 跟随当前臂 | 描述"这台臂的接口/结构",只可能有一个正确值 | 必须改,否则控制器报关节不存在 | arm_control/config/controllers.yaml、named_poses.yaml、arm_moveit_config/config/moveit_controllers.yaml、arm_moveit_config/srdf/arm.srdf、arm_bringup/config/bringup.yaml、arm_description/config/joint_names.yaml、arm_application/config/servo.yaml、xunhuan_trajectory.yaml |
| 按臂各留一份 | 记录某台臂的实测结果,多台共存是对的 | 不用改,该新增一份(如 named_poses_91.yaml) |
config/site.yaml、named_poses_89/91.yaml、arm_application/config/dual_grippers.yaml、handeye/waypoints.yaml |
第一类一定要显式检查,因为漏改的症状有两种,而且都能伪装成别的问题:
- 控制器起不来 →
Joint 'Rizon4-063989_joint1' not found(这条还算好查); - URDF 换了名字、SRDF 没换 → MoveIt 规划成功但机械臂不动(难查得多)。
所以有一把专用尺子:
python3 scripts/check_arm_prefixes.py # 一致则 0,不一致逐条列出 file:line + 修法
python3 scripts/check_arm_prefixes.py --list # 只看当前臂的应然前缀,以及它来自哪儿
换臂的完整动作(少一步就会出上面那种症状):
- 改
config/site.yaml(robot.sn+robot.robots[]); source scripts/env.sh --arm <新臂>;python3 scripts/check_arm_prefixes.py—— 必须输出 OK;bash scripts/build.sh(让install/同步)。
⚠️ 第 3 步为什么不能省:这个脚本的
_SN_TOKEN正则第一版写成\bRizon\d+-\d{6}\b,结果 SRDF 里 24 处硬编码一个都没报(_joint1的 下划线属于\w,\b不成立)。检查器的漏洞和配置的漏洞长得一模一样, 所以它自己也必须有负向测试(见 R5.2)。
⚠️
ROBOT_SN/ROBOT_ARM环境变量会覆盖site.yaml的robot.sn(为了支持"这个终端临时用另一台臂")。排查"明明改了配置却没生效"时, 先echo $ROBOT_SN——check_arm_prefixes.py --list会直接告诉你值从哪来。
R1.2 新增 site.yaml 键必须二选一:接线 或 明确告警
声明了却没接线的键是最难查的一类问题:用户按文档改了 site.yaml,行为毫无变化,
也不报错。所以每个新键都要么真的接线,要么在启动时告警(wired: false +
unwired_warnings() 那套做法)。
⚠️ 现状:
wired:目前只是manifest.yaml里的注释性约定,arm_interfaces.manifest并不解析它。所以"未接线"的保障实际来自sim_site.unwired_warnings()这类代码里的告警,不是清单。 加键时不要指望清单替你守法。
R1.3 设备一律走 devices/<kind>/<brand>/,并带 manifest.yaml
devices/<kind>/<brand>/
├── manifest.yaml # id/label/kind/vendor/version + assets + driver + capabilities + models + config_schema
├── driver/ # 与厂商 SDK 对接的实现(可替换)
└── models/<型号>/ # 该品牌的型号专属文件
kind用固定的那几个(robots/grippers/cameras/sensors/sim), 目录名要与arm_interfaces.KIND_FOLDERS一致。- 换品牌 = 新增一个目录,不是改现有目录。上层按
manifest.yaml选型, 不认具体品牌名。 assets[].required: true的物料缺失时,框架会报错并给出格式与命名要求 (不只是"文件不存在")。
R1.4 权重一律走 models/,规则在 models/README.md
- 二进制(
.pt/.onnx/.engine)不进 git;.labels.yaml(文本标签)要进。 - 转换链:
models/staging/放待转换的原始文件 →weights.sh convert→ 自动改名归位到models/<分组>/并固化标签。 models/**/best.pt这类悬空引用(配置指向不存在的文件)必须修掉或改成实际存在的, 查法:weights.sh doctor。
R1.5 什么入库、什么不入库
| 入库 | 不入库 | 理由 |
|---|---|---|
src/ scripts/ docs/ devices/ config/site.example.yaml |
config/site.yaml |
站点数据含本机 IP/序列号 |
models/README.md、models/**/*.labels.yaml |
models/**/*.{pt,onnx,engine} 等 |
二进制体积大、每台机器重转 |
handeye/poses/ handeye/results/ |
handeye/tmp/ |
示教点与标定结果是用真机时间换来的资产,要能回滚 |
config/site.yaml 改了以后必须重跑 python3 scripts/gen_site_example.py,
否则模板会漂移(有一条 parity 测试会红)。
2. 几何、单位与坐标系
R2.1 单位一律米与弧度
SDF / URDF / 几何计算里不允许出现毫米。用毫米建模的 mesh 必须先缩放。
例外且必须显式:真机 RealSense 的深度图是 16UC1 毫米(见 R2.3)。
R2.2 相机的 frame_id 必须是光学系
ROS 的 image / camera_info 必须用光学约定(Z 朝前 / X 朝右 / Y 朝下,REP-103/105),
不能用相机本体系(X 朝前 / Y 朝左 / Z 朝上)。两者差 rpy=(-90°, 0, -90°)。
盖错 frame 的后果:tf2 查询能查到、不报错,但反投影出的三维点整体转错,
而单目预览完全看不出来。
R2.3 深度:真机是毫米,Gazebo 是米 —— 必须真换算
| 来源 | 编码 | 单位 |
|---|---|---|
| 真机 RealSense aligned_depth | 16UC1 |
毫米,0 = 无读数 |
| Gazebo 深度相机 | 32FC1 |
米 |
直接改个名字转发,应用侧按毫米解释会整体差 1000 倍。换算只做一次,
放在 sim_camera_adapter。
R2.4 rpy 的复合顺序是 Rz·Ry·Rx
不是 Ry·Rz·Rx。单轴旋转看不出来,多轴组合才暴露。
URDF 相机的视线是本体 X 轴("X 朝前"约定)。所以"先俯 50° 再偏航 180°" 会把视线翻到背面。D435 就这么翻过:背对台面只看得见地面,什么都不报, 而且几何自洽(点确实落在真实地面上)。
R2.5 Gazebo 发的 camera_info 不可信
Fortress 恒发 fx=fy=277, cx=160, cy=120(它的默认 60° 视场 + 半分辨率主点),
与真正渲染用的视场无关。内参必须由 cameras.yaml 的 horizontal_fov 与图幅算出,
并覆盖 gz 发的那份。
R2.6 像素门槛与图幅绑定
hole_detect.yaml 的门槛(hough_min_radius、hole_distance_min)是像素量,
只在特定分辨率下有意义。改图幅要同时改三处(cameras.yaml、SDF、
必要时重算真机门槛),启动时会比对并告警。
R2.7 位姿检查要有容差,且容差要说明理由
物体的落座微动是正常的。卡到 1e-6 只会得到永远为真的告警,人就会把检查关掉。
默认 5mm 起步(见 sim_scene.pose_mismatches)。
3. 触觉与力
R3.1 力的语义是"相对静止基线的增量"
真机 Orisys 输出的是形变任意单位(非牛顿),且基线会漂移,所以全链路 (含下游阈值)都以相对基线的增量为语义。仿真必须做同样的事: 不减基线,下游阈值就对不上。
基线要慢跟踪(只在确认无接触时更新),否则换姿态后的残余会被当成接触。
R3.2 fnormal 压下去为正,且 clamp 到非负
两指符号一致。这条是实测结论(config/tactile.yaml 里有实测记录),
不是从 URDF 推导出来的。
R3.3 轴向映射必须实测,不许推导
哪个轴是"指腹法向"要用实验确定(夹一块大平面薄板等稳态)。猜错不会报错,
只会让所有力的语义静默错位。实测方法与结果记在 config/tactile.yaml。
用小方块做这个实验是错的:点/边接触会让法向偏、摩擦混进来。
R3.4 接触布尔必须用超时判据
Gazebo 的接触消息是事件式的:接触时发,离开时一条都不发。
写成"收到=接触 / 没收到=不管",离开工件后 contact 会永远停在 True。
判据 = "最近 contact_timeout_s 内收到过消息"。
R3.5 仿真与真机的力的量级不同,force_scale 是必须确认的口径
仿真输出 = Gazebo 牛顿 × force_scale。force_scale = 1.0 时输出牛顿,
不能直接套用真机那套阈值(250/600)。位置控制器是"刚性地压进去"的,
实测能到几百。
选错口径不报任何错:阈值只是"不触发"或"一碰就炸",看起来像算法问题。
R3.6 视触觉的图像在仿真里只能是占位
仿真没有弹性体成像,深度图/力矢量图只能是空/NaN。话题与消息结构要保留 (下游不用改代码),但要打一次明确日志说明它不是真实数据。
4. 仿真(Gazebo)
R4.1 仿真与真机共用同一套 site.yaml 参数
任何"仿真专用参数"都是未来"仿真里调好的值搬到真机不生效"的隐患。
只允许物理引擎自身的参数(重力、步长)放在 sim.* 下。
R4.2 world 里声明任何一个 <plugin> 都会挤掉默认系统
声明了任何插件后,Gazebo 不再加载默认的 Physics / UserCommands / SceneBroadcaster。
少了 UserCommands 的后果最阴:/world/<名>/create 服务不存在 → 模型根本没进 Gazebo,
而 create 仍然打印 OK creation of entity。
所以这些系统必须在 world 里显式声明(含 contact-system、forcetorque-system)。
R4.3 插件与资源的搜索路径必须在 ros2 launch 之前设好
conda 激活不会往 LD_LIBRARY_PATH 加东西,而 gz 的插件搜索路径需要
$CONDA_PREFIX/lib。症状是 couldn't find shared library 而库其实就在那儿。
不要绕过 scripts/sim_env.sh 直接 ros2 launch。
R4.4 上游描述(flexiv_description 等)不许直接改
第三方包一律在启动时归一化,不在上游文件里下手:
- 缺
<inertial>/ 零质量 link → NaN 惯性、物体消失 <mimic>标签被sdformat丢弃 → 夹爪"控制器 active 但手指不动"- 无质量的根 link(world link)会连整棵子树被静默丢掉
改上游还有个额外代价:下次 fetch_flexiv.sh 会把你的改动覆盖掉。
R4.5 场景物体走 scene.yaml,不进 world
world 是"环境"(地面/台面/相机/系统插件),scene.yaml 是"这一批活做什么"。
动 world 的风险面太大(R4.2)。
模型约定:原点在底面中心、单位米、目录名 = <model name> = model.config
里的 <name> = 推理类别名。
R4.6 模型/场景校验必须逐碰撞体、三维判
- 不能拿总包围盒判重叠:正常装配里本来就有嵌套(规立在规座方孔里), 粗判会把正常嵌套报成碰撞,逼着人把检查关掉。
- 恰好贴合不算重叠(规的台肩正好坐在规座顶面上)。
- 变换必须用"中心 + 半宽":对 min/max 取绝对值再对称化会把偏移量吃掉、 盒子凭空变大。
- 校验要用从 SDF 解析出来的几何,不能在
scene.yaml里再抄一份尺寸。
R4.7 建模:碰撞体用简单几何,视觉可以加"只给算法看"的细节
<collision> 决定物理,用 box/cylinder;<visual> 决定相机看见什么,
可以加亮环、色块这类只服务于检测的东西。
工件的碰撞体是方孔、视觉是圆环,这是刻意的:真机的 HoughCircles 靠亮环找孔, 只按碰撞体建视觉会让孔检测在仿真里永远失败,而那看起来像算法问题。
由此产生的约定:
visual的名字有意义(insert_*_ring/center_bore),sim_scene.features_from_workpiece按名字前缀读特征。改名字要同步改那个函数。
R4.8 夹爪/被夹物体的接触刚度要按"位置控制"的实际行为调
位置控制器会"刚性地压进去",默认刚性接触会把物体弹飞。需要放软时要写明理由 (照抄现成参数前先想清楚你的物体是不是也要被夹)。
R4.9 不用"有图像 / 有话题 / TF 查得到"验收
这些只能证明"链路通"。验收必须是对物理量的断言:
| 工具 | 判据 |
|---|---|
scripts/check_sim_cameras.py |
深度反投影必须命中已知平面(地面 z=0 / 台面 z=0.40) |
scripts/check_sim_scene.py |
摆放自洽 + 像素门槛与真机一致 + 成像几何达标 + 手外相机完整看得见工件 |
scripts/check_sim_gripper.py |
命令开口 mm → 两指尖在 world 里的实际间距变化同样的 mm;两侧行程相等;指尖坐标在 --verbose 里打出来(判错时可以回看它到底取了哪两个点) |
R4.10 停仿真用 scripts/stop_sim_gz.sh,不要 pkill -f
pkill -f 会先匹配到调用它的那个 shell(它的命令行里含同样的字符串),
表现是"命令秒返回、什么都没停"。
更重要的是:残留进程比"没停干净"严重得多(R4.11 解释为什么)。 脚本走两条路:进程组(主)+ 模式扫描(兜底),杀完复查残留并非零退出。
R4.11 仿真栈必须能"整组停掉":残留会伪造出"代码写错了"的现场
实测(2026-09-24)踩到的两个残留,症状与"夹爪换算写错了"一模一样:
| 残留的进程 | 它干了什么 | 现象 |
|---|---|---|
sim_gripper_adapter(旧会话的) |
与新适配器同时订阅 /joint_states、同时发布 /gripper/status;服务调用被随机分给其中一个 |
验收读到"开口恒为 0 mm、指尖间距 0.03 mm"(真实 127 mm) |
static_transform_publisher(5 个孤儿) |
发 latched 的 /tf_static,把旧相机外参一直按在新仿真的 TF 树上 |
TF 查询返回互相矛盾的数据 |
所以定下三条:
- 仿真必须整栈起在自己的会话/进程组里(
setsid,PGID 记在log_sim/sim.pgid), 停止时kill -TERM -PGID一次带走 —— 不依赖任何"进程名模式表"; - 模式扫描只当兜底,且除点名表外要有兜底规则(命令行含
<仓库>/install_sim/就算仿真进程),这样"以后新增节点忘了改表"不会再留孤儿; - 启动前预检 + 验收第一步查重名节点:宁可拒绝启动,也不给一个 "看起来能跑、数据是错的"仿真。
R4.12 验收工具自己也会说谎:判据要挑"与实现无关的物理量"
check_sim_gripper.py 的第 3 层先后给过两个错误结论,都是脚本自己的问题:
| 写出来的判据 | 为什么会错 |
|---|---|
| 指尖间距差(米)与期望(毫米)比较 | 漏了 ×1000:127 mm 显示成 0.13 mm,恰好看起来像夹爪没张开 |
| "两指中点漂移"判对称 | tip frame 固连在会转动的指垫上、走的是弧线,中点必然位移 16 mm;而左右行程其实相等(52.56 / 52.56 mm) |
推论:
- 单位换算只在一个函数里做(
_dist_mm),别处不许再乘 1000; - 判据要挑与机构无关的量("两侧各走了多远"),别挑带方向的量再投影;
--verbose要把原始物理量(指尖坐标)打出来,判错时能回看;- 结论不对时,先怀疑判据,再怀疑被测对象 —— 上面两条都是先怀疑了代码。
5. 代码与测试
R5.1 纯逻辑要抽成纯函数,才可能被钉住
把"数据变换"从 ROS 节点里抽出来(sim_cameras、sim_tactile、sim_scene),
因为静默失效点的错误不会在运行时暴露:轴向映射反了照样"有力、会响应、
松开归零"。抽出来才有单测可写。
R5.2 每条"检查/校验"都要有能证明它有效的测试
尤其是负向测试:故意造出坏数据,断言检查会报错。 只测"好数据通过"的测试,无法区分"检查有效"和"检查恒真"。
R5.3 不许把"降级跳过"写成"通过"
调用方必须把降级当作告警打印出来并说明原因。真实反例见 R5.4。
R5.4 解析外部输出:先确认真实格式,再写解析器
外部工具(ign、sdformat、厂商 SDK)的文本输出没有稳定契约,
而且格式与直觉不一致是常态。
先用真实输出跑一遍看清楚了再写解析;解析失败要返回"未知"而不是"零"。
真实反例:按 x: N 键值对写解析器,而 Fortress 实际输出是方括号两行,
于是位姿检查从来没跑过。
R5.5 删除 / 重构前先做全仓库引用扫描
不靠印象。反例:一个 assets_root() 函数指向从未存在的 assets/ 目录、
零调用者,长期没人发现(见 R8)。
R5.6 不要在中文路径下 colcon build
ROS 构建对非 ASCII 路径会出各种问题。工作目录保持 ASCII(推荐 ~/ros2)。
R5.7 真机与仿真的构建产物必须隔离
build/ install/ log/ 是真机;build_sim/ install_sim/ log_sim/ 是仿真
(scripts/build_sim.sh)。两套并存是刻意的:同一份源码,两个环境的依赖路径不同,
共用 install/ 会互相覆盖。
6. 文档与提交
R6.1 文档与实现不一致时:以实现为准,立刻修文档
文档里"已就绪 / 未接线"必须如实标注。看到 ✘ 不要凭印象改成 ✔。
R6.2 每份文档要有"读法"和"这份文档记录什么事实"的说明
本仓库的文档面向"明天就要动手的人",所以要写清:先读哪节、哪些是硬事实、 哪些是踩过的坑、怎么验证。
R6.3 结论性数字必须能复现
写进文档的性能/几何数字(如"台面命中 11.9%")要么给出命令,要么给出脚本名。
R6.4 提交信息:中文,讲为什么,附实测验收数据
格式:<阶段号> <一句话结果> + 正文说明"问题是什么、为什么这么改、怎么验证的"。
修静默失效时,把"症状"写进去 —— 那是后来人搜索时唯一能匹配上的东西。
R6.5 注释、docstring、提示信息一律用中文
现场操作的人读中文。所以本仓库(src/arm_*/**、scripts/**)里:
- 注释、docstring、
print/日志/异常里给人看的句子 = 中文 - 标识符、API 名、话题名、参数名、字段名、文件名、命令名 = 保留英文原文,
写在中文句子里时用双反引号包起来,例如
SITE_ROS2_ROOT、devices/<kind>/<brand>/ - 单位、十六进制、协议报文、CRC 值、寄存器地址、命令示例原样保留
例外:src/flexiv_*/ 是厂商上游代码,不许改、也不许翻译(见 R4.4)。
只改文本的提交,必须能自证"没碰到逻辑"。做法:把新旧两份源码都解析成 AST, 删掉 docstring、把字符串常量抹平、把 f-string 只留"插值了几次 + 插值是什么", 再比对 —— 必须完全相同。这条检查会抓出"顺手改名""顺手调整语句顺序""漏掉一个 f-string 插值"这类问题;而"为了中文语序把两个插值换个显示顺序"属于允许的改动。
工具:python3 scripts/check_text_only.py(基准是 git HEAD,所以要在提交前跑)。
R6.6 行尾一律 LF;验脚本要真跑,不能只看 bash -n
行尾由 .gitattributes 兜住(* text=auto eol=lf,厂商上游 -text 不动)。
为什么专门立一条:scripts/run_hole_detect.sh 和 scripts/run_dual_gripper.sh
曾经是 CRLF,于是 bash 把行尾的 \r 当成标识符的一部分,
case "$1" in\r、set -eo pipefail\r 全部变成非法记号 ——
文件在、五处文档说能用、命令一敲就死,而报错信息里的 \r 根本看不出来。
更坑的是:bash -n 对其中一个还返回 OK,实际执行立刻死在第 6 行。
所以"静态检查通过"不等于"脚本能用":
- 验语法:
bash -n(必要但不充分,CRLF 会漏报) - 验能用:
bash <脚本> --help - 验行尾:
file <文件>,或看.gitattributes有没有覆盖到
R6.7 验收命令必须是真跑过的
写进文档的任何命令,都要当场执行一遍再写。理由是这个仓库已经踩过两次:
run_hole_detect.sh/run_dual_gripper.sh是 CRLF,五处文档让用户跑, 一敲就死(R6.6);bash -n对其中一个还报 OK。
推论:
- 文档里的命令复制粘贴要能跑,别凭记忆写;
- 外链先确认目标文件存在(改完文档跑一次链接检查);
- 文档说"已就绪"的能力,要能指出用哪条命令证明它已就绪(R6.1)。 指不出来的,就老实标 ❌。
R6.8 真机与仿真的 install/ 产物会互相污染(已发现,待修)
colcon build 会把当时激活的那个 python 的绝对路径写进每个 console script 的
shebang。于是:
$ head -1 install/arm_interfaces/lib/arm_interfaces/device_check
#!/home/cc/anaconda3/envs/ros_gz_sim/bin/python3.11 ← 仿真环境的 python
.gitattributes 管不到这件事,install/ 也不入库,所以它只能靠规矩和检查:
# 真机 install/ 里有多少入口脚本的 shebang 指向仿真环境(正常应当是 0)
grep -l ros_gz_sim install/*/lib/*/* | wc -l
当前实测:install/ 是 31 / 59,install_sim/ 是 31 / 55(后者是对的)。
即真机产物是在仿真 conda 环境里构建出来的,违反 R5.7。
为什么现在没炸:两个 conda 环境的 Python 依赖当前完全相同
(pyrealsense2 / serial / cv2 / numpy / torch / ultralytics / tensorrt 都在),
所以指错了也照样 import 成功。这是一颗哑弹:哪天某个环境多装/少装一个包,
真机栈就会以"模块找不到"的形式炸,而人是不会想到去查 shebang 的。
修法:在真机 conda 环境里重建 install/:
source scripts/env.sh # 确认 python3 指向 ros_flexiv
./scripts/build.sh # 只重建 arm_* 与 flexiv_description
grep -l ros_gz_sim install/*/lib/*/* | wc -l # 应当变成 0
预防:构建脚本开头应当断言当前 conda 环境与目标一致(待补)。
7. 改动前的检查清单
改任何东西之前/之后,逐条过一遍:
| # | 检查 | 对应规则 |
|---|---|---|
| 1 | 我要改的这个事实,仓库里有几处在写?其余的是派生的吗? | M2 |
| 2 | 我加/改的配置键,真的接线了吗?没接线会告警吗? | R1.2 |
| 3 | 涉及单位/坐标系/符号吗?换算只做了一次吗?写下来了吗? | M3、R2 |
| 4 | 我加的检查,故意弄坏数据它会红吗?有负向测试吗? | M1、R5.2 |
| 5 | 解析外部输出前,我看过真实输出了吗? | R5.4 |
| 6 | 我改的是上游第三方文件吗?(应该走归一化/适配层) | R4.4 |
| 7 | 改完跑了验收工具吗(check_sim_cameras / check_sim_scene)? |
R4.9 |
| 8 | 文档里的状态标注与实现还一致吗? | R6.1 |
| 9 | 提交信息里有"为什么"和实测数据吗? | R6.4 |
| 10 | 我写进文档的命令,自己跑过一遍吗? | R6.7 |
| 11 | 这次构建是在目标环境里做的吗?(head -1 install/*/lib/*/*) |
R6.8、R5.7 |
| 12 | 我碰的文件属于"跟随当前臂"还是"按臂各留一份"?改完跑 check_arm_prefixes.py 了吗? |
R1.1.1 |
| 13 | 我验收用的判据,是"与被测实现无关的物理量"吗?单位只在一处换算了吗? | R4.12 |
| 14 | 我起过仿真/长跑进程吗?停干净了吗(stop_sim_gz.sh --list 应为空)? |
R4.10、R4.11 |
8. 历史反例清单(别再犯)
按"症状"排序 —— 因为出问题时你是先看到症状,再回来查这张表。
| 症状 | 真实原因 | 规则 |
|---|---|---|
| 三维点整体偏半米,但单目预览正常 | 用了 gz 发的 camera_info(恒为 60° 视场的默认值) |
R2.5 |
深度差 1000 倍 / frombuffer 读出垃圾 |
把 gz 的 32FC1 米直接当毫米用 |
R2.3 |
反投影的点转到别处,tf2 却能查到 |
frame_id 盖成了相机本体系 |
R2.2 |
| 相机背对目标,只看得见地面 | rpy 复合顺序理解错 / 视线是本体 X 轴 |
R2.4 |
模型"根本没进 Gazebo",但 create 打印成功 |
world 里声明了插件,挤掉了 UserCommands | R4.2 |
gz_ros2_control 报找不到共享库,但 ldd 全齐 |
插件搜索路径没含 $CONDA_PREFIX/lib |
R4.3 |
| 物体一 spawn 就消失 | 缺 <inertial> / 零质量 link → 惯性 NaN |
R4.4 |
| 夹爪控制器 active 但手指不动 | <mimic> 被 sdformat 丢弃 |
R4.4 |
| 世界相机与它的话题一起消失 | 挂在无质量的 world link 上,整棵子树被丢 | R4.4 |
contact 离开工件后永远是 True |
接触消息是事件式的,没有"离开"事件 | R3.4 |
| 力的语义全错,但"有力、会响应、松开归零" | 轴向映射猜的,没实测 | R3.3 |
明明夹住了,fnormal 恒为 0 |
基线把真实接触吃掉了(基线更新条件写错) | R3.1 |
| 阈值永不触发或一碰就炸 | 仿真牛顿与真机任意单位混用(force_scale 没标定) |
R3.5 |
| 位姿检查从来没跑过,日志只说"跳过" | 解析器格式对不上,降级把"没检查"伪装成通过 | M1、R5.4 |
| 校验代码永远通过 | sdformat 用 __default__ / __default_topic__ 填了缺失字段 |
M1 |
| 报出根本不存在的碰撞 | 包围盒变换对 min/max 取了绝对值、把偏移吃掉 | R4.6 |
| 正常嵌套被判成重叠,只好把检查关掉 | 拿总包围盒判重叠,没逐碰撞体三维判 | R4.6 |
| 孔检测在仿真里根本不可能通过 | 仿真分辨率与真机不同,像素门槛失效 | R2.6 |
| 仿真"通过"、真机不认 | 像素门槛抄了两份,漂移了 | M2、R2.6 |
| 脚本说"已停止",进程还在跑 | 进程模式表漏项(漏过 robot_state_publisher) |
M1、R4.10 |
pkill -f gz 秒返回、什么都没停 |
它先匹配到调用它的 shell | R4.10 |
| 换了台臂却连错机器 | 硬编码的 Rizon4-063989 默认值 |
R1.1 |
| 换了台臂,MoveIt 说规划成功、机械臂不动 | SRDF/controllers 还是旧 SN 的关节名(跟随臂的一级配置漏改) | R1.1.1 |
| 前缀检查器"全部通过",其实一处都没查 | 正则用 \b 收尾,而关节名是 Rizon4-063989_joint1 |
R5.2、R1.1.1 |
assets_root() 指向从未存在的 assets/ |
零调用者的死代码,没人发现 | R5.5 |
脚本报 $'\r': 未找到命令、set: pipefail: 无效的选项名 |
脚本是 CRLF 行尾,\r 被当成标识符的一部分 |
R6.6 |
bash -n 说没问题,真跑立刻死 |
bash -n 对 CRLF 会漏报 —— 静态检查≠能用 |
R6.6 |
| 代码里一半英文 docstring 一半中文注释 | 没有统一口径,谁写谁定 | R6.5 |
| 真机栈哪天突然报"模块找不到",明明装过 | install/ 的入口脚本 shebang 指向仿真环境的 python(哑弹) |
R6.8 |
| 照着文档敲命令,命令根本不存在 | 写文档时没真跑过 | R6.7 |
夹爪明明开着,status 一直报"开口 0 mm" |
反馈取了"虚拟宽度关节",它在 Gazebo 里根本不动 | R4.11、坑 12 |
| 验收说指尖只张开 0.13 mm(其实 127 mm) | 验收脚本自己漏了 ×1000(米 vs 毫米) | R4.12 |
| 服务/话题/TF 全在,但读数全是乱的 | 上一轮的 sim_gripper_adapter / static_transform_publisher 还活着 |
R4.11 |
| 命令"开 20 mm",两指实际 21.75 mm,夹不住工件 | 上游 <mimic> 是线性近似,开口必须走正运动学反解 |
R4.4、坑 12 |
| 残留进程把旧相机外参按在新仿真上 | static_transform_publisher 发的是 latched /tf_static |
R4.11 |
9. 这份文档怎么用(给 AI 的具体指令)
改这个仓库时:
- 先读第 0 节,记住三条元规则。
- 按第 7 节的清单逐条过。
- 出问题时先查第 8 节(按症状查)。
- 要在代码里"绕开"某条规则时,先改这份文档并写清理由 —— 绕开而不改文档,下一个人会按文档改回去。