Files
ros_contorl/devices/sim/gazebo/README.md
T
arm_dev 47ad9d9a8b P5c-4 相机出图联调:桥接 d405/d435 到 ROS,并修掉两处静默的几何错误
相机链路(devices/sim/gazebo/config/cameras.yaml 是唯一真相)
  - sim_cameras.py        纯逻辑:话题映射 / 深度换算 / 内参 / 视场一致性比对
  - sim_camera_adapter.py 改光学系 frame_id、32FC1 米 → 16UC1 毫米、覆盖内参
  - gazebo_sim.launch.py  桥接 + 适配器 + 世界相机的静态 TF(位姿从 world 解析)

本轮修掉两个**静默**的几何错误(都能出图、都能查到 TF,但点位是错的)

  1. gz 发的 camera_info 是错的:恒为 fx=fy=277, cx=160, cy=120
     (等于它的默认 60° 视场 + 半分辨率主点),而渲染用的是配置的 87° 视场。
     照抄它做三维反投影,点会偏半米量级;单目预览完全正常。
     用已知地面平面做最小二乘反解,得 fy=337.2 / cy=239.5,与
     640/(2·tan(87°/2))=337.2、480/2=240 吻合(残差中位 0.2mm)。
     改为由 sim_cameras.intrinsics() 按 cameras.yaml 的视场重算并覆盖。
     代价:cameras.yaml 的 horizontal_fov 必须与 SDF 一致,启动时自动比对告警。

  2. assembly.world 里 D435 的 rpy 写成 (0, 50°, 180°),
     而 URDF 相机的视线是**本体 X 轴**、rpy 复合顺序又是 Rz·Ry·Rx,
     于是"先俯 50° 再偏航 180°"把视线翻到了背面:相机背对台面、只看地面。
     它不报错,而且几何自洽(点确实落在真实地面上),只查数值发现不了。
     改为 (0, 50°, 0),并加回归测试锁住。

新增验收工具 + 测试
  - scripts/check_sim_cameras.py  反投影深度、要求命中地面/台面才算通过。
    "有图像"不是验收标准;实测 d435 台面 11.9%/地面 75.3%、d405 台面 37.8%。
  - 97 项单测(新增 sim_cameras 22 项、sim_site 16 项)

顺带修掉一处"改了配置静默无效"
  - sim.world 此前硬编码在 robot_description._WORLD_REL,而 site.yaml 与
    manifest.yaml 都宣称改它就生效。现在真的读了:
    优先级 --world > site.yaml:sim.world > 内置默认(相对路径按仓库根解析)。
  - physics_enable / joint_prefix 仍未接线,但若其值与实际状态冲突,
    启动时会告警(sim_site.unwired_warnings),不再静默忽略。

文档
  - devices/sim/gazebo/README.md 重写:原文还写着"仿真不是 Gazebo"、
    相机插件用 libgazebo_ros_camera.so,与实现早已不符。现分三部分:
    哪个"仿真"是真的 / 现状与缺什么 / 五条静默失效的坑及怎么验。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 15:19:16 +08:00

149 lines
9.5 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.
# devices/sim/gazebo/ —— Gazebo 仿真平台
> 这份文档记录的是**已经跑通的事实**和**实测踩到的坑**。仿真里绝大多数错误都是
> 静默的(不报错、能出图、能查到 TF,结果却是错的),所以下面每条坑都写了
> **怎么验**。改仿真之前先把"坑"那一节看完。
---
## 先读这段:哪个"仿真"是真的
仓库里有两个都叫"仿真"的东西,很容易混:
| 入口 | 底层 | 有物理吗 | 用途 |
|------|------|----------|------|
| `scripts/run_sim.sh` | `arm_bringup` 的 **mock 驱动**(`mock_arm_driver.py`) | ✘ 假数据 + RViz | 验证话题/流程/UI,不耗资源 |
| `scripts/run_sim_gz.sh` | **Gazebo Fortress**(真物理、真接触) | ✔ | 验证接触/力控/几何 |
**要验证装配的接触与力判据,只能用后者。** 前者永远不会产生接触力。
---
## 第一部分:现状(截至 P5c-4,2026-09-23)
### 已就绪
| 部分 | 位置 | 说明 |
|------|------|------|
| 运行环境 | `scripts/install_sim_env.sh` / `sim_env.sh` | 独立 conda 环境 `ros_gz_sim`(Gazebo Fortress 6.16 + ROS 2 Humble),与真机环境 `ros_flexiv` 隔离 |
| 世界 | `worlds/assembly.world` | 地面 + 工作台 + D435 + 通止规标记位 |
| 机器人描述 | `urdf/*.xacro` | 复用官方 `flexiv_description`,启动时**归一化**后喂 Gazebo |
| 控制器 | `config/controllers.yaml` | 控制器名与真机一致(`rizon_arm_controller` + `joint_state_broadcaster`) |
| 启动器 | `src/arm_sim_gazebo` 的 `gazebo_sim_launcher` | 起 Gazebo → 发归一化 URDF → spawn → 拉起控制器 |
| 相机 | `config/cameras.yaml` | 两台相机的**唯一真相**:话题、坐标系、视场 |
### 还没做
| 部分 | 缺什么 | 影响 |
|------|--------|------|
| `models/` | 工件(k0/k1/k2)与通止规模型 | 装配任务(P6)跑不起来 |
| `sensor_plugins/` | Orisys 三向力插件 | **无法验证力控**(S07 接触、S08 拧紧判据) |
> **Orisys 视触觉没有现成的 Gazebo 插件。** 3 路切向力 + 1 路法向力 + 深度图 +
> 力矢量图这套数据必须自己写插件。建议做法:订阅 Gazebo 的接触传感器数据,
> 按接触深度与相对滑动速度合成一组"类触觉"读数,再按真实传感器的量纲发布到
> 同样的话题上(`/tactile/{left,right}/status`,`arm_msgs/TactileStatus`)。
> 在它做出来之前,仿真里跑 P6 只能验证**几何与流程**,力判据要等真机。
---
## 第二部分:日常怎么用
### 启动
```bash
bash scripts/run_sim_gz.sh # 无头(无显示器环境用这个)
bash scripts/run_sim_gz.sh --gui # 带 Gazebo GUI
bash scripts/run_sim_gz.sh --check # 只自检描述,不启动任何东西
bash scripts/run_sim_gz.sh --no-gripper --no-sensors # 最省资源,只验运动学
```
首次启动约需 10~20 秒(Gazebo 初始化 + spawn + 拉控制器)。
### 验收(★ 必做,别只看"有没有图像")
```bash
ros2 topic echo /clock --once # 仿真时间在走
ros2 control list_controllers # 两个控制器 active
ros2 topic echo /joint_states --once # 7 个 sim_joint
python3 scripts/check_sim_cameras.py # ★ 相机的真正验收
```
**`check_sim_cameras.py` 才是相机的验收标准。** 它把深度反投影到 world,
检查落点是否命中已知平面(地面 z=0、台面 z=0.40)。"有图像"证明不了什么 ——
详见下面的坑 4 与坑 5。
### 放置格式与命名要求
| 类型 | 放哪 | 格式 | 命名要求 |
|------|------|------|----------|
| 世界文件 | `worlds/` | `.world` / `.sdf` | `<场景名>.world`,如 `assembly.world` |
| 机器人模型 | `urdf/` | `.urdf` / `.xacro` | 关节前缀必须与真机不同(仿真用 `sim_`) |
| 工件/夹具网格 | `models/<模型名>/` | `<模型名>.sdf` + `meshes/` | **模型名必须与推理用的类别名一致**,如 `workpiece` |
| 传感器插件 | `sensor_plugins/` | `.so` 源码 + `CMakeLists.txt` | 插件类名以 `Orisys` 开头 |
| 网格文件 | `models/<模型名>/meshes/` | `.stl` / `.dae` | **单位必须是米**。用毫米建模的必须先缩放到米 |
| 相机声明 | `config/cameras.yaml` | YAML | `ros_ns` 必须等于应用启动时的 `camera_name` |
### 改配置:改哪里才会真正生效
| 想改什么 | 改哪里 | 真的生效吗 |
|----------|--------|------------|
| 用哪个世界 | `site.yaml` → `sim.world`,或命令行 `--world` | ✔ 已接线(优先级:`--world` > `site.yaml` > 内置默认) |
| 相机的视场/坐标系/话题 | `config/cameras.yaml`,**并且**同步 SDF 里对应的 `horizontal_fov` | ✔ 启动时自动比对,不一致会告警 |
| 物理开关 | `site.yaml` → `sim.physics_enable` | ✘ **未接线**。要真关,改 `worlds/assembly.world` 里的 `<gravity>` |
| 关节前缀 | `site.yaml` → `sim.joint_prefix` | ✘ **未接线**。前缀由 `urdf/arm.sim.urdf.xacro` 的 `arm_prefix` 决定,且 `config/controllers.yaml` 硬编码了 `sim_joint1..7` —— 三处要一起改 |
| 仿真里的悬高/速度 | **复用真机那套参数**(`servo.*` / `hole_detect.stage2.*`) | ✔ |
> **设计约束**:仿真与真机必须共用同一套 `site.yaml` 参数。任何"仿真专用参数"都是
> 未来"仿真调好的值搬到真机不生效"的隐患。只允许物理引擎自身的参数(重力、步长)
> 放在 `sim.*` 下。
>
> ✘ 标记的两个键由 `arm_sim_gazebo.sim_site.unwired_warnings()` 守着:只要你写的值
> 和实际状态不一致,启动时就会告警。**不要让它们重回静默。**
---
## 第三部分:实测踩到的坑(都属于"静默失效")
这些坑的共同特征是:**不报错**。话题在、图像有、TF 查得到、`create` 打印成功,
但结果是错的。所以每条都配了"怎么发现"。
| # | 坑 | 症状 | 怎么发现 / 怎么修 |
|---|----|------|-------------------|
| 1 | 官方 `flexiv_description` 直接喂 Gazebo 会被 `sdformat` 拒掉:7 轴本体缺 `<inertial>` 包裹;夹爪有零质量 link 导致惯性叠加出 NaN | 只有 Gazebo 拒绝,RViz/MoveIt 完全正常 | 启动时**归一化**(`urdf_normalize.py`)。单独排查:`gazebo_sim_launcher --check` |
| 2 | world 里只要声明了**任何一个** `<plugin>`,就会挤掉默认的 Physics / UserCommands / SceneBroadcaster | 少了 UserCommands → `/world/<名>/create` 服务不存在 → 模型根本没进 Gazebo,而 `create` 仍打印 `OK creation of entity`;控制器永远起不来 | 启动时会检查并告警。修法:在 world 里显式声明这三个系统 |
| 3 | gz 的插件搜索路径不含 `$CONDA_PREFIX/lib`(`ros_gz_sim` 的 launch 只拼 `LD_LIBRARY_PATH`,而 conda 激活不往那里加) | `gz_ros2_control` 报 `couldn't find shared library`,而库其实就在那儿(`ldd` 全齐) | 必须在 `ros2 launch` **之前**设 `GZ_SIM_SYSTEM_PLUGIN_PATH`;资源路径同理(否则 mesh 全加载不出来)。都由 `scripts/sim_env.sh` 负责 |
| 4 | **gz 发的 `camera_info` 是错的**:恒为 `fx=fy=277, cx=160, cy=120`(它的默认 60° 视场 + 半分辨率主点),而**渲染**用的是配置的 87° 视场 | 照抄它做三维反投影,点会偏**半米量级**;单目预览完全正常 | 用已知地面平面做最小二乘反解内参,得到 `fy=337.2 / cy=239.5`,与 `640/(2·tan(87°/2))=337.2`、`480/2=240` 吻合(残差中位 0.2mm)。已改为由 `sim_cameras.intrinsics()` 按 `cameras.yaml` 的视场**重算并覆盖** |
| 5 | **相机位姿的 `rpy` 顺序**:URDF 相机是"X 朝前"约定,**视线是本体 X 轴**;而 `rpy` 复合顺序是 `Rz·Ry·Rx`,"先俯 50° 再偏航 180°"会把视线翻到**背面** | D435 背对台面、只看得见地面。什么都不报,**而且几何自洽**(点确实落在真实地面上),只查数值发现不了 | 用 `scripts/check_sim_cameras.py` 检查"该看见的平面有没有看见"。已修正 + 加回归测试 |
关于坑 4:代价是 `cameras.yaml` 的 `horizontal_fov` 必须与 SDF 真正渲染用的值一致。
启动时会自动比对并告警(`sim_cameras.check_fov_consistency`)。
关于相机还有两条**规格差异**,是有意保留的(不是 bug,但会让人困惑):
- **畸变**:gz 不模拟镜头畸变(`d` 全 0,`R` 单位阵)。所以"仿真里用畸变矫正
调出来的参数"搬到真机仍需重调。
- **分辨率**:真机 D405 是 848x480、D435 是 1280x720,仿真建模用 640x480。
对"算法能不能跑通"没影响(内参从 `cameras.yaml` 自洽推导),
但"仿真里调好的像素阈值搬到真机"会因像素尺度不同而需要重调。
---
## 相关文件一览
```
devices/sim/gazebo/
├── manifest.yaml # 设备清单:进度、配置声明、放置要求
├── README.md # 本文档
├── config/
│ ├── cameras.yaml # ★ 两台相机的唯一真相(话题/坐标系/视场)
│ └── controllers.yaml # 控制器配置(名字与真机一致)
├── urdf/ # 仿真用机器人描述(含 ros2_control 与传感器)
├── worlds/assembly.world # 场景
├── models/ # ✘ 待做:工件/通止规/夹具
└── sensor_plugins/ # ✘ 待做:Orisys 触觉插件
```
配套代码:`src/arm_sim_gazebo/`(启动器、描述归一化、相机适配器)。
配套脚本:`scripts/run_sim_gz.sh`(入口)、`scripts/check_sim_cameras.py`(相机验收)。