Files
ros_contorl/devices/sim/gazebo/urdf/arm.sim.sensors.xacro
T
arm_dev c1cd862eea 真机落地前的仿真补齐:夹持/腕力/真值位姿,让仿真能验证 S07–S09
这批是 09-24 那轮仿真工作的收尾,一直没提交。内容按目的分四块:

1) 仿真真能「夹住」工件
   sim_grasp_lock:给规与夹爪之间加接触锁(默认关,run_gauge_assembly --sim 才开)。
   没有它时仿真里规只靠摩擦,S08 棘轮一转就滑出指间,于是把「仿真不真」误读成
   「力判据不对」。新增 test_sim_grasp_lock.py。

2) 腕部力传感器进仿真
   sim_wrist_ft(+ config/wrist_ft.yaml):S07 的首选判据是腕部 FT 的轴向突变,
   而仿真里以前根本没有腕部力,S07 只能退到触觉兜底,两套判据从没在同一场景下比过。
   现在两条通道都在,接触判定可对照。

3) 真值位姿可查(不再靠猜)
   sim_live_pose / sim_pose_service:把 Gazebo 里刚体的实测位姿发布出来,
   stage2 的真值路径按**实测**位姿算孔位,而不是按场景文件里的标称位姿。
   顺带把真值孔位的推导(truth_holes)独立出来并补测。

4) 工具几何成为可配置项
   config/sim_tool_geometry.yaml + stage2 的 tool_tip_below_flange_m 启动参数:
   仿真里规会在指间下滑,工具长每次装夹都不同(实测见过 243/254mm),
   悬停高度与 S07 下探预算必须按实测值算,不能再写死。

应用层同时跟进:gauge_fsm / gauge_config 把「悬停时工具尖端入没入工件」的判据
(hover_z_guard)与拖件判定(drag_verdict)收成纯函数并补测;stage3 搜孔、
site 导出、run_gauge_assembly 与若干文档同步。
新增 test_gauge_exit_lift / test_gauge_gripper_client / test_node_handler_collisions。

测试:全仓库 1085 passed。
2026-09-28 10:17:06 +08:00

371 lines
22 KiB
XML
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.
<?xml version="1.0"?>
<!--
=============================================================================
外挂传感器:眼在手上的相机 + 双指触觉。
分工(按"装在哪"划分,不是按品牌):
D405 眼在手上(eye-in-hand)—— 装在法兰上,跟着手走 → 属于**机器人**,
所以在这个文件里,跟着 URDF 一起展开
D435 眼在手外(eye-to-hand)—— 装在工作台支架上,俯视台面 → 属于**场景**,
所以在 worlds/assembly.world 里(每台相机只出一份定义,不重复)
为什么 D435 不能挂在 URDF 的 world link 上(实测踩过):
挂了之后整台相机在转 SDF 时凭空消失。sdformat 会丢弃无质量的根 link,
连带它整棵子树;而它**退出码 0、不报任何错**,只是 SDF 里再也找不到
d435 的 frame 和 sensor,启动后话题永远没数据。
另一个理由:渲染系统插件必须是 world 级的,相机和插件放一起最不容易错配。
=============================================================================
和真机的对应关系(重要)
=============================================================================
真机上是 RealSense,出 RGB + 深度。仿真用 rgbd_camera 同时给两者,
话题名对齐 ros_realsense,应用层(跟踪、粗定位)才能不加分支地两边通用:
真机 / 仿真: /d405/color/image_raw + /d405/depth/image_rect_raw
换相机型号时只改这里的尺寸/内参,应用层不用动。
============================================================================
插件文件名
============================================================================
Fortress 的传感器系统插件是 libignition-gazebo6-sensors-system.so,
名称 ignition::gazebo::systems::Sensors。实测确认过:
ls $CONDA_PREFIX/lib/libignition-gazebo6-sensors-system.so
换到更新版本(Harmonic+)后名字会变成 libgz-sim*-sensors-system.so,
且插件名去掉 ignition:: 前缀 —— 那时要改这里。
=============================================================================
-->
<robot xmlns:xacro="http://www.ros.org/wiki/xacro">
<xacro:macro name="arm_sim_sensors"
params="
prefix
sensors_file:=''
d405_enable:=true
tactile_enable:=true
wrist_ft_enable:=true
wrist_ft_topic:='/sim/ft_sensor_wrench'
d405_x:=0.0
d405_y:=-0.06
d405_z:=0.05
d405_roll:=0.0
d405_pitch:=-1.5708
d405_yaw:=0.0
image_width:=848
image_height:=480
image_hz:=30
">
<!-- =======================================================================
图像尺寸(image_width / image_height)—— **必须与真机一致**
=======================================================================
⚠️ 注意:这段说明**不能**写成上面 params="..." 里的 XML 注释 ——
XML 不允许在属性值里放注释,那样整个文件会 not well-formed,
xacro 直接展开失败(踩过)。所以只能放在这里。
真机 D405 跑 848x480@30(见 config/site.yaml 的 camera.width/height)。
仿真也必须 848x480,理由是**像素尺度的可迁移性**:
阶段二的孔检测(hole_detect_hough)直接在**原始分辨率**图上跑
HoughCircles,不做缩放。所以 hole_detect.yaml 里那些以像素为单位的
门槛(hough_min_radius 10 / min_radius 10 / hole_distance_min 90 …)
只有在"每毫米对应多少像素"与真机相同时才有意义:
fx = width / (2·tan(hfov/2))
视场角两边都是 87°,所以像素尺度完全由 width 决定:
真机 848 → fx ≈ 447
曾用 640 → fx ≈ 337(每毫米像素数只有真机的 75%)
用 640 的后果很具体:58mm 的两孔间距在真机上成像约 152px(过 90 的门槛),
在仿真里只剩 115px —— 仍是能过,但整套像素门槛的余量都被吃掉 25%,
而"看起来不对"的表现只是"孔检测偶尔不认",极难归因。
所以在仿真侧就把尺度对齐,别留给调试。
启动时会比对这里与 cameras.yaml 的 width/height,不一致会告警
(见 sim_cameras.check_camera_consistency)—— 图幅写错不报错,
只会让像素门槛静默失效,所以要主动查。
======================================================================= -->
<!-- =======================================================================
传感器系统插件放在 **world 文件**里,不放这里
=======================================================================
渲染是所有传感器共用的世界级能力,而且 D435 在 world 里(见下)。
插件与相机分处两地的话,很容易出现"插件在模型里、相机在世界里"
这种错配 —— 症状是话题永远没数据且不报错。
所以统一由 worlds/assembly.world 声明一次,机器人上的 D405
和触觉也由它一并渲染。
======================================================================= -->
<!-- =======================================================================
RealSense D405 —— 眼在手上
=======================================================================
位姿默认值说明(法兰坐标系):
xyz: x=0, y=-0.06, z=+0.05
z=+0.05 相机略微伸出法兰,避免看到夹爪本体
y=-0.06 偏置到一侧,与真机上的支架安装位置一致
rpy: roll=0, **pitch=-1.5708(-90°)**, yaw=0
这些值**不是**真机的标定结果,只是仿真里的合理起始位。
仿真里"手眼关系"本来就精确已知(我们直接定义了相机的安装位姿),
所以不需要跑手眼标定 —— 但保留了可调位姿,方便模拟"标定不准"的情况,
验证跟踪算法对误差的容忍度。
⚠️⚠️ pitch 这一项是**必须**的,不是"随手调的角度"。原因在 Gazebo 的
相机惯例:**传感器/模型的视轴是 +X**(不是 +Z)。世界相机 d435 就是靠
在 world 文件里给它模型 pitch=50° 才瞄准台面的 —— 同一个惯例。
腕相机要在法兰垂直朝下时看工件,就得让 d405_link 的 +X 对上法兰的 +Z,
即绕 Y 转 -90°。
为什么"少转这 90°"极难发现(曾经默认 0,踩过):
· 话题有数据、预览有图像、camera_info 内参也对;
· 深度反投影甚至"通过"了检查 —— 因为它看到的是**远处的地面**,
反投影出来同样是 z≈0 的水平面(check_sim_cameras 当时只看
"命中不用平面",判据太松,现在已改成核对视轴朝向);
· 症状只出现在应用侧:S04 跟踪里真值/检测永远说"目标不在相机前方",
伺服一直"寻找工件"原地转圈 —— 看起来像算法或标定问题。
所以:改这里之前先想清楚视轴指向,改完跑
python3 scripts/check_sim_cameras.py
它会核对"腕部相机光学系的视轴是否与法兰 +Z 一致"。
======================================================================= -->
<xacro:if value="${d405_enable}">
<link name="${prefix}d405_link"/>
<joint name="${prefix}d405_joint" type="fixed">
<parent link="${prefix}flange"/>
<child link="${prefix}d405_link"/>
<origin xyz="${d405_x} ${d405_y} ${d405_z}"
rpy="${d405_roll} ${d405_pitch} ${d405_yaw}"/>
</joint>
<gazebo reference="${prefix}d405_link">
<sensor name="d405_rgbd" type="rgbd_camera">
<update_rate>${image_hz}</update_rate>
<always_on>true</always_on>
<!-- visualize=false:headless 下没有 GUI,开着只会白耗渲染资源 -->
<visualize>false</visualize>
<camera name="d405">
<!-- 水平视场角。D405 的实际值约 87°(H) × 58°(V),这里取近似 -->
<horizontal_fov>1.5184</horizontal_fov>
<image>
<width>${image_width}</width>
<height>${image_height}</height>
<format>R8G8B8</format>
</image>
<clip>
<!-- 近裁剪面必须小于相机到工件的最近距离,否则工件会被裁掉 -->
<near>0.02</near>
<far>10.0</far>
</clip>
<depth_camera>
<clip><near>0.02</near><far>10.0</far></clip>
</depth_camera>
</camera>
<!-- 话题名对齐真机 ros_realsense 的命名,应用层两边通用 -->
<topic>/d405</topic>
</sensor>
</gazebo>
</xacro:if>
<!-- =======================================================================
RealSense D435 —— 眼在手外:**不在这个文件里**
=======================================================================
它挂过 world link(URDF 里那个空 link),结果整台相机在转换到 SDF 时
凭空消失:sdformat 会丢掉无质量的根 link,连带它整棵子树。
现象极具误导性 —— sdformat 退出码 0、不报任何错,只是 SDF 里
d435 的 frame 和 sensor 都不存在,启动后话题永远没数据。
正确位置是 world 文件(worlds/assembly.world):眼在手外的相机在物理上
就是装在工作台支架上、不属于机械臂。放那里还有个好处 ——
渲染系统插件必须是 world 级的,相机和插件在一起,不会再出现
"插件在模型里、相机在世界里"这种错配。
======================================================================= -->
<!-- =======================================================================
Orisys 视触觉传感器
=======================================================================
真机上是视触觉:一块弹性体 + 内置相机,读出三向力、深度图、力矢量图。
仿真里没法真的模拟弹性体形变成像,采取的务实方案是**两个内置传感器协作**:
力的三分量 ← <sensor type="force_torque"> 挂在**指尖关节**上
是否接触 ← <sensor type="contact"> 挂在**指尖 link** 上
接触位置 ← 同上,取接触点坐标 → 映射成 centroid_u/v
⚠️ 为什么力必须用 force_torque,而不是从接触传感器拿 —— 这条很反直觉,
而且已经为此走过一次弯路,务必看完:
gz.msgs.Contacts(接触传感器发的消息)**不带任何力**。它的
Contact 消息体里确实有 wrench/force 字段(ros_gz_interfaces/Contact.msg
也照着声明了 wrenches),但 Fortress 的 Contact 系统**从不填充它们**:
实测一条接触消息全文只有 header + 碰撞体名 + 接触点坐标,
连 normal / depth 都是空的。而 Contact 系统的二进制里连一个
"wrench" 字符串都搜不到 —— 所以这不是配置问题,是它设计上就只有位置。
正确的力来源是关节:**转动关节在物理上要承受三个方向的约束力**
(它只放开绕轴转动这一个自由度)。所以挂在指尖关节上的
force_torque 传感器,读到的就是"整条手指链传过来的力" ≈ 接触力
(相差一个手指自身重力的偏置,正好由适配器做基线相减消掉,
这与真机 Orisys "相对静止基线取增量"的口径天然一致)。
由 Gazebo 内置的 forcetorque-system 实现,**无需自写 C++ 插件**。
⚠️ URDF 语法里没有"关节传感器"。这里靠 sdformat 的扩展写法实现:
<gazebo reference="关节名"><sensor .../></gazebo>
sdformat 的 URDF 解析器会把这类 sensor **移进对应的 <joint> 块**
(已实测确认,生成的 SDF 里传感器确实在 joint 内)。
为什么要挂两个(left/right):
真机是双指各贴一片,tangential_1/2 + normal 三路力分别来自两片,
单指的数据无法复现"旋拧时切向力从哪一侧来"这个判据。
⚠️ 单位不是牛顿!真机读数是**形变任意单位**、基线会漂移,
所有阈值都按"相对静止均值的增量"使用(见
devices/sensors/orisys/manifest.yaml)。仿真给的是牛顿,
所以两者的数值**不能直接共用阈值** —— 标度换算由
devices/sim/gazebo/config/tactile.yaml 的 force_scale 负责,
该文件头把两种用法讲清楚了。
⚠️ 三向力的轴映射(哪根轴是指腹法向)是**实测**出来的,不是读代码推的,
结论与实测方法记在同一个 yaml 的 axis: 段里。改夹爪/换传感器后
要重测,否则力的语义会静默错位(有数值、但方向没意义)。
======================================================================= -->
<xacro:if value="${tactile_enable}">
<gazebo reference="${prefix}left_finger_tip">
<sensor name="tactile_left" type="contact">
<always_on>true</always_on>
<update_rate>100</update_rate>
<contact>
<!-- ⚠️ 这个名字不是我们起的,是 sdformat 从 URDF 转 SDF 时**自动生成**的:
规则 =「fixed 关节链的父 link」+ "_fixed_joint_lump__" +「本 link」
+ "_" + collision 名。本机实测生成名(2026-09-23,Fortress 6.16):
sim_left_finger_mount_fixed_joint_lump__sim_left_finger_tip_collision
为什么不得不写死一个"自动生成的名字"(很脆,但有理由):
a) URDF 的 <collision> 不能命名,所以没法给它一个稳定名字;
b) 留空 <contact/> 不会"监测全部",而是被补成 SDF 规范的默认值
`collision=__default__` —— 一个不存在的 collision。
症状是传感器**永远没有输出**,gz 不报错、话题也不出现。
已实测确认(这是本节最大的一条坑)。
所以只能写死,并用启动检查兜住改名:
robot_description.check_tactile_collisions() 会把展开后的
URDF 转成 SDF,核对这两个名字是否存在 —— 一旦 sdformat 改了命名
规则,启动时就会明确报错,而不是静默失效。 -->
<collision>${prefix}left_finger_mount_fixed_joint_lump__${prefix}left_finger_tip_collision</collision>
<!-- ⚠️ 话题必须写在**这里**(contact/topic),不能写在外层的
<sensor><topic> 里 —— 接触传感器只认 contact/topic。
写错位置不会报错:gz 会静默改用默认的 scoped 话题名
/world/<world>/model/<model>/link/<link>/sensor/<sensor>/contact
于是"按 /tactile/left/raw 去订阅"永远收不到东西。实测确认。 -->
<topic>/tactile/left/raw</topic>
</contact>
</sensor>
</gazebo>
<gazebo reference="${prefix}right_finger_tip">
<sensor name="tactile_right" type="contact">
<always_on>true</always_on>
<update_rate>100</update_rate>
<contact>
<collision>${prefix}right_finger_mount_fixed_joint_lump__${prefix}right_finger_tip_collision</collision>
<topic>/tactile/right/raw</topic>
</contact>
</sensor>
</gazebo>
<!-- =====================================================================
三向力:指尖关节上的 force_torque 传感器
=====================================================================
挂在**指尖关节**(left/right_inner_finger_joint)上,而不是指尖 link ——
force_torque 在 SDF 里只能属于关节。这个关节是手指链上最靠近指尖的
一个(child = finger_mount,tip 用固定关节连在它下面),
所以它传过去的力就是整条手指链的力 ≈ 接触力。
三个非轴方向的约束力就是我们要的"三向力":
frame=child → 力的表达在 finger_mount 坐标系里
measure_direction=parent_to_child
→ 读"父 link 施加给子 link 的力"。
静止时它等于 -(接触力 + 手指自重),
所以真正符号如何、哪根轴对应"法向",
以实测为准(见 devices/sim/gazebo/config/
tactile.yaml 的
axis_* 映射项与 README 的实测记录)。
⚠️ 这个传感器是**持续发布**的(即使没有任何接触,也按 update_rate
发零力)。这一点很关键:接触传感器只在接触时发消息,如果适配器
只靠接触消息驱动,那么"离开工件"这段时间下游会认为话题断流。
真机 Orisys 也是持续发布的,所以这里必须与真机一致。
===================================================================== -->
<gazebo reference="${prefix}left_inner_finger_joint">
<sensor name="tactile_left_wrench" type="force_torque">
<always_on>true</always_on>
<update_rate>100</update_rate>
<!-- force_torque 的话题写在外层 <topic> 即可(这点与 contact 不同:
contact 只认 contact/topic,写外层会被静默忽略)。 -->
<topic>/tactile/left/wrench</topic>
<force_torque>
<frame>child</frame>
<measure_direction>parent_to_child</measure_direction>
</force_torque>
</sensor>
</gazebo>
<gazebo reference="${prefix}right_inner_finger_joint">
<sensor name="tactile_right_wrench" type="force_torque">
<always_on>true</always_on>
<update_rate>100</update_rate>
<topic>/tactile/right/wrench</topic>
<force_torque>
<frame>child</frame>
<measure_direction>parent_to_child</measure_direction>
</force_torque>
</sensor>
</gazebo>
</xacro:if>
<!-- =======================================================================
腕部力扭矩(wrist force/torque)—— 与手指触觉是**两回事**,别互相替代
=======================================================================
为什么必须有它(2026-09-24 踩过,代价是整整两轮排查方向错):
"通止规"是**靠夹持摩擦抱住**的工具。轴向力(往下压的力)要传到手指上,
必须经过"指垫↔规"的摩擦面,而摩擦能传的上限是 µ·N:
夹持段 µ=0.25、实测握力 N≈46N → 单指 ~11.5N、两指 ~23N
台肩一落面,接触刚度 5e4 N/m 意味着**过压 0.5mm 就是 25N** —— 一步
步进量就足以让规在爪里打滑。所以手指触觉**根本读不到**"台肩落面"那
一下的力(读数停在 0,而它是对的)。拿手指触觉当"到底了"的判据,
阈值调多大都不对:调小了被噪声骗,调大了物理上永远达不到。
腕部 FT 装在这条力路的**上游**:工件→规→夹爪→法兰→腕部关节。
工具打不打滑都要经过它,所以它测得到真实的轴向力。真机同理 ——
真机上这个量由 Flexiv 的 flexiv_robot_states_broadcaster 发在
/<sn>/ft_sensor_wrench(geometry_msgs/WrenchStamped,法兰系)。
为什么挂在 ${prefix}joint7 而不是 flange_joint:
flange_joint 是 **fixed** 关节,物理引擎不会把它当独立关节求解
(它是刚体的一部分),挂上去的 force_torque 传感器永远没有输出、
而且**不报任何错**。joint7 是最后一个 revolute 关节,它下游挂着
link7 + flange + 夹爪 + 工具 —— 正好是"整个腕部以下"的全部载荷,
这才是工程意义上的腕部 FT。
为什么是 frame=child:
child 是关节的下游连杆(link7)。工具竖直朝下时 link7 的 z 轴
就是工具轴,所以 **z 分量 = 轴向力**,与真机 ft_sensor_wrench
(法兰系,z=法兰轴)口径一致 —— 应用层因此可以两边共用一套判据。
⚠️ 话题名必须与"应用层按 sn 推出的那个名字"一致:
仿真臂的 sn 固定是 `sim`(见 config/site.yaml 的 robot.arms[sim]),
应用层算 `/{sn}/ft_sensor_wrench` = /sim/ft_sensor_wrench。
改 sn 时必须同步改这里,否则只是"话题上没有数据"(不报错)。
-->
<xacro:if value="${wrist_ft_enable}">
<gazebo reference="${prefix}joint7">
<sensor name="wrist_ft" type="force_torque">
<always_on>true</always_on>
<!-- 与真机 flexiv_robot_states_broadcaster 的发布率(100Hz)对齐:
下游按"够不够新"判活,率太低会被当成掉线。 -->
<update_rate>100</update_rate>
<topic>${wrist_ft_topic}</topic>
<force_torque>
<frame>child</frame>
<measure_direction>parent_to_child</measure_direction>
</force_torque>
</sensor>
</gazebo>
</xacro:if>
</xacro:macro>
</robot>