Files
ros_contorl/docs/FRAMEWORK_RULES.md
T
arm_dev f5ed44a987 配置:消灭代码里的硬编码臂 SN,并给"换臂漏改"加一把尺子
## 为什么

"设备 ID 全部可配置"这条要求在代码里只做了一半:site.yaml 能配,
但**代码和静态配置里到处躺着 Rizon4-063989 当兜底**。这类兜底的危害
不是"改起来麻烦",而是它把"配置没生效"伪装成"配置生效了":

  * `robot_sn` 默认 `Rizon4-063989` → 换臂后节点去订**上一台臂**的关节话题,
    节点起来了、不报错、什么都不动;
  * launch 里用 `ee_frame == "Rizon4-063989_flange"` 当"用户没指定"的哨兵 →
    换个站点这个判断永远不成立,于是 site.yaml 里的 ee_frame 再也不会被用上,
    静态 TF 挂错坐标系、点还照样查得到;
  * `if SN == "Rizon4-063989"` 决定要不要拉触觉服务 → 换臂后触觉静默消失。

## 改了什么

1. **删掉所有"兜底成某台臂"的默认值**(15 处脚本 + 6 个 launch + 6 个节点)。
   没配就为空 / 直接 `:?` 报错,宁可起不来也不要连错机器:
   - `scripts/lib/export_site_env.py`:别名解析改为从 `robots[]` 现算
     (`arm_short_id` / `resolve_robot_sn` / `describe_arms`),选不出臂时
     抛 `ValueError`;`main()` 捕获后打人话 + 退出码 2(供 shell `eval`);
   - 手眼文件/路点/工作目录由当前臂 short id 派生,不再写死 `89_*`;
   - `ROBOT_HAS_TACTILE` 改为读 site.yaml 的 `robots[].tactile`;
   - `arm_application/site_io.py` 新增 `joint_prefix_default()` /
     `base_frame_default()` / `ee_frame_default()`:优先读 `SITE_*`,
     退而由 `SITE_ROBOT_SN` 现算,**不写死 SN**;
   - `arm.urdf.xacro` 的 `robot_sn` 默认值改成明显的占位符 `UNSET_ARM_SN`
     (关节名会带出来,报错信息自解释);
   - `ros2_control.launch.py` / `move_group.launch.py` **原先根本不传
     robot_sn**,靠 xacro 写死默认值"蒙对"本机,现在都从环境取。

2. **新增 `scripts/check_arm_prefixes.py`**:Flexiv 把 SN 编进关节名,
   所以有一批一级配置(SRDF/controllers/joint_names/bringup/servo/...)
   必须跟着当前臂走。漏改的症状是"MoveIt 规划成功但机械臂不动",很难查。
   这个脚本逐文件逐行列出对不上的地方,并直接给出修法。
   `--list` 还会说明 SN 来自 site.yaml 还是被 `ROBOT_SN` 覆盖。

3. **文档**:`FRAMEWORK_RULES.md` 新增 R1.1.1(两类含臂名文件怎么分、
   换臂四步)+ 检查清单第 12 条 + 反例 2 条;`HANDBOOK.md` 新增 3.6.1
   与症状表 2 行。

4. **顺带修掉的真 bug**(都是这次改动带出来、被 pyflakes + 真跑抓住的):
   - `convert_flexiv_traj.py` / `rdk_drag_bridge.py` 用了 `os` 但没 import
     → `--help` 直接 NameError;
   - `check_arm_prefixes.py` 第一版正则用 `\b` 收尾,而关节名是
     `Rizon4-063989_joint1`(`_` 属于 \w,`\b` 不成立)→ SRDF 里 24 处
     硬编码**一处都没报**,还打印 OK。已加负向测试钉住;
   - 删掉若干死代码/死 import(`pos_line`/`flat`/`names`、`IfCondition` 等)。

## 验收(都真跑过)

- `python3 -m pyflakes <全部改过的 py>` → 干净
- 逐包 pytest:233 + 69 + 78 + 24 + 236 = **640 passed**
- `env -u ROBOT_SN python3 scripts/check_arm_prefixes.py` → OK(8 文件)
- 换成 SN=Rizon4-777001 的临时 site.yaml → 报出**8 个**文件不一致并给修法(退出码 1)
- 故意不给 SITE_* → 各 launch 的 `robot_sn` / `ee_frame` / `handeye_file`
  默认值为 `''`,不再悄悄指向上一台臂
- `ros2 launch ... --show-args` 对 stack/sim/ros2_control/mock_control/
  move_group/display/visual_servo **全部通过**
- `convert_flexiv_traj.py --help` 正常;拿 `xunhuan.traj` 真转 5 个路点,
  输出 `joint_prefix: "Rizon4-063989_"` 来自 site.yaml

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-24 09:18:44 +08:00

547 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 框架规则(`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 规划成功但**机械臂不动**(难查得多)。
所以有一把专用尺子:
```bash
python3 scripts/check_arm_prefixes.py # 一致则 0,不一致逐条列出 file:line + 修法
python3 scripts/check_arm_prefixes.py --list # 只看当前臂的应然前缀,以及它来自哪儿
```
**换臂的完整动作**(少一步就会出上面那种症状):
1. 改 `config/site.yaml`(`robot.sn` + `robot.robots[]`);
2. `source scripts/env.sh --arm <新臂>`;
3. `python3 scripts/check_arm_prefixes.py` —— **必须输出 OK**;
4. `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` | 摆放自洽 + 像素门槛与真机一致 + 成像几何达标 + 手外相机**完整**看得见工件 |
### R4.10 停仿真用 `scripts/stop_sim_gz.sh`,不要 `pkill -f`
`pkill -f` 会先匹配到**调用它的那个 shell**(它的命令行里含同样的字符串),
表现是"命令秒返回、什么都没停"。脚本按进程树精确识别,并在杀完之后**复查残留**。
---
## 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/` 也不入库,所以它只能靠**规矩**和**检查**:
```bash
# 真机 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/`:
```bash
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 |
---
## 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 |
---
## 9. 这份文档怎么用(给 AI 的具体指令)
改这个仓库时:
1. 先读第 0 节,记住三条元规则。
2. 按第 7 节的清单逐条过。
3. 出问题时先查第 8 节(按症状查)。
4. 要在代码里"绕开"某条规则时,**先改这份文档**并写清理由 ——
绕开而不改文档,下一个人会按文档改回去。