通止规装配:配置段进 site.yaml + 中文手册 + 成败判定「不看 $?」

配置(site.yaml → gauge_assembly:,38 键全部带中文注释)
------------------------------------------------------
- 键名/单位/类型/交叉矛盾由 gauge_config.py 校验:拼错键名报错并给最相近的候选,
  单位后缀(_deg/_mm/_m/_s/_pct/_delta)逐条对表,`contact_safe_delta ≤
  contact_tangential_delta` 这类矛盾启动就拦(R1.2:没接线的键不许静默存在)。
- 借机清掉 4 个**没接线**的键(require_preview / track_*_s / hover_m:前两个是
  stage1 那层的、后两个是 servo 那层的,放在这里等于第二份真相),
  并把 contact_speed_m_s / coarse_timeout_s / 夹爪力速 真的接上。
- site.example.yaml 由 site.yaml 生成(gen_site_example.py --check 通过)。

成败判定:`ros2 launch` 的退出码**恒为 0**
------------------------------------------
实测(Humble):子进程 exit 3,`ros2 launch` 仍返回 0。用 `$?` 判成败的脚本会把
"装配失败"记成"成功"——没人会去查一个说成功的脚本。改成:

- 节点把结论**落盘**(--result-file,默认 /tmp/gauge_assembly_result.json,原子写、
  先落盘再发话题),任何"没跑到收尾"的退出也会补一份 ok=false;
- 脚本读结论文件判成败:0=成功、1=失败、**文件不存在也算失败**并说清;
- 新增 test_gauge_result_gate.py:把脚本里那段判成败的 Python 抽出来,
  对成功/失败/干跑/缺文件/坏 JSON 五种输入断言退出码 —— 判据本身也有测试。

顺带修掉两个"看着像玄学"的现象
------------------------------
- **流程打完了终端不回提示符**:launch 里的静态 TF 发布者是常驻的,主节点退出
  带不走它 —— 挂 OnProcessExit → Shutdown。
- **"已装 1/2 孔"**:结果里 hole_index 是 0 基当前孔号,不是"装完几个",
  新增 holes_done 给人和脚本看。

文档(全部中文,含"怎么调参/还没做")
------------------------------------
- 新增 docs/GAUGE_ASSEMBLY.md:状态机全图、每步判据表、E1 掉件处理(**按实现**:
  掉件后该孔作废、换下一个孔;最后一个孔掉件则序号被夹住=重试本孔)、
  话题契约、调参顺序、验收命令、踩过的坑。
- FRAMEWORK_RULES 新增 R5.8(同一份数学只许一份 + 守卫测试)、R5.9(自检的
  期望类型与调用处同源)、R5.10(缺依赖 skip,别留常年红)、R6.9(硬退出前打
  traceback)、R6.10(ros2 launch 退出码恒 0)+ R5.7 补真机/仿真"生效方式不同"
  (真机软链即改即生效,仿真拷贝必须重编)+ 5 条症状反例、.cursor/rules 同步。
- WORKSPACE / INTERFACES / SITE_CONFIG / HANDBOOK / 两份 README 索引与决策表补齐。

验证(本机真跑)
----------------
  pytest(仿真环境)  436 passed, 14 skipped(缺 onnx 跳过)
  干跑 --sim --dry-run  → 脚本退出码 0,结论 ok=true  已装 2/2 孔
  不干跑 --sim          → 脚本退出码 1,S00 拦住,未发任何运动指令

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
arm_dev
2026-09-24 10:30:11 +08:00
parent dfc945b7e0
commit bda2fa7129
16 changed files with 741 additions and 37 deletions
+88
View File
@@ -405,6 +405,53 @@ ROS 构建对非 ASCII 路径会出各种问题。工作目录保持 ASCII(推
(`scripts/build_sim.sh`)。两套并存是刻意的:同一份源码,两个环境的依赖路径不同,
共用 `install/` 会互相覆盖。
**但两者的"生效方式"不一样,这一点比隔离本身更容易坑人**:
| 环境 | 构建 | 产物形态 | 改了源码之后 |
|------|------|----------|--------------|
| 真机 | `build.sh`(`--symlink-install`) | **软链** | 立即生效 |
| 仿真 | `build_sim.sh`(**有意不加** `--symlink-install`:仿真 conda 的 setuptools 旧,不认 `--editable`) | **拷贝** | **必须重新构建**,否则跑的是旧拷贝 |
所以"在仿真里验证一个改动"之前,先确认 `install_sim/` 里的那份和源码一致:
```bash
diff src/arm_application/arm_application/<文件> \
install_sim/arm_application/lib/python3*/site-packages/arm_application/<同名>
```
不一致时的现象是"我明明改了,行为却和没改一样",而且**没有任何提示** ——
最容易被误判成"我的改动没效果"。
### R5.8 同一份数学/解析实现只许有一份,并加**守卫测试**
四元数换算曾经在 `pipeline` / `stage2` / `stage3` / `visual_servo` 四个 launch 里
各写一份,退化分支(180°/90°)的公式**各不相同** —— 也就是"同一个手眼 TF,
不同 launch 发出来不是同一个",而四种都能"看起来正常"。
规矩:
- 换算/解析只留一处(本例:`handeye_lib.read_transform_yaml`);
- 加一条**守护测试**扫描调用方,禁止它们再出现私有实现:
见 `test_handeye_lib_roundtrip.py::test_launch_files_do_not_reimplement_the_quaternion_math`;
- 测试要覆盖**分支点**(本例用 180°/270° 这些会让公式退化的角度),而不是只测一般值。
### R5.9 自检/探头里的"期望类型"必须与调用处同源
S00 自检曾把 `arm_jtc` 的期望类型写成 `arm_msgs/action/FollowJointTrajectory`,
而流程真正调用的是 `ros2_control` 的 `control_msgs/action/FollowJointTrajectory` ——
于是自检**永远**说"类型不对",而人只会去怀疑服务本身。
规矩:期望类型/话题名/服务名不要在两处各写一遍字面量,取同一个来源
(常量或构造函数);自检报"类型不对"时要说清**期望什么、实际什么**。
### R5.10 环境缺依赖就 `skip`,不要让"红"变成常态
仿真环境没装 `onnx`(只有真机 conda 装了),14 个用例长期报红。
长期红的结果是**没有人再看测试结果** —— 真出问题时也看不出来("狼来了")。
规矩:缺依赖用 `pytest.importorskip(...)` 并写清原因;只有"本环境本该能过"的
用例才允许失败。
---
## 6. 文档与提交
@@ -511,6 +558,39 @@ grep -l ros_gz_sim install/*/lib/*/* | wc -l # 应当变成 0
**预防**:构建脚本开头应当**断言**当前 conda 环境与目标一致(待补)。
### R6.9 硬退出(`os._exit` / `sys.exit`)前必须先把 traceback 打出来
节点入口一度直接 `os._exit(1)`,现场现象是"**退出码 1,日志里什么都没有**",
只能靠猜。规矩:
```python
try:
...
except Exception:
traceback.print_exc() # 先落日志
os._exit(1) # 再硬退
```
同理,`main()` 里凡是"exit 非 0 却没有任何输出"的路径,都算缺陷。
### R6.10 `ros2 launch` 的退出码**恒为 0**,判成败必须另有依据
实测(Humble):子进程 `exit 3`,`ros2 launch` 仍返回 **0**。所以任何
"`ros2 launch …` 之后看 `$?`"的脚本都会把**失败记成成功**,在自动化里极其危险。
规矩:
- 节点把结论**落盘**(`gauge_assembly` 写 `--result-file`,默认 `/tmp/gauge_assembly_result.json`),
脚本/CI 读文件判成败,不读 `$?`;
- "没有结论文件"要**当成失败**并说明(节点在写结论前就退出了),不能当成"没跑";
- 落盘要先于发话题(进程一退,话题里的最后一条就没了)。
另一个同类现象:**流程节点退出不会带走常驻节点**(如 `static_transform_publisher`),
launch 会一直挂着、终端不回提示符。要显式挂 `OnProcessExit → Shutdown`。
---
## 7. 改动前的检查清单
@@ -577,6 +657,14 @@ grep -l ros_gz_sim install/*/lib/*/* | wc -l # 应当变成 0
| 服务/话题/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 |
| 同一个手眼 TF,不同 launch 发出来不一样 | 四元数换算被抄了四份,退化分支公式各不相同 | R5.8 |
| 自检永远说"服务类型不对",而服务明明是好的 | 期望类型写成了适配器的类型,流程实际调的是 `control_msgs` 那份 | R5.9 |
| 节点退出码 1,日志里一个字都没有 | 入口 `os._exit()` 把异常吞了 | R6.9 |
| 每次 pytest 十几条红,没人看测试结果了 | 环境缺 `onnx`,用例直接失败而不是 `skip` | R5.10 |
| `site.yaml` 里改了参数,行为一点没变 | 那个键**没接线**(配置表里有、代码里从没读过) | R1.2 |
| 改了源码,仿真里行为一点没变,也没有任何提示 | 仿真 `install_sim/` 是**拷贝**(不能 `--symlink-install`),没重编就跑 | R5.7 |
| 装配失败了,脚本/CI 却报成功 | `ros2 launch` 的退出码恒为 0,脚本看了 `$?` | R6.9 |
| 流程明明打完了,终端一直不回到提示符 | launch 里的静态 TF 发布者是常驻的,主节点退出不会带走它 | R6.9 |
---