From d78e66ce98c695cbb3b5e49121b74bca12ad2017 Mon Sep 17 00:00:00 2001 From: arm_dev Date: Wed, 23 Sep 2026 17:16:47 +0800 Subject: [PATCH] =?UTF-8?q?P5a=20=E6=A1=86=E6=9E=B6=E8=A7=84=E5=88=99?= =?UTF-8?q?=E6=96=87=E6=A1=A3=EF=BC=9AM1-M3=20=E5=85=83=E8=A7=84=E5=88=99?= =?UTF-8?q?=20+=20R1-R6=20=E5=88=86=E9=A2=86=E5=9F=9F=E8=A7=84=E7=9F=A9=20?= =?UTF-8?q?+=20=E5=8F=8D=E4=BE=8B=E6=B8=85=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 背景:这个框架踩过的坑几乎都是同一类——"没报错但结果是错的"。 之前这些教训只存在于聊天记录和零散注释里,改一次代码就要重踩一遍。 现在收敛成一份可引用的文档,并压缩成 .cursor/rules/ 常驻规则。 docs/FRAMEWORK_RULES.md(414 行) 第 0 节 三条元规则:M1 静默失效是头号敌人 / M2 单一数据源 / M3 口径必须显式 第 1 节 配置与数据 R1.1-R1.5(分层优先级、site.yaml 新键必须接线或告警、设备走 devices/、权重走 models/) 第 2 节 几何/单位/坐标系 R2.1-R2.7(米与弧度、frame_id 必须是光学系、深度 mm↔m、 rpy=Rz·Ry·Rx、Gazebo camera_info 不可信、像素门槛绑图幅、位姿容差要写理由) 第 3 节 触觉与力 R3.1-R3.6(力是相对基线的增量、fnormal 压为正、轴向映射必须实测、 接触布尔要用超时、force_scale 口径、仿真图像只能占位) 第 4 节 仿真 R4.1-R4.10(唯一 site.yaml、world 里写 plugin 会挤掉默认系统、搜索路径要在 launch 前设好、上游 description 不许改、场景走 scene.yaml、校验要逐碰撞体、 不用"有话题就算通"验收、停仿真用 stop_sim_gz.sh) 第 5 节 代码与测试 R5.1-R5.7(纯逻辑抽纯函数、校验要有测试证明有效、不许把降级写成通过、 解析外部输出先确认真实格式、删除前扫全仓库引用、别在中文路径 colcon build、真机仿真产物隔离) 第 6 节 文档与提交 R6.1-R6.4(文档与实现不一致立刻修文档、每份文档说明记录什么事实、 结论数字要可复现、提交信息讲为什么并附实测数据) 第 7 节 改动前检查清单 第 8 节 历史反例清单——按"你看到的症状"查表,不是按道理排序 第 9 节 给 AI 的具体指令 .cursor/rules/*.mdc 三份压缩版(常驻) framework-core.mdc alwaysApply,元规则 + 检查清单 + 禁止项 sim-gazebo.mdc globs=devices/sim/**,src/arm_sim_gazebo/** config-and-devices.mdc globs=config/**,devices/**,models/**,src/arm_interfaces/** 顺手修掉文档与实现不一致(R6.1) docs/WORKSPACE.md 此前【完全没有 devices/ 这一层】——通用化落点却没进目录总表。 补 devices/ 一节(kind/brand 目录约定、registry.yaml 与 manifest.yaml 分工、 assets[].required 缺失会报错),补 build_sim/install_sim/log_sim 与 .cursor/rules/, 并把 FRAMEWORK_RULES.md 标为 docs/ 首行 README.md 文档表首行加 FRAMEWORK_RULES.md 顺手删死代码(R5.5:先扫引用) src/arm_interfaces/arm_interfaces/paths.py 删 assets_root() 它返回 ros2_root()/"assets",而仓库里从来没有 assets/ 这个目录——实际权重树是 models/, 且 models/ 的定位已由 arm_application.weights_layout.default_weights_root() 负责 (带 SITE_WEIGHTS_ROOT 覆盖,且能从 install/ 反查仓库根)。 两个实现并存违反 M2,且这个还指向不存在的路径,属于"照着名字用就踩空"。 src/arm_interfaces/test/test_real_devices.py 从清环境变量列表里去掉 SITE_ASSETS_ROOT 验收 src/arm_application 233 passed / arm_driver_flexiv 69 / arm_interfaces 46 / arm_kinematics 24 / arm_sim_gazebo 236 = 608 passed (flexiv_rdk 的 test 需装厂商 flexivrdk 包,仿真环境无此依赖,与本改动无关) Co-authored-by: Cursor --- .cursor/rules/config-and-devices.mdc | 42 ++ .cursor/rules/framework-core.mdc | 42 ++ .cursor/rules/sim-gazebo.mdc | 31 ++ README.md | 1 + docs/FRAMEWORK_RULES.md | 414 +++++++++++++++++++ docs/WORKSPACE.md | 36 +- src/arm_interfaces/arm_interfaces/paths.py | 8 - src/arm_interfaces/test/test_real_devices.py | 2 +- 8 files changed, 565 insertions(+), 11 deletions(-) create mode 100644 .cursor/rules/config-and-devices.mdc create mode 100644 .cursor/rules/framework-core.mdc create mode 100644 .cursor/rules/sim-gazebo.mdc create mode 100644 docs/FRAMEWORK_RULES.md diff --git a/.cursor/rules/config-and-devices.mdc b/.cursor/rules/config-and-devices.mdc new file mode 100644 index 0000000..730dfe1 --- /dev/null +++ b/.cursor/rules/config-and-devices.mdc @@ -0,0 +1,42 @@ +--- +description: 配置与设备模块的改动规则(site.yaml 分层、devices 布局、权重归位) +globs: config/**,devices/**,models/**,src/arm_interfaces/** +--- + +# 配置与设备改动规则 + +权威全文:`docs/FRAMEWORK_RULES.md` 第 1 节(R1.1–R1.5)。 + +## 配置分层 + +- 优先级固定:**命令行 > `config/site.yaml` > 包内默认值**。 +- `config/site.yaml` 是站点数据(SN / IP / 序列号 / 锁高 / 权重路径),**不进 git**; + 改过它之后必须重跑 `python3 scripts/gen_site_example.py`,否则模板漂移 + (有一条 parity 测试会红)。 +- **禁止硬编码站点值到代码里**。历史反例:15 处硬编码的 `Rizon4-063989`, + 换一台臂就会静默连错机器。 +- 新增 `site.yaml` 键时二选一:**真的接线**,或在启动时**告警**。 + 声明了却没接线、也不告警,是最难查的一类问题(用户改了配置毫无反应)。 + 注意 `wired:` 只是 manifest 里的注释性约定,`arm_interfaces.manifest` 不解析它 —— + 保障要靠代码里的告警函数(如 `sim_site.unwired_warnings()`),别指望清单。 + +## 设备模块布局 + +``` +devices/// +├── 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` 的物料缺失时,报错信息要给出**格式与命名要求**, + 不能只说"文件不存在"。 + +## 权重 + +- 二进制(`.pt/.onnx/.engine`)**不进 git**,`.labels.yaml`(文本标签)**要进**。 +- 转换链:`models/staging/` → `weights.sh convert` → 自动改名归位 + 固化标签。 +- 配置里指向不存在的模型(悬空引用)必须修掉:`bash scripts/weights.sh doctor`。 diff --git a/.cursor/rules/framework-core.mdc b/.cursor/rules/framework-core.mdc new file mode 100644 index 0000000..3443c4c --- /dev/null +++ b/.cursor/rules/framework-core.mdc @@ -0,0 +1,42 @@ +--- +description: 本仓库的框架硬规矩(三条元规则 + 改动前检查清单),改任何代码前先读 +alwaysApply: true +--- + +# 框架硬规矩(权威全文:`docs/FRAMEWORK_RULES.md`,规则编号 R1.1…R8) + +这不是风格建议。违反它们的改动会在真机上以**不报错但结果是错的**方式失败, +而防这件事是这个仓库花最多代价在做的事。要绕开某条规则,先改文档并写清理由。 + +## 三条元规则 + +1. **静默失效是头号敌人。** 任何检查/校验都必须**能真的失败** —— + 写完先问"我故意弄坏它,这个检查会红吗?"。不会红 = 等于没检查。 + 宁可当场报错,也不要"宽容降级":降级会把"没检查"伪装成"检查通过"。 + 校验代码自己也会静默失效(外部工具会用 `__default__` 这类**假值**填缺失字段)。 +2. **单一数据源。** 同一事实只写一处,其余派生;能算的不要抄。 + 必须抄的(如仿真侧的像素门槛)要有人守着 —— 脚本交叉核对真机配置,不一致判失败。 +3. **口径必须显式。** 单位/坐标系/符号/参考基准写下来,换算只做一次、放在适配层。 + +## 改动前必答 + +- 这个事实仓库里有几处在写?其余的是派生的吗? +- 我加的配置键真的接线了吗?没接线会在启动时告警吗? +- 涉及的检查有**负向测试**吗(故意弄坏能红)? +- 解析外部输出前,我看过它的**真实**输出吗?(文本格式与直觉不一致是常态) +- 我改的是上游第三方文件吗?→ 不许直接改,走归一化/适配层。 +- 文档里的"已就绪 / 未接线"标注与实现还一致吗?不一致立刻改文档(以实现为准)。 + +## 严禁 + +- 硬编码站点数据(SN / IP / 相机序列号 / 锁高 / 权重路径)—— 一律走 `config/site.yaml`。 +- 用"有图像 / 有话题 / TF 查得到"作为验收 —— 必须断言物理量。 +- 把"降级跳过"写成"通过"。 +- 在中文路径下 `colcon build`。 +- 提交信息只写"改了什么"而不写"为什么"(中文,附实测验收数据)。 + +## 出问题先查症状表 + +`docs/FRAMEWORK_RULES.md` 第 8 节按**症状**列出全部历史反例 +("三维点偏半米"、"深度差 1000 倍"、"夹爪 active 但不动"、"脚本说已停止进程还在"…), +先按症状查表,再动手。 diff --git a/.cursor/rules/sim-gazebo.mdc b/.cursor/rules/sim-gazebo.mdc new file mode 100644 index 0000000..677b917 --- /dev/null +++ b/.cursor/rules/sim-gazebo.mdc @@ -0,0 +1,31 @@ +--- +description: Gazebo 仿真的改动规则(静默失效高发区:路径/内参/接触力/描述归一化) +globs: devices/sim/**,src/arm_sim_gazebo/** +--- + +# 仿真改动规则 + +权威全文:`docs/FRAMEWORK_RULES.md` 第 4 节;平台手册:`devices/sim/gazebo/README.md`。 +R4.1–R4.10 全部适用,以下是**最容易踩的几条**: + +- **world 里声明任何一个 `` 都会挤掉默认系统**(Physics / UserCommands / + SceneBroadcaster)。少了 UserCommands,模型根本没进 Gazebo,而 `create` **仍然打印** + `OK creation of entity`。要加插件时必须把这几个系统一起显式声明。 +- **插件/资源搜索路径必须在 `ros2 launch` 之前设好**(conda 激活不会加 + `$CONDA_PREFIX/lib`)。不要绕过 `scripts/sim_env.sh` 直接 launch。 +- **不许改上游描述文件**(`flexiv_description` 等):`` 会被 `sdformat` 丢弃、 + 零质量 link 会出 NaN 惯性、无质量根 link 会连子树被丢。一律走启动时归一化。 +- **gz 发的 `camera_info` 是错的**(恒为 60° 视场的默认值),内参必须按 + `cameras.yaml` 的 `horizontal_fov` 与图幅重算并覆盖。 +- **深度单位**:gz 是 `32FC1` 米,真机是 `16UC1` 毫米。换算只做一次(适配器)。 +- **相机 `frame_id` 必须是光学系**;`rpy` 的复合顺序是 `Rz·Ry·Rx`。 +- **触觉**:轴向映射必须实测(不许从 URDF 推导);接触布尔必须用超时判据 + (接触消息是事件式的,没有"离开"事件);`force_scale` 决定仿真与真机能否共用阈值。 +- **验收不用"有图像 / TF 查得到"**:跑 `scripts/check_sim_cameras.py` 与 + `scripts/check_sim_scene.py`(判据是物理量:反投影命中平面、像素门槛、位姿一致)。 +- **停仿真用 `scripts/stop_sim_gz.sh`**,不要 `pkill -f`(会先杀掉调用它的 shell)。 + 给栈里加了新进程时,记得加进该脚本的模式表 —— 它末尾会复查残留,漏项会报出来。 +- **模型约定**:原点在**底面中心**、单位米、目录名 = `` = + `model.config` 的 `` = 推理类别名;碰撞体用简单几何,`visual` 可以加 + 只服务于检测的细节(如工件上的亮环),但 `visual` **名字有意义**,改名字要同步改 + `sim_scene.features_from_workpiece`。 diff --git a/README.md b/README.md index 207cd3d..3ceb093 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ bash ~/ros2/scripts/run_visual_servo_rt.sh --arm 89 --control | 文档 | 内容 | |------|------| +| [docs/FRAMEWORK_RULES.md](docs/FRAMEWORK_RULES.md) | **框架规则**(改代码前先读;含历史反例的症状表) | | [docs/WORKSPACE.md](docs/WORKSPACE.md) | 每个目录 / 脚本 / 包 | | [docs/QUICKSTART.md](docs/QUICKSTART.md) | 环境与第一次跑通 | | [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | 进程与几何 | diff --git a/docs/FRAMEWORK_RULES.md b/docs/FRAMEWORK_RULES.md new file mode 100644 index 0000000..05320cc --- /dev/null +++ b/docs/FRAMEWORK_RULES.md @@ -0,0 +1,414 @@ +# 框架规则(`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.2 新增 `site.yaml` 键必须二选一:**接线** 或 **明确告警** + +声明了却没接线的键是最难查的一类问题:用户按文档改了 `site.yaml`,行为毫无变化, +也不报错。所以每个新键都要么真的接线,要么在启动时**告警**(`wired: false` + +`unwired_warnings()` 那套做法)。 + +> ⚠️ 现状:`wired:` 目前只是 `manifest.yaml` 里的**注释性约定**, +> `arm_interfaces.manifest` 并不解析它。所以"未接线"的保障实际来自 +> `sim_site.unwired_warnings()` 这类**代码里的告警**,不是清单。 +> 加键时不要指望清单替你守法。 + +### R1.3 设备一律走 `devices///`,并带 `manifest.yaml` + +``` +devices/// +├── 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` 里声明任何一个 `` 都会挤掉默认系统 + +声明了任何插件后,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` 等)**不许直接改** + +第三方包一律在**启动时归一化**,不在上游文件里下手: + +- 缺 `` / 零质量 link → NaN 惯性、物体消失 +- `` 标签被 `sdformat` **丢弃** → 夹爪"控制器 active 但手指不动" +- 无质量的根 link(world link)会**连整棵子树被静默丢掉** + +改上游还有个额外代价:下次 `fetch_flexiv.sh` 会把你的改动覆盖掉。 + +### R4.5 场景物体走 `scene.yaml`,不进 world + +`world` 是"环境"(地面/台面/相机/系统插件),`scene.yaml` 是"这一批活做什么"。 +动 world 的风险面太大(`R4.2`)。 + +模型约定:**原点在底面中心**、单位米、目录名 = `` = `model.config` +里的 `` = 推理类别名。 + +### R4.6 模型/场景校验必须逐**碰撞体**、三维判 + +- 不能拿总包围盒判重叠:正常装配里本来就有嵌套(规立在规座方孔里), + 粗判会把正常嵌套报成碰撞,逼着人把检查关掉。 +- 恰好贴合不算重叠(规的台肩正好坐在规座顶面上)。 +- 变换必须用"中心 + 半宽":对 min/max 取绝对值再对称化会把偏移量吃掉、 + 盒子凭空变大。 +- 校验要用**从 SDF 解析出来的**几何,不能在 `scene.yaml` 里再抄一份尺寸。 + +### R4.7 建模:碰撞体用简单几何,视觉可以加"只给算法看"的细节 + +`` 决定物理,用 box/cylinder;`` 决定相机看见什么, +可以加亮环、色块这类只服务于检测的东西。 + +工件的碰撞体是方孔、视觉是圆环,**这是刻意的**:真机的 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 提交信息:中文,讲**为什么**,附实测验收数据 + +格式:`<阶段号> <一句话结果>` + 正文说明"问题是什么、为什么这么改、怎么验证的"。 +修静默失效时,把"症状"写进去 —— 那是后来人搜索时唯一能匹配上的东西。 + +--- + +## 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 | + +--- + +## 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 就消失 | 缺 `` / 零质量 link → 惯性 NaN | R4.4 | +| 夹爪控制器 active 但手指不动 | `` 被 `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 | +| `assets_root()` 指向从未存在的 `assets/` | 零调用者的死代码,没人发现 | R5.5 | + +--- + +## 9. 这份文档怎么用(给 AI 的具体指令) + +改这个仓库时: + +1. 先读第 0 节,记住三条元规则。 +2. 按第 7 节的清单逐条过。 +3. 出问题时先查第 8 节(按症状查)。 +4. 要在代码里"绕开"某条规则时,**先改这份文档**并写清理由 —— + 绕开而不改文档,下一个人会按文档改回去。 diff --git a/docs/WORKSPACE.md b/docs/WORKSPACE.md index a6a5403..94aa4bd 100644 --- a/docs/WORKSPACE.md +++ b/docs/WORKSPACE.md @@ -15,20 +15,51 @@ | 路径 | 进 git? | 做什么 | |------|----------|--------| | `README.md` | 是 | 克隆后怎么跑 | -| `docs/` | 是 | 说明文档(本文件是目录总表) | +| `docs/` | 是 | 说明文档(本文件是目录总表);**规矩在 [`FRAMEWORK_RULES.md`](FRAMEWORK_RULES.md)** | | `config/site.example.yaml` | 是 | 现场配置模板 | | `config/site.yaml` | **否** | 本机 SN / IP / 相机 / 锁高 | | `config/fastdds_visual_servo.xml` | 是 | 可选 FastDDS;默认不用 | | `scripts/` | 是 | 唯一操作入口 | | `src/` | 是 | ROS 2 包源码 | +| `devices/` | 是 | **设备模块树**(机器人/夹爪/相机/传感器/仿真),见下节 | | `handeye/` | 目录是;`tmp/` 内容 **否** | 手眼标定配置 / 采点 / 结果,见 `handeye/README.md` | | `models/` | 目录与 `.labels.yaml` 是;权重二进制 **否** | 算法权重模块,见 `models/README.md` | -| `build/` `install/` `log/` | **否** | `colcon` 产物,本地编译生成 | +| `build/` `install/` `log/` | **否** | 真机环境的 `colcon` 产物 | +| `build_sim/` `install_sim/` `log_sim/` | **否** | **仿真环境**的产物(`scripts/build_sim.sh`)。两套并存是刻意的:同一份源码、两个环境的依赖路径不同,共用 `install/` 会互相覆盖 | +| `.cursor/rules/` | 是 | 给 AI 的常驻规则(`FRAMEWORK_RULES.md` 的压缩版,权威仍是那份文档) | | `thirdparty/` `vendor/` | **否** | 可选离线 RDK / Isaac 克隆 | | `.gitignore` | 是 | 上面「否」的规则 | --- +## `devices/` — 设备模块树(换品牌 = 加目录,不改代码) + +这一层是"通用化"的落点:上层按 `manifest.yaml` **选型**,不认具体品牌名。 +所以换一台机器人/夹爪/相机/传感器,是**新增一个 `<品牌>` 目录**, +而不是改现有目录或改应用代码。 + +``` +devices/// +├── manifest.yaml # id/label/kind/vendor/version + assets + driver + capabilities + models + config_schema +├── driver/ # 与厂商 SDK 对接的实现(可替换) +└── models/<型号>/ # 该品牌的型号专属文件 +``` + +| kind | 现有品牌 | 说明 | +|------|----------|------| +| `robots/` | `flexiv` | Flexiv Rizon / AICO(RDK) | +| `grippers/` | `dh_pgea` | 大寰 PGEA 平行爪 / RGI 旋转爪 | +| `cameras/` | `realsense` | Intel RealSense D405 / D435 | +| `sensors/` | `orisys` | Orisys 视触觉(三向力 + 深度图 + 力矢量图) | +| `sim/` | `gazebo` | Gazebo 物理仿真平台,手册见 `devices/sim/gazebo/README.md` | + +`devices/registry.yaml` 声明"本仓库装了哪些品牌";`manifest.yaml` 里 +`assets[].required: true` 的物料缺失时框架会报错,并给出**格式与命名要求**。 + +> 选型与接线细节见 [`FRAMEWORK_RULES.md`](FRAMEWORK_RULES.md) 第 1 节(R1.3)。 + +--- + ## `config/` — 现场配置 | 文件 | 作用 | @@ -147,6 +178,7 @@ | 文件 | 内容 | |------|------| +| `FRAMEWORK_RULES.md` | **框架规则**(硬规矩,改代码前先读;第 8 节是按症状查的反例表) | | `WORKSPACE.md` | 本页,目录与职责 | | `QUICKSTART.md` | 环境、编译、第一次实机 | | `ARCHITECTURE.md` | 进程与数据流 | diff --git a/src/arm_interfaces/arm_interfaces/paths.py b/src/arm_interfaces/arm_interfaces/paths.py index 6f177fc..be58563 100644 --- a/src/arm_interfaces/arm_interfaces/paths.py +++ b/src/arm_interfaces/arm_interfaces/paths.py @@ -49,14 +49,6 @@ def registry_path() -> Path: return devices_root() / "registry.yaml" -def assets_root() -> Path: - """Return the root of the model-weight asset tree (``assets/``).""" - override = os.environ.get("SITE_ASSETS_ROOT", "").strip() - if override: - return Path(override).expanduser() - return ros2_root() / "assets" - - def handeye_root() -> Path: """Return the root of the hand-eye module directory (``handeye/``).""" return ros2_root() / "handeye" diff --git a/src/arm_interfaces/test/test_real_devices.py b/src/arm_interfaces/test/test_real_devices.py index 3a98c72..c191c3a 100644 --- a/src/arm_interfaces/test/test_real_devices.py +++ b/src/arm_interfaces/test/test_real_devices.py @@ -33,7 +33,7 @@ class TestRealWorkspace: def test_contract_satisfied(self, monkeypatch): """The shipped device tree and site.yaml must pass validation.""" # Guard against a leftover SITE_* override from another test leaking in. - for var in ("SITE_DEVICES_ROOT", "SITE_CONFIG", "SITE_ROS2_ROOT", "SITE_ASSETS_ROOT"): + for var in ("SITE_DEVICES_ROOT", "SITE_CONFIG", "SITE_ROS2_ROOT"): monkeypatch.delenv(var, raising=False) stack = load_device_stack()