Files
ros_contorl/docs/HANDBOOK.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

455 lines
24 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.
# 新手手册 —— 从「这是干什么的」到「我能自己改」
> **这份文档记录什么事实**
> 它不是 API 参考,也不是环境安装说明书。它回答三个问题:
> 1. 这台机器到底在干什么活(业务),代码里哪一段对应哪一步;
> 2. 我要改一个东西时,**改哪个文件、用哪条命令验证**;
> 3. 出问题时,**先看哪份文档**(不重复写,只给索引)。
>
> **怎么读**:第 1 节读完你才知道后面在讲什么;第 3 节的决策表是日常最高频的一页;
> 第 5 节是排障入口。细节一律外链到权威文档,避免两份说法不一致(见
> [`FRAMEWORK_RULES.md`](FRAMEWORK_RULES.md) 的 M2)。
>
> **真值来源**:本文所有结论都能用命令复现;凡本文说得含糊的,以
> `config/site.yaml`、各 `devices/*/manifest.yaml` 和源码为准(R6.1)。
---
## 1. 这台机器在干什么
### 1.1 一句话
把一支**螺纹通止规**自动送进工件上的螺纹孔,旋入到指定深度,用"能不能旋进去、
旋入过程中的力怎么变"来判断这个孔的螺纹加工是否合格。
"通止规"是机械行业检验螺纹的标准量具:**通端**应当能顺利旋到底,
**止端**应该旋不进去(或只能进很少几圈)。人来测就是拿规往里旋、靠手感判断;
这个项目要做的是**让机械臂拿着规自己旋,并把判断依据(力)记下来**。
### 1.2 为什么要自动化
人手旋规的判断依赖老师傅手感,无法留证据、无法 24 小时连续测。
换成机械臂 + 视触觉传感器之后,每一次旋入都能留下力曲线,
"合格 / 不合格"就有了可追溯的数据依据。
### 1.3 一次完整任务分几步
代码里用 **stage1 / stage2 / stage3** 三段串起来,一条命令跑完:
```
run_all.sh
│
├─ stage1_track 视觉跟踪:D405 看住工件,法兰维持悬停高度跟着它走,
│ 直到"锁定"(连续 N 帧稳定)→ 停跟踪、钉住当前关节
│
├─ stage2_locate 拍照对孔:垂直朝下、降到拍摄高度、拍一张照,
│ YOLO 找工件外框 → Hough 找左右两个孔心 →
│ 深度 + 内参 + 手眼 → 换算出孔的基座坐标 →
│ 移动夹爪轴线到目标孔正上方 → 复拍验证 → 锁定
│
└─ stage3_search 柔顺搜孔:力闭环下的螺旋搜索,直到规的尖端"掉进"孔里,
再十字微扫定心,最后在力限内下压(可选按螺距旋入)
```
三段之间**在同一个进程里交接**,所以相机、触觉、手眼 TF 只初始化一次。
盯日志就能知道跑到哪了 —— 三行关键输出:
```
[pipeline] === stage1_track === → 在等跟踪锁定
[pipeline] === stage2_locate === → 在拍照对孔
[pipeline] === stage3_search === → 触面 → ★ 入孔 → 轻压到位
```
日常入口(三个终端,**必须同一个 `--arm`**):
```bash
bash ~/ros2/scripts/run_real.sh --arm 89 # T1 控制栈,一直开着
bash ~/ros2/scripts/run_camera_d405.sh --arm 89 --preview # T2 相机预览,一直开着
bash ~/ros2/scripts/run_all.sh --arm 89 # T3 三段串联(默认不拧入)
```
细节、参数、退出码见 [`HOLE_DETECT.md`](HOLE_DETECT.md)。
### 1.4 现在到哪一步了(如实标注,别信印象)
| 能力 | 状态 | 怎么自己确认 |
|------|------|--------------|
| 三段串联(跟踪 → 对孔 → 搜孔轻压) | ✅ 真机跑通 | `run_all.sh --arm 89` |
| 按螺距旋入(螺纹规) | ✅ 有参数,`--depth`/`--pitch` 控制 | `run_stage3_search.sh --arm 89 --depth 6 --pitch 1.0` |
| Orisys 双指触觉(法向 + 两路切向) | ⚠️ 能读,但**轴向映射尚未实测确认** | 见 [`TACTILE.md`](TACTILE.md) |
| Orisys 的深度图 / 力矢量图 | ⚠️ 消息结构与话题已在,真机值待确认 | 同上 |
| D435 手外相机(S03 粗定位兜底) | ❌ **还没做手眼标定**,链路不可用 | 见 `devices/cameras/README.md` |
| 自动启动全部服务(一条命令拉起所有进程) | ❌ 还没做,目前靠三个终端手工起 | 见第 6 节待办 |
| 「规有没有被夹住」自检 | ❌ 还没做 | 见第 6 节待办 |
| 装配合格判定(通/止) | ❌ 还没做。当前只输出几何与力的原始结果 | `/tmp/stage3_result.json` |
| Gazebo 仿真 | ✅ 场景、相机、触觉、夹爪可跑 | [`devices/sim/gazebo/README.md`](../devices/sim/gazebo/README.md) |
> 这张表是**承诺书**:❌ 的别当成已具备。文档说"已就绪"而实际没有,
> 是本仓库最忌讳的那类问题(R6.1)。
---
## 2. 系统长什么样
### 2.1 三层,各有硬边界
```
┌─ 现场数值层 ─────────────────────────────────────────────┐
│ config/site.yaml 唯一权威:SN / IP / 串口 / 阈值 / 锁高 │
│ devices/registry.yaml 只用哪个品牌的驱动(不含任何数值) │
└──────────────────────────────────────────────────────────┘
↓ 启动时注入
┌─ 设备模块层 ─────────────────────────────────────────────┐
│ devices/<类别>/<品牌>/ manifest.yaml + driver/ + 厂商物料 │
│ 换品牌 = 加一个目录 + 改 registry 一行,**不改应用代码** │
└──────────────────────────────────────────────────────────┘
↓ 框架接口(src/arm_interfaces 定义的契约)
┌─ 应用层 ─────────────────────────────────────────────────┐
│ src/arm_application/ 感知 / 伺服 / 手眼 / 夹爪 / 入孔 / 触觉 │
│ src/arm_control/ 命名位姿、状态、轨迹适配 │
│ src/arm_bringup/ launch 总装(real | mock | sim) │
└──────────────────────────────────────────────────────────┘
```
**唯一允许的配置入口是 `config/site.yaml`。** 包内 `config/*.yaml` 只是缺省值,
改了会被 site 覆盖 —— 这是新手最常踩的一个坑(见 3.2)。
两块硬资源是**独占**的,同时开会互相咬:
| 资源 | 谁占着 | 冲突表现 |
|------|--------|----------|
| RDK(机器人控制接口) | `run_real.sh`、`run_handeye.sh teach` | 后启动的连不上 / 报错 |
| D405 USB | 相机节点、示教、标定 | 后启动的拿不到图 |
所以 `run_handeye.sh` 会**先停掉旧相机**;`go_home.sh` 会**先杀掉 perception/servo**
再回零,否则两条轨迹抢同一个臂。
### 2.2 从像素到关节:一条链路看懂全栈
```
D405 YOLO 手眼 T 伺服 ros2_control
出图 → 找工件框 → 像素+深度 → 相机系 3D → 法兰系 → 基座系 → 关节角
↓
/visual_servo/filtered_point
100 Hz
```
链路里每一步都有一个"口径",写错任何一处都不会报错,只会让点**偏掉**:
| 口径 | 写错的表现 | 规矩 |
|------|-----------|------|
| 深度是米还是毫米 | 偏 1000 倍 | R2.3 |
| `frame_id` 是光学系还是相机本体系 | 点转到别处,但 `tf2` 查得到 | R2.2 |
| `rpy` 复合顺序 | 相机背对目标,只看得见地面 | R2.4 |
| 内参从哪来 | 偏半米量级,但单目预览完全正常 | R2.5 |
| 手眼 `T_4x4` 是哪台臂的 | 始终差几厘米,很难怀疑到标定值上 | 第 1 节末尾 |
这些坑**全部**在 [`FRAMEWORK_RULES.md`](FRAMEWORK_RULES.md) 第 8 节有症状表。
### 2.3 三条设计取舍(理解了才不会误改)
1. **仿真与真机共用同一套参数。** 所以你在仿真里调的悬停高、阈值,搬到真机直接生效;
代价是仿真里不能有"仿真专用参数",只有物理引擎自己的参数(重力、步长)放 `sim.*`。
2. **设备模块里不许出现现场数值。** 数值只有 `site.yaml` 一个来源。
这样换一台臂就是改 `site.yaml`,而不是满仓库找硬编码。
3. **校验必须能真的失败。** 故意弄坏它、检查会红,才算校验;
不然就是"把没检查伪装成检查通过"(M1)。
---
## 3. 我要改东西,改哪里
### 3.1 ★ 决策表(先查这张,再动手)
| 我想改… | 改这里 | 验证命令 | 细节 |
|---------|--------|----------|------|
| 换机械臂 / 换臂号 | `site.yaml` → `robot.sn`,或所有终端加同一 `--arm` | `scripts/show_site_config.sh` | [`SITE_CONFIG.md`](SITE_CONFIG.md) |
| 悬停高 / 锁高 / 伺服参数 | `site.yaml` → `robots[].servo.*` | 重启第三终端即可 | [`TRACKING.md`](TRACKING.md) |
| 夹爪速度 / 力 / 行程 | `site.yaml` → `gripper:` | `run_gripper.sh --arm 89 --cli open` | [`GRIPPER.md`](GRIPPER.md) |
| 入孔检测算法参数 | `site.yaml` → `hole_detect:` | `run_hole_detect.sh --arm 89 --preview` | [`HOLE_DETECT.md`](HOLE_DETECT.md) |
| 触觉灵敏度 | `site.yaml` → `hole_detect.stage3:` 的 `*_delta` | 看启动日志打印的派生阈值 | [`TACTILE.md`](TACTILE.md) |
| 相机曝光 / 图幅 / 视场 | `site.yaml` → `cameras:`(真机) | `list_realsense.sh` | [`SITE_CONFIG.md`](SITE_CONFIG.md) |
| 仿真场景里放什么、放哪 | `devices/sim/gazebo/config/scene.yaml` | `python3 scripts/check_sim_scene.py` | [仿真手册 §4.1](../devices/sim/gazebo/README.md) |
| 仿真相机视场/图幅 | 仿真手册 §4.3,**三处要同时改** | `python3 scripts/check_sim_cameras.py` | 同上 §4.3 |
| 算法权重(换模型) | `models/<分组>/` + `site.yaml` 的路径 | `scripts/weights.sh list` | [`models/README.md`](../models/README.md) |
| 接一个新品牌设备 | `devices/<类别>/<品牌>/` + `registry.yaml` 一行 | `ros2 run arm_interfaces device_check` | [`devices/README.md`](../devices/README.md) |
| 命名位姿(回零点等) | `src/arm_control/config/named_poses_89.yaml` / `_91.yaml` | `go_home.sh --arm 89` | [`INTERFACES.md`](INTERFACES.md) |
| 框架规矩本身 | `docs/FRAMEWORK_RULES.md`,**同时**改 `.cursor/rules/` | `python3 scripts/check_text_only.py` | R6.1 |
### 3.2 改一个现场数值(最高频)
**唯一入口是 `config/site.yaml`。** 不要改 `src/arm_application/config/*.yaml` ——
那只是缺省值,会被 site 覆盖,改了不生效**而且不报错**(典型静默失效)。
```bash
cp config/site.example.yaml config/site.yaml # 只做一次(site.yaml 不进 git)
vim config/site.yaml
bash scripts/show_site_config.sh # 看解析结果对不对
# 重启对应终端
```
生效范围不一样,别白改:
| 改了什么 | 要重启什么 |
|----------|-----------|
| 锁高 / YOLO / 伺服参数 | 跑伺服的那一个终端 |
| 相机分辨率 / 曝光 | 相机那个终端 |
| 夹爪参数 | 夹爪进程 |
| 设备选型(`registry.yaml`) | 对应的启动脚本 |
### 3.3 加一个场景物体(仿真)
三步,**只改一处配置文件**:
1. 在 `devices/sim/gazebo/models/<物体名>/` 下建 `model.sdf` + `model.config`
(建模要求见[仿真手册 §2](../devices/sim/gazebo/README.md):原点必须落在**底面中心**,
碰撞体用简单几何,质量与惯性别省)。
2. 在 `devices/sim/gazebo/config/scene.yaml` 的 `objects:` 里加一条(位姿、是否 static)。
3. 校验:
```bash
source scripts/sim_env.sh
python3 scripts/check_sim_scene.py # 悬空 / 穿模 / 出界 / 互相嵌入 / 像素几何
```
### 3.4 加一个算法权重
放进去 → 打标签 → 改路径 → 体检。TensorRT 转换也在这条链上:
```bash
bash scripts/weights.sh convert <文件> --name <模型名> # PT→ONNX→TensorRT,自动改名归位
bash scripts/weights.sh labels <模型名> --set "工件,通止规"
bash scripts/weights.sh doctor --check-engines # 查版本不匹配这类静默失效
```
布局是 `models/<分组>/<模型名>.{pt,onnx,engine}` + 旁挂 `<模型名>.labels.yaml`。
**二进制不进 git**,标签文件进。规则见 [`models/README.md`](../models/README.md)。
### 3.5 接一个新品牌设备
四步(完整说明在 [`devices/README.md`](../devices/README.md),这里只给顺序):
1. 把厂商物料丢进 `devices/<类别>/<品牌>/`(格式与命名按该类别 `README.md`,**必须照做**)。
2. 复制同类别的 `manifest.yaml`,改 `id` / `label` / `driver.package` / `config_schema`。
3. 在 `driver/` 下实现框架接口契约(见 `src/arm_interfaces/`)。
**这一步躲不掉** —— 厂商给的是 C++/Python 库而不是 ROS 2 驱动时,
外面永远需要一层薄适配器;框架能做的是把它写到最薄(只翻译 API,不含业务逻辑)。
4. 改 `devices/registry.yaml` 一行。
验:
```bash
ros2 run arm_interfaces device_check # 契约校验
ros2 run arm_interfaces device_check --list-brands # 看每类现在有哪些可选
```
> 校验会读 `registry.yaml` → 各 `manifest.yaml` → `site.yaml`,检查三件事:
> 选中的品牌目录在不在、`assets` 里声明为必需的物料文件在不在、
> `required: true` 的配置键在 `site.yaml` 里有没有值。
> 任何一项不满足**立即报错并指出该去哪个文件补什么**,不静默降级。
### 3.6 改相机分辨率(最容易漏改)
真机改 `site.yaml` 就行。**仿真要改三处**,因为像素门槛是绑在图幅上的 ——
改完不算清楚,孔检测在仿真里会"根本不可能通过",而看起来像算法不行。
详见[仿真手册 §4.3](../devices/sim/gazebo/README.md)。
### 3.6.1 换一台机械臂(最容易漏改,而且很难查)
`site.yaml` 里写的是**哪台臂**,但有一批一级配置文件里写死了**臂的关节名**
(Flexiv 官方把 SN 编进关节名:`Rizon4-063989_joint1`)。这些文件必须跟着改:
```bash
python3 scripts/check_arm_prefixes.py # 逐条列出哪个文件哪一行对不上,并给修法
python3 scripts/check_arm_prefixes.py --list # 只看当前臂的应然前缀
```
漏改的症状有两种,**都不像"配置错了"**:
- 控制器报 `Joint 'Rizon4-063989_joint1' not found`(这条还算好查);
- URDF 换了名字、`srdf/arm.srdf` 没换 → MoveIt **规划成功但机械臂不动**。
换臂四步(少一步就出上面那种症状):
```bash
vim config/site.yaml # 1. robot.sn + robot.robots[]
source scripts/env.sh --arm 新臂 # 2. 选定当前臂
python3 scripts/check_arm_prefixes.py # 3. 必须输出 OK
bash scripts/build.sh # 4. 让 install/ 同步
```
哪些文件该跟着改、哪些是"按臂各留一份"的现场资产,
见 [`FRAMEWORK_RULES.md`](FRAMEWORK_RULES.md) 的 R1.1.1 表。
> 排查提示:`ROBOT_SN` / `ROBOT_ARM` 环境变量会**覆盖** `site.yaml` 的 `robot.sn`。
> "明明改了配置却没生效"时先 `echo $ROBOT_SN`,或看 `--list` 打印的"来源"。
### 3.7 改完必须做的一件事
**问自己:我故意弄坏它,哪个检查会红?**
如果答不上来,说明这个改动没有任何东西在守着 —— 出问题只会以"结果是错的"形式出现。
这条写在 [`FRAMEWORK_RULES.md`](FRAMEWORK_RULES.md) 第 7 节的改动前检查清单里,
逐条过一遍只要两分钟。
另外:**只改注释/文档**时,跑 `python3 scripts/check_text_only.py` 自证没碰逻辑(R6.5)。
---
## 4. 怎么验
### 4.1 三层,只有后两层算验收
| 层 | 判据 | 够不够 |
|----|------|--------|
| 1 | 不报错、有话题、TF 查得到 | ❌ **不算验收**(R4.9) |
| 2 | 物理量落在已知真值上(深度反投影命中已知平面、内参与公式吻合) | ✅ 模块级 |
| 3 | 任务真的成功(跑出结果、力曲线符合预期) | ✅ 端到端 |
第 1 层是陷阱:仿真里相机背对目标**照样出图**、`create` 打印成功但模型**没进 Gazebo**、
夹爪控制器 `active` 但手指**不动**。这些全都不报错。
### 4.2 命令清单
```bash
# —— 环境 ——
source scripts/env.sh # 真机环境
source scripts/sim_env.sh # 仿真环境(独立 conda)
# —— 设备与配置 ——
ros2 run arm_interfaces device_check # 设备契约 + site.yaml 必填项
bash scripts/show_site_config.sh # site.yaml 解析结果
python3 scripts/check_arm_prefixes.py # 含臂名的配置是否跟着当前臂(换臂必跑)
# —— 仿真验收(★ 必做,别靠肉眼)——
python3 scripts/check_sim_cameras.py # 深度反投影命中已知平面才算过
python3 scripts/check_sim_scene.py # 摆放 / 像素门槛漂移 / 成像几何 / 可见性
# —— 只改文字时自证 ——
python3 scripts/check_text_only.py
# —— 权重体检 ——
bash scripts/weights.sh doctor --check-engines
```
### 4.3 单元测试怎么跑
**每个功能包都有自己的测试**,改完对应包就跑一下。当前 pytest 收集 **608 项**。
```bash
source scripts/env.sh # 或 sim_env.sh
# 逐包跑(跳过需要厂商 flexivrdk 包的 flexiv_rdk,那个环境里没有)
for d in src/*/test; do
case "$d" in src/flexiv_rdk/*) continue;; esac
printf '%-28s ' "$d"; python3 -m pytest "$d" -q -p no:cacheprovider 2>&1 | tail -1
done
# 只跑一个包
python3 -m pytest src/arm_sim_gazebo/test -q
```
改动前后对比数量是最省事的回归判据 —— 现在应该是(pytest 收集数,共 608 项):
| 包 | 测试数 | 覆盖什么 |
|----|--------|----------|
| `arm_application` | 233 | 纯逻辑:滤波器、解析器、几何、参数合并 |
| `arm_sim_gazebo` | 236 | 场景校验、URDF 归一化、相机内参、触觉适配 |
| `arm_driver_flexiv` | 69 | 驱动适配、下压控制、超时 |
| `arm_interfaces` | 46 | 路径解析、清单校验、配置守卫 |
| `arm_kinematics` | 24 | 正逆运动学 |
> `src/flexiv_rdk/test` 需要厂商的 `flexivrdk` Python 包,仿真环境里没有,
> 跑不过属正常,与我们的改动无关。
### 4.4 验收工具自己也会失效
这是本仓库最贵的教训,单独写一节:
- 校验代码可能**永远通过** —— 因为 `sdformat` 会用 `__default__` /
`__default_topic__` 这种**假值**把缺失字段填上,"检查是否为空"的代码就查不出来(M1)。
- 解析外部输出可能**格式对不上**,然后代码"优雅降级成跳过检查" ——
于是"没检查"被伪装成"通过"(R5.3、R5.4)。
- `bash -n` 说脚本语法 OK,脚本却**跑不起来** —— CRLF 行尾会漏报,
只有真跑 `--help` 才暴露(R6.6)。
**所以**:写完校验,先故意弄坏被校验的对象,确认检查真的会红;
负向测试和正向测试一样重要(R5.2)。
---
## 5. 出问题按现象查
### 5.1 三个权威处,按"你在问什么"分工
| 你在问 | 去哪 | 为什么 |
|--------|------|--------|
| 「这个现象可能是什么原因」 | [`FRAMEWORK_RULES.md`](FRAMEWORK_RULES.md) **第 8 节** | 按**症状**排序的反例表,一条一个真实原因 |
| 「这是 Gazebo 的问题」 | [仿真手册 §5](../devices/sim/gazebo/README.md) | 11 条仿真专属坑,每条写了"怎么发现" |
| 「这个脚本/话题/参数是干嘛的」 | [`WORKSPACE.md`](WORKSPACE.md) / [`INTERFACES.md`](INTERFACES.md) | 目录、脚本、话题、参数契约 |
| 「手眼/跟踪/夹爪/入孔/触觉怎么用」 | 各自那份 `docs/*.md` | 见下节对应表 |
**先查症状表,再动手改代码。** 这个仓库的坑绝大多数"看着像新问题,其实是老问题复发"。
### 5.2 常见现象 → 先想到什么
| 现象 | 先怀疑 | 详细 |
|------|--------|------|
| 跟踪一直差几厘米 | 用了**别的臂**的手眼文件,或没重新标定 | [`HANDEYE.md`](HANDEYE.md) |
| 图像有、但点算错 | 深度单位 / `frame_id` / 内参来源 | 第 2.2 节、R2.2–R2.5 |
| 模型"根本没进 Gazebo",但 `create` 打印成功 | world 里声明了插件,挤掉了默认的 UserCommands | 仿真手册坑 2 |
| 物体一 spawn 就消失 | 上游 URDF 缺 `<inertial>` / 有零质量 link | 仿真手册坑 1 |
| 夹爪控制器 active 但手指不动 | `<mimic>` 被 `sdformat` 丢了 | 仿真手册坑 10 |
| 触觉"离开"之后接触标志永远是 `True` | contact 是**事件式**消息,离开时不发 | 仿真手册坑 6 |
| 消息定义里有 `wrenches` 字段,运行时却永远是空的 | Fortress 的 contact-system 根本不填力 | 仿真手册坑 7 |
| 触觉"有力、会响应、松开归零"但语义全错 | 轴向映射是猜的,没实测 | R3.3 |
| 明明夹住了,`fnormal` 恒为 0 | 基线把真实接触吃掉了 | R3.1 |
| 校验代码永远通过 | `sdformat` 用 `__default__` / `__default_topic__` 假值填了缺失字段 | 仿真手册坑 9 |
| 脚本报 `$'\r': 未找到命令` | 脚本是 CRLF 行尾 | R6.6 |
| 改了参数不生效 | 改的是包内 yaml,被 `site.yaml` 覆盖了 | 3.2 |
| MoveIt 说规划成功、机械臂不动 | SRDF / controllers 里还是**旧臂**的关节名 | 3.6.1、R1.1.1 |
| 控制器报 `Joint 'Rizon4-xxxxxx_joint1' not found` | 换了臂但一级配置没跟着改(或改了两处漏一处) | 3.6.1 |
| 仿真镜头位置不对、只看得见地面 | 相机 `rpy` 复合顺序 | 仿真手册坑 5 |
| 脚本说"已停止",进程还在 | 进程模式表漏项 | R4.10 |
| 孔检测在仿真里怎么都不过 | 图幅与真机不同,像素门槛失效 | 仿真手册 §4.3 |
---
## 6. 术语表与待办
### 6.1 术语
| 词 | 意思 |
|----|------|
| **通止规** | 检验螺纹的量具。通端应能旋到底,止端应旋不进(或只进几圈) |
| **89 / 91** | 左臂 / 右臂的现场编号,**各有独立的手眼、相机、夹爪、锁高,禁止混用** |
| **法兰** | 机械臂末端安装面;手眼标定求的就是相机→法兰的 `T_4x4` |
| **eye-in-hand / eye-to-hand** | 相机装在手上 / 装在手上之外(D405 在手上,D435 在外部) |
| **stage1/2/3** | 跟踪 / 对孔 / 搜孔,见 1.3 |
| **S03 / S04 / S07 / S08** | 整体装配任务书里的阶段编号(S03 粗定位、S04 跟踪、S07 柔顺下压、S08 夹爪旋转循环)。代码里对应的是 stage1/2/3 的拆分实现,**没有一一对应**,别硬套 |
| **基线** | 触觉静止时的读数均值。力的语义是"相对基线的增量",不是绝对牛顿值 |
| **R1.1 / M1 …** | [`FRAMEWORK_RULES.md`](FRAMEWORK_RULES.md) 里的规则编号,文档和提交信息里直接引用 |
| **静默失效** | 不报错但结果是错的。本仓库的头号敌人,见 M1 |
### 6.2 还没做的事(别按已具备来用)
按优先级:
1. **自动启动全部服务** —— 目前靠三个终端手工起,容易漏、
且顺序错了会出"控制器没起来但脚本继续跑"这类问题。
2. **夹爪持规自检** —— 启动后先用触觉判断"规真的被夹着",没有就报错停机
(现在没有这一步,规掉了会一路跑到撞工件才发现)。
3. **D435 手眼标定** —— 标完 S03 粗定位链路才可用(工件不在 D405 视野里时靠它先粗居中)。
4. **装配合格判定** —— 把力曲线转成"通/止"结论。当前只输出原始几何与力,
判定逻辑(含螺纹旋入圈数)还没写。
### 6.3 怎么往下读
```
你刚来 → 第 1、2 节,然后照着 1.3 的三个终端跑一次
要动手改 → 第 3 节决策表
要交付 → 第 4 节验收 + FRAMEWORK_RULES 第 7 节检查清单
被 bug 咬了 → 第 5 节
要给 AI 下指令 → FRAMEWORK_RULES 第 9 节
```