Files
arm_dev d32e6ae53f P5c-6 场景物料落地 + Gazebo 中文手册:工件/通止规模型、场景校验、两处静默失效修复
一、场景物料(新增)
  devices/sim/gazebo/models/workpiece/   带三个孔的铝板。碰撞体是"带方孔的平板",
    视觉额外加两个 φ14/φ7 亮环 —— 因为真机的 HoughCircles 就是靠这两个亮环找孔,
    只按碰撞体建视觉会让"孔检测"在仿真里永远失败,而那看起来像算法问题。
  devices/sim/gazebo/models/gauge/       通止规:通端/套环/握柄/止端四段,各段异色
    便于肉眼判断方向。接触刚度刻意比台面软 —— 位置控制器会"刚性压进去",
    用默认刚度会被弹飞(注释里写了原因,不是随手抄的参数)。
  devices/sim/gazebo/models/gauge_stand/ 规座(static)。规直接站在台面上会立刻倒,
    加个方孔底座把它立住。
  三个模型都是 model.sdf + model.config,原点在**底面中心**,单位米。

二、场景配置与校验(新增)
  config/scene.yaml     物体不再写死在 world 里,改为 scene.yaml + 启动时动态 spawn。
    理由:world 是"环境"(地面/台面/相机/系统插件),场景是"这一批活做什么"。
    动 world 的风险面太大 —— 它只要丢一个系统声明,所有模型都 spawn 不进去(见旧坑 2)。
    含 operating(相机到工件的距离等作业条件)与 gates_px(像素门槛)。
  sim_scene.py          摆放校验:从 SDF **逐碰撞体**解析 AABB(不抄尺寸,避免漂移),
    检查原点在底面 / 不穿台面 / 不出界 / 物体之间不嵌入。
    重叠判定必须逐碰撞体三维判,不能拿总包围盒相交 ——
    正常装配里本来就有嵌套(规立在规座方孔里),粗判会误报,
    而误报的后果是人把检查整个关掉,之后真重叠也抓不到。
    另有成像几何:把亮环/孔间距按相机距离换算成像素,与门槛比对。
  scripts/check_sim_scene.py  验收工具(退出码可用):摆放 + 门槛漂移 + 成像几何 +
    悬停高度 vs 工作空间 + 手外相机是否**完整**看得见工件 + Gazebo 实际位姿。

三、修掉两处静默失效(都是"看起来一切正常")
  1) parse_spawned_poses 一直是**死代码**:它按 "x: N / y: N / z: N" 解析,
     而 Fortress 实际输出是方括号两行(XYZ / RPY)。对不上 → 永远返回空 →
     调用方按设计"降级成跳过位姿核对" → 位姿检查**从来没跑过**,
     而日志只说"跳过",看起来像正常降级。
     现在两种格式都支持,并用真实抓下来的输出样本做回归测试。
     教训:宽容降级会把"没检查"伪装成"检查通过"。
  2) stop_sim_gz.sh 的模式表里漏了 robot_state_publisher(而注释里写着它有覆盖):
     脚本报"已停止",它却还在发上一份 /robot_description 与 TF,
     下一次启动就会"改了描述却不生效"。
     顺带修掉同源的两个问题:awk 用 -v 传模式导致它匹配到自己、每次复查都多报
     一个空命令行的幽灵 PID;以及"杀的时候"和"复查的时候"两份扫描实现会各自漂移。
     现在共用同一个 scan_pids(),模式改用环境变量传递,并在杀完之后**再扫一遍**:
     有残留就报出来并非零退出 —— 下次再有漏项,不再静默。
     实测:--list 列出全套 9 个进程;停止后 rc=0 且 pgrep 干净。

四、相机:D405 分辨率对齐真机
  仿真的 D405 此前渲染 640x480,而真机跑 848x480(fx 337 而非 447)。后果是
  58mm 的两孔间距成像只剩 85px,低于 hole_distance_min=90 ——
  孔检测在仿真里根本不可能通过,而现象看起来像"算法不行"。
  已把 xacro 与 cameras.yaml 一起改成 848x480,并把启动时的比对从"只比视场"
  扩成"视场 + 图幅"(sim_cameras.check_camera_consistency),
  不一致时说明它对像素门槛的影响。

五、文档
  devices/sim/gazebo/README.md 重写为中文手册:使用方法 / 建模要求 / 导入格式 /
    配置方式(含"改哪里才真的生效"的接线表)/ 11 条实测坑(每条都有"怎么发现")。
  manifest.yaml 的进度、assets、description、坑清单与本轮状态对齐。
  scripts/README.md 补两个验收工具与停止脚本;根 README 补仿真入口与手册链接。
  cameras.yaml 里一处过期的函数名(check_fov_consistency)修正。

六、测试
  新增 test_sim_scene.py(79 条,此前 sim_scene.py 完全没有单测 —— 这正是
    位姿解析死掉却无人发现的原因)与 test_check_sim_scene.py(21 条),
  含三条回归:world_boxes 的偏移碰撞体不能被对称化放大(假重叠)、
  center_bore 不能被 _bore 后缀吃掉、Fortress 位姿输出格式。
  全量:src/arm_sim_gazebo + arm_interfaces + arm_application 共 515 通过。

同时附上 config/site.example.yaml 的重新生成(此前 site.yaml 的注释改动没同步,
它的 parity 测试是红的)。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 17:13:00 +08:00

196 lines
12 KiB
YAML
Raw Permalink 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/manifest.yaml
#
# Gazebo 仿真平台清单。
#
# 进度(截至 P5c-6,如实记录,不要凭印象改):
# ✔ 环境 独立 conda 环境 ros_gz_sim(Gazebo Fortress + ROS 2 Humble)
# 安装见 scripts/install_sim_env.sh,激活见 scripts/sim_env.sh
# ✔ 世界文件 worlds/assembly.world(sdformat 校验通过)
# ✔ 机器人描述 urdf/(复用官方 flexiv_description,已通过 sdformat 校验)
# ✔ 控制器 config/controllers.yaml(控制器名与真机一致)
# ✔ 描述归一化 arm_sim_gazebo 包的 urdf_normalize(37 项单测)
# ✔ 启动器 gazebo_sim_launcher 已实现并端到端跑通
# 起 Gazebo → 发归一化 URDF → spawn → 拉起两个控制器(active)
# 入口脚本 scripts/run_sim_gz.sh(此前缺失,被 install_sim_env.sh
# 与 build_sim.sh 的提示语引用,照做会报文件不存在)
# ✔ 场景模型 models/ 已有工件(带检测亮环的孔板)、通止规、规座
# 布局在 config/scene.yaml,启动时动态 spawn 并做摆放/几何校验
# ✔ 触觉 **不需要自定义插件**:用 Gazebo 内置的 force_torque(三向力)
# + contact(接触布尔)传感器,由 sim_tactile_adapter 合成
# arm_msgs/TactileStatus。标定与轴向映射在 config/tactile.yaml
# (轴向是实测的,不是猜的)。视触觉图像只能是占位。
# ✔ 相机出图 D405/D435 已注册并桥接到 ROS,且**几何正确性已验证**
# 桥接+适配:config/cameras.yaml → sim_cameras(纯逻辑:
# 话题映射 / 深度换算 / 内参,单测覆盖)
# → sim_camera_adapter(改光学系 frame_id、米→毫米、覆盖内参)
# 验收工具:scripts/check_sim_cameras.py(反投影必须命中地面/台面)
# 实测:d435 台面 11.9% / 地面 75.3%;d405 台面 37.8% / 地面 54.0%
# ✔ 配置接线 sim.world 现在真的从 config/site.yaml 读(此前硬编码,改了无效)
#
# 所以:相机、触觉三力、场景物体都可用,装配状态机(P6)可以在这里验几何与接触。
# 仍然做不到的:视触觉的**图像**(深度图/力矢量图只有占位),以及走真机那套
# 夹爪服务接口(仿真里目前直接发关节指令)。
#
# 实测踩到的坑(都属于"静默失效",后续改仿真务必先看这几条):
# 1. 官方 flexiv_description 直接喂 Gazebo 会被 sdformat 拒掉:7 轴本体缺
# <inertial> 包裹;夹爪有零质量 link 导致惯性叠加出 NaN。两者都只有
# Gazebo 会暴露,RViz/MoveIt 完全正常。已在启动时归一化解决。
# 2. world 文件里只要声明了**任何一个** <plugin>,Gazebo 就不再加载默认的
# 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" 而库其实就在那儿。必须在 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° 视场。
# 照抄它做三维反投影,点会偏半米量级,而单目预览完全看不出来。
# 已改为由 sim_cameras.intrinsics() 按 cameras.yaml 的视场重算并覆盖。
# 代价:cameras.yaml 的 horizontal_fov 必须与 SDF 一致,启动时会自动比对告警。
# 5. **相机位姿的 rpy 顺序**:URDF 相机是"X 朝前"约定,**视线是本体 X 轴**;
# 而 rpy 复合顺序是 Rz·Ry·Rx,"先俯 50° 再偏航 180°"会把视线翻到背面。
# d435 就踩了这个坑:相机背对台面、只看得见地面,却什么都不报,
# 而且几何自洽(点确实落在真实地面上),只查数值发现不了。
# 已修正并加回归测试(test_robot_description 的 d435 位姿用例)。
# 6. 接触传感器的消息是**事件式**的:只在接触时发,离开时不发。写成
# "收到=接触/没收到=不管"会让 contact 永远停在 True。判据必须是
# "最近 contact_timeout_s 内收到过"。
# 7. **contact-system 发的 Contacts 消息里没有力**(wrenches 恒空,实测确认)。
# 消息结构里有这个字段,看起来"应该有"—— 这是最耗时的一个坑。三向力改由
# force_torque 传感器提供;且它所需的 forcetorque-system 也必须像坑 2 那样
# 在 world 里显式声明,否则话题根本不出现。
# 8. **sdformat 会用占位值填缺失的东西**:<contact> 缺 <collision> → "__default__",
# 缺 <topic> → "__default_topic__"。于是"检查字段是否为空"的校验代码永远通过。
# check_tactile_collisions 已显式判这两个占位值(有负向单测)。
# 通用教训:校验代码自己会静默失效。
# 9. **<mimic> 标签会被 sdformat 丢掉**:夹爪从动指节不再跟随(控制器 active
# 但手指不动)。解法是在归一化时记录 mimic 关系、去掉标签、把全部 7 个夹爪
# 关节都接入 ros2_control(从动关节原先 effort=0,也一并修掉)。
# 10. `ign model -m <名> -p` 的文本输出没有稳定契约,验收脚本按"宽松解析 +
# 解析不出来就跳过"处理 —— 宁可漏报,不可误报。
# =============================================================================
id: gazebo
label: Gazebo 仿真
kind: sim
vendor: openrobotics
# 实测版本。注意:Humble 配的是 Fortress(ignition-gazebo6),不是现代 gz-sim ——
# 后者要 libprotobuf>=6.31,与 RoboStack 给 Humble 钉的 5.29.3 解不开依赖。
version: "Fortress 6.16 (ignition-gazebo6, ros_gz + gz_ros2_control 0.7.15)"
description: >
物理仿真平台。环境、世界、机器人描述、控制器与启动器均已就绪并端到端验证
(起 Gazebo → spawn → 两个控制器 active → 机械臂在重力下站住)。
D405/D435 已桥接到 ROS,且几何正确性有自动验收(scripts/check_sim_cameras.py)。
场景物体(工件/通止规/规座)在 devices/sim/gazebo/models/,布局在 config/scene.yaml,
摆放与成像几何有自动验收(scripts/check_sim_scene.py)。
触觉用 Gazebo 内置的 force_torque + contact 传感器,由 sim_tactile_adapter
合成 arm_msgs/TactileStatus;轴向映射是实测的(config/tactile.yaml)。
视触觉的**图像**(深度图/力矢量图)在仿真里只能是占位,不可信。
sim.world 已真正从 config/site.yaml 读取(physics_enable / joint_prefix 尚未接线,
写了不一致的值会告警而非静默忽略,见文件头 config_schema)。
手册见 devices/sim/gazebo/README.md(含建模要求、导入格式、配置方式、避坑清单)。
# --- 物料放置要求 ----------------------------------------------------------
assets:
- path: worlds
required: false
what: Gazebo 世界文件(.world / .sdf)
format: Gazebo 原生格式
naming: 场景名.world,如 assembly.world
where_used: 由 site.yaml 的 sim.world 指向
- path: models
required: false
what: 工件 / 夹具 / 通止规模型
format: >
每个模型一个子目录,内含 model.sdf 与 model.config(两个文件名都是固定的,
Gazebo 按这个约定查找)。原点必须在**底面中心**,这样 scene.yaml 里的
pose.z 就等于"放在台面上的高度"。碰撞体用 box/cylinder 等简单几何,
视觉可以额外加只服务于算法的细节(如工件上的检测亮环)。
naming: >
目录名 = SDF 里的 <model name> = model.config 里的 <name> = 推理用的类别名
(如 workpiece),四者必须一致,否则仿真里算出的坐标与推理结果对不上。
网格单位必须是米,用毫米建模的必须先缩放。
where_used: >
由 config/scene.yaml 的 objects[].model 引用,启动时动态 spawn;
摆放合法性与成像几何由 scripts/check_sim_scene.py 验收。
- path: urdf
required: false
what: 仿真版机器人描述
format: .urdf / .xacro,需含 gazebo 与 ros2_control 标签
naming: 关节前缀必须以 sim_ 开头,与真实臂(Rizon4-xxxxxx_)区分
where_used: 仿真机器人加载
- path: sensor_plugins
required: false
what: 传感器插件(自定义 C++ 插件用;当前**为空**)
format: 插件源码 + CMakeLists.txt
naming: 插件类名以 Orisys 开头
where_used: >
目前没有使用者:触觉走 Gazebo 内置的 force_torque + contact 传感器,
由 sim_tactile_adapter(Python)合成 TactileStatus,无需自研插件。
保留此目录用于"以后真需要写插件"的场景(如自研视触觉弹性体成像)。
# --- 适配器 ----------------------------------------------------------------
driver:
package: arm_sim_gazebo
executable: gazebo_sim_launcher
build: colcon
notes: >
已实现并在 setup.py 注册。它只做"准备描述(展开 + 归一化)+ 转发给
launch/gazebo_sim.launch.py",启动逻辑不在可执行文件里 —— ros2 launch 的
日志/退出码/中断处理都更成熟,自己重造一份只会更难调试。
入口建议走 scripts/run_sim_gz.sh(它会先 source scripts/sim_env.sh,
而 Gazebo 的插件/资源搜索路径必须在 ros2 launch 之前设好,见 sim_env.sh)。
单独排查描述用:gazebo_sim_launcher --check / --print-urdf。
# --- 配置项声明 ------------------------------------------------------------
# 设计约束:仿真与真机共用同一套 site.yaml 参数(servo.* / hole_detect.stage2.*),
# 避免「仿真调好的值搬到真机不生效」。只有物理引擎自身的参数放在 sim.* 下。
#
# ⚠️ 每个键都必须如实标注"是否真的接线"(wired)。声明了却没接线的键是最难查的
# 一类问题:用户按文档改了 site.yaml,行为毫无变化,也不报错。
# 目前由 arm_sim_gazebo.sim_site 读取的是 world(已接线);
# physics_enable / joint_prefix 尚未接线,若其值与世界/描述的实际状态冲突,
# 启动时会打印告警 —— 见 sim_site.unwired_warnings()。
config_schema:
- key: world
site_path: sim.world
type: str
required: false
default: ""
wired: true
doc: >
仿真世界文件路径。留空 = 用内置默认(devices/sim/gazebo/worlds/assembly.world)。
相对路径按仓库根解析。优先级:命令行 --world > 本项 > 内置默认。
实现在 arm_sim_gazebo/sim_site.resolve_world()。
- key: physics_enable
site_path: sim.physics_enable
type: bool
required: false
default: true
wired: false
doc: >
是否启用物理引擎。**尚未接线**:世界文件里的 <physics>(重力/步长)是写死的,
本项不会传导过去。若这里写的值与世界实际状态不符,启动时告警而不是静默忽略。
要真正生效,需改 world 文件里的 <gravity>,或把本项接进启动流程。
- key: joint_prefix
site_path: sim.joint_prefix
type: str
required: false
default: sim_
wired: false
doc: >
仿真关节名前缀。必须与真实臂的前缀(Rizon4-xxxxxx_)不同,
否则仿真与真机的 TF 会互相污染。
**尚未接线**:实际前缀由 urdf/arm.sim.urdf.xacro 的 arm_prefix 决定,
且 config/controllers.yaml 里硬编码了 sim_joint1..7。写了个不同的值会告警。
要真改,这三处必须一起改。