真机落地前的仿真补齐:夹持/腕力/真值位姿,让仿真能验证 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。
This commit is contained in:
@@ -70,6 +70,12 @@ python3 scripts/check_sim_gripper.py # ★ 夹爪的几何验收(开
|
||||
机械臂应当**在重力下保持站住**。若它瘫下去或消失,说明描述有问题 ——
|
||||
见 [坑 1](#坑-1官方-flexiv_description-直接喂-gazebo-会被-sdformat-拒掉)。
|
||||
|
||||
> ⚠️ `check_sim_scene.py` 是**跑前**检查("现在这一轮能不能开始"),不是跑后检查:
|
||||
> 一轮装配跑完之后场景**必然**是脏的 —— 放手时抓取锁释放、规掉在台面上,
|
||||
> 落点可能磕到工件。所以跑完再查会报红,这是正常的。下一轮
|
||||
> `run_gauge_assembly.sh --sim` 开头的上料脚本会把 `scene.yaml` 里**所有**物体
|
||||
> 复位回名义位姿(`sim_load_gauge.py::_reset_scene_objects`),届时就干净了。
|
||||
|
||||
**这几条 `check_*.py` 才是验收标准,"有图像 / 服务有返回"证明不了什么。**
|
||||
它们做的判断分别是:
|
||||
|
||||
@@ -532,6 +538,88 @@ python3 -m pytest src/arm_sim_gazebo/test/test_truth_holes.py -q
|
||||
|
||||
---
|
||||
|
||||
### 坑 14:上一轮把工件推走了 → 下一轮 S04 报"一个视觉目标点都没收到"
|
||||
|
||||
现象(`/tmp/pipeline_stage1.log`):
|
||||
|
||||
```
|
||||
[sim_truth_perception-1] [ERROR] 仿真物体 'workpiece' 已经不在 scene.yaml 说的地方了 —— **停止发真值点**
|
||||
名义 (x,y,z)=(0.5200,0.0000,0.4000) yaw=0.0°
|
||||
实际 (x,y,z)=(0.4923,-0.0717,0.4000) yaw=8.3°
|
||||
偏差 |Δp|=76.9mm、姿态差 8.3°(容差 2.0mm / 2.0°)
|
||||
```
|
||||
|
||||
然后下一轮 `gauge_assembly` 报「一阶段 150s 没锁定:**一个视觉目标点都没收到**」,
|
||||
看起来像"真值节点没起来 / 场景名读不出来"。
|
||||
|
||||
**这条不是 bug,是两条正确行为叠在一起**:
|
||||
|
||||
1. 真值感知是**纯几何**的 —— 它按 `scene.yaml` 的名义位姿算目标点与孔位。
|
||||
物体一挪,算出来的就是假测量,而假测量**不会报错**:现象会是
|
||||
「S05 两孔间距 169mm(真值 58mm)」或「S07 一路下压不接触」,
|
||||
看起来像检测/标定/阈值坏了。所以 `sim_truth_perception` 有漂移看门狗。
|
||||
它**不是一条线,是两条**(2026-09-24 改,判据是纯函数 `pose_deviation()`,
|
||||
有单测;看门狗本身会自检"话题一帧都没收到"):
|
||||
* `pose_info.drift_tol_m`(2mm)→ **告警**,但**照发点**:真值几何已经跟着
|
||||
**实测**位姿算了(见下面 §5.1),2mm 只是"这场戏跟 scene.yaml 写的不一样了";
|
||||
* `pose_info.drift_stop_m`(20mm)→ **停发**:到这个量级说明工件掉了/被搬走了,
|
||||
场景前提没了,再发点等于指挥机械臂去装一个不存在的工件。
|
||||
2. 谁把工件推走的:上一轮 S07 判到接触的那一刻臂上顶着几十牛顿
|
||||
(位置控制的臂撞硬面**不会**在"刚碰到"停住),若卸载没生效,S08 再一转,
|
||||
工件就被推走/推歪了。所以第 1 条其实是在**替你报告真因**。
|
||||
|
||||
**修法**:把场景搬回去。`scripts/sim_load_gauge.py` 开头现在会复位
|
||||
`scene.yaml` 里**所有**物体(不止通止规)—— 所以正常情况下重跑一次就恢复正常,
|
||||
不必重启仿真。若复位失败(比如工件卡在爪里),看门狗会继续拒发点,
|
||||
那时**重启仿真**(`bash scripts/run_sim_gz.sh`)让它按场景重新 spawn。
|
||||
|
||||
排查顺序(按物理可信度):
|
||||
|
||||
```
|
||||
① 看 sim_truth_perception 的日志有没有"不在 scene.yaml 说的地方了"
|
||||
①a 偏差 ~2mm 级 → 只是告警,点照发,不用管(这是落地/蠕动的常态)
|
||||
①b 偏差 >20mm → 场景前提没了:复位(重跑 sim_load_gauge.py)或重启仿真;
|
||||
顺手查"谁推的它"(S07 的卸载有没有生效:日志里那行
|
||||
"接触后卸载:回退 X mm … 残余 Y N")
|
||||
② 没有 → 才轮到查"真值节点起没起来 / 场景名对不对"
|
||||
```
|
||||
|
||||
### 5.1 真值口径:**几何按实测位姿算**,名义位姿只用来报"物体被搬走了"
|
||||
|
||||
(2026-09-24 定;实现 `arm_sim_gazebo/sim_live_pose.py`,谁要谁订阅。)
|
||||
|
||||
真值路径最早是"按 `scene.yaml` 的名义位姿算目标点与孔位",也就是**假设物体没动过**。
|
||||
实测这个假设站不住,而且偏差是**安静**的:
|
||||
|
||||
| 现象 | 真因 |
|
||||
|------|------|
|
||||
| S05 算出的两孔间距 169mm(真值 58mm) | 工件掉进 90mm 通槽、落到地面(z≈0.6m),而真值仍按名义位姿把孔投到像素上 |
|
||||
| S07 一路下压不接触 | 孔位差 2~3mm ⇒ 规尖(φ6)对不上孔(φ7,单边 0.5mm 余量),压在工件面上而不是进孔 |
|
||||
|
||||
而 2~3mm 是**天天都有**的:物体是"在名义位姿上方 spawn、再落下"的,落地就偏
|
||||
2.6mm;工件骑在台面通槽上静置十几分钟还会再挪 1.9mm。
|
||||
|
||||
所以现在的口径是:
|
||||
|
||||
1. **算几何用实测位姿**(`/world/<world>/pose/info` → `LiveScenePose`)。
|
||||
真值感知的目标点、二阶段定位的孔位都走这一条。这也更接近真机 ——
|
||||
真机上的视觉也是"量到哪就是哪",不会拿图纸坐标去算。
|
||||
2. **名义位姿只做两件事**:判"物体还在不在该在的地方"(上面那两条线),
|
||||
和开局复位。
|
||||
3. **开局复位**(`pose_info.reset_on_start: true`):只在节点启动后 20s 窗口内、
|
||||
只做一次,把被盯的物体 set_pose 回名义位姿 —— 专门消掉 spawn 落地的
|
||||
2~3mm。运行中的漂移**必须报错**,不能悄悄搬回去(那会抹掉"谁把它撞歪了"
|
||||
的现场,还可能把工件从爪里拽走)。实现见
|
||||
`sim_truth_perception._maybe_startup_reset` 与 `sim_pose_service.set_model_pose_yaw`。
|
||||
|
||||
```
|
||||
⚠️ 复位/搬运用的是 Gazebo 的 `set_pose` 服务,命令行调用时 `--reqtype` 必须写
|
||||
`ignition.msgs.Pose`(写 `gz.msgs.Pose` 会报 "Unable to create request",且不说是哪错了)。
|
||||
实现只有一份:`arm_sim_gazebo/sim_pose_service.py`。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 现状与进度
|
||||
|
||||
### 已就绪(端到端验证过)
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# =============================================================================
|
||||
# devices/sim/gazebo/config/grasp_lock.yaml
|
||||
#
|
||||
# 仿真"抓取锁"(grasp lock)的口径。**只对仿真有意义**,真机没有任何对应物 ——
|
||||
# 真机靠夹爪的摩擦夹持,本来就不会掉。
|
||||
#
|
||||
# -----------------------------------------------------------------------------
|
||||
# 它解决什么问题(2026-09-24 一整轮排查换来的)
|
||||
# -----------------------------------------------------------------------------
|
||||
# 仿真里的夹持力不是"设"出来的,而是**位置误差 × 接触刚度**(见 gauge/model.sdf
|
||||
# 里 kp 那段)。规站着不动时够用;机械臂一动就不行:
|
||||
#
|
||||
# S01 绕工具轴快转 150°、pre_lift/粗定位是 60~100mm 的连续轨迹 ——
|
||||
# 这两步里规在指间打滑。实测工具长度 0.243m → 0.285m、规歪 62°,
|
||||
# S07 随后拿到的几何全错,现象是"S07 判不到接触"或"规掉了(E1)"。
|
||||
#
|
||||
# 真机不会这样:握力 46N、µ≈0.8、规 0.265kg —— 摩擦余量两个数量级。
|
||||
# 所以这把锁补的是 **Gazebo 缺的那块物理**(刚性接触没有静摩擦迟滞、也不模拟
|
||||
# 指垫橡胶的贴合),不是绕过物理:规与指垫的接触对仍在,触觉与腕部 FT 的读数
|
||||
# 依旧是真实接触力。
|
||||
#
|
||||
# -----------------------------------------------------------------------------
|
||||
# ⚠️ 一处对不上就**静默失效**(不报错,只是规不跟着走)
|
||||
# -----------------------------------------------------------------------------
|
||||
# 本文件的名字必须与下面三处一致:
|
||||
# ① gauge/model.sdf 里 DetachableJoint 插件的 <child_model>/<child_link>
|
||||
# ② gauge/model.sdf 里插件的 <attach_topic>/<detach_topic>
|
||||
# ③ 真名(Gazebo 里实际存在的连杆名)—— URDF 里与固定关节相连的连杆会被
|
||||
# sdformat **合并(lump)**,名字会变:手指只剩 sim_left_finger_mount。
|
||||
# 查真名的办法:
|
||||
# ros2 topic echo /sim/world_pose_info ← 看全部 child_frame_id
|
||||
# gazebo_sim.launch.py 启动时会读 gauge 的 SDF 核对 ① ②,不一致就直接报出来。
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# 总开关。关掉它仿真照样跑,只是回到"靠摩擦夹持",也就是上面那串毛病。
|
||||
enabled: true
|
||||
|
||||
# 被夹住的物体(scene.yaml 里的物体名 = Gazebo 里的模型名)。
|
||||
object: "gauge"
|
||||
# 该模型里被夹的那根连杆。
|
||||
object_link: "body"
|
||||
|
||||
# 机器人侧:模型名 = launch 里 spawn 的 -name;连杆名必须是 Gazebo 的**真名**。
|
||||
#
|
||||
# ⚠️ 必须锁到**不会跟着张爪动的**连杆上 —— 默认是 `sim_link7`(法兰)。
|
||||
# 为什么(2026-09-24 实测,一条假 E1 的真因):
|
||||
# 起初锁到 `sim_left_finger_mount`(左手指)。夹住的规被**刚性**锁在这根
|
||||
# 手指上,而 S08 棘轮每轮都要"松爪 → 回转 → 夹紧":张爪时手指往外走
|
||||
# 40mm,被锁住的规却还插在孔里(6mm 销 / 7mm 方孔)——两处约束打架,
|
||||
# 物理解算把规**从孔里拽出来**(实测规尖跳 +17.5mm、偏摆 64°),
|
||||
# 然后在回转那一步掉到地上。日志只看得到"通止规掉了(E1)"。
|
||||
# `sim_grav_base_link`(夹爪本体)在 sdformat 里被**合并**进了 `sim_link7`
|
||||
# (见 /sim/world_pose_info 里的 `sim_link7_fixed_joint_lump__...`),
|
||||
# 所以锚点就写 `sim_link7`:张爪不影响它,松爪/夹紧都不会带偏规。
|
||||
# `sim_grasp_lock.load_grasp_lock()` 会拒绝含 `finger` 的连杆名(带这段原因)。
|
||||
robot_model: "rizon_sim"
|
||||
robot_link: "sim_link7"
|
||||
|
||||
# 触发话题(gz 侧,由本模块用 ign topic 发一个空消息触发)。
|
||||
# 名字必须与 gauge/model.sdf 里插件的 attach_topic/detach_topic 一致。
|
||||
attach_topic: "/sim/grasp_lock/attach"
|
||||
detach_topic: "/sim/grasp_lock/detach"
|
||||
# 插件状态回执(会 attach/detach 成功时各发一条)。用它来判断"锁到底有没有生效",
|
||||
# 而不是靠"看规掉没掉"——后者要等故障发生才知道。
|
||||
state_topic: "/sim/grasp_lock/state"
|
||||
|
||||
# 发一次触发消息后等多久 [ms](ign topic 是短命进程,给足启动时间)。
|
||||
publish_timeout_ms: 3000
|
||||
@@ -99,6 +99,58 @@ gates_px:
|
||||
insert_ring_radius: [6, 30]
|
||||
hole_spacing: [90, 700]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 世界模型位姿回读 —— 仿真里唯一能发现"物体被撞走/掉下去了"的手段
|
||||
# ---------------------------------------------------------------------------
|
||||
# 为什么需要它(2026-09-24 花了一整轮排查换来的,值得完整读一遍):
|
||||
# `perception.source=truth` 是**纯几何**的:它按下面 objects 里的**名义位姿**
|
||||
# 把孔投到像素上,**从不问 Gazebo 物体现在到底在哪**。
|
||||
# 于是工件一旦被碰走,"真值"就开始撒谎,而且每一处现象都指向别的原因:
|
||||
#
|
||||
# 实测:工件掉进台面通槽、落到地面(z≈0),而 scene.yaml 仍写着 z=0.40。
|
||||
# S05 拿"名义孔位"的像素去查**真实**深度图 → 孔位处看到的是槽底/地面
|
||||
# (0.6m,而工件面其实只有 0.21m)→ 三角化出 k1/k2 间距 169mm
|
||||
# (真值 58mm)→ 被间距门槛丢掉 → 精拍连丢 9 拍 → S05 超时 ABORT。
|
||||
# 现场现象像"检测坏了 / 标定坏了 / 阈值调高了",全都不是。
|
||||
# 真正的信息只有一句:"**工件现在不在 scene.yaml 说的位置**。"
|
||||
#
|
||||
# Gazebo 自己会把每个模型的实际位姿发在 /world/<world>/pose/info
|
||||
# (gz.msgs.Pose_V),桥成 tf2_msgs/TFMessage 之后,漂移监视器
|
||||
# (sim_truth_perception)就能把"名义位姿 vs 实际位姿"直接对比出来并**指名报错**。
|
||||
#
|
||||
# 话题名里的 world 名必须与 worlds/assembly.world 的文件名一致(`assembly`)。
|
||||
pose_info:
|
||||
gz_topic: "/world/assembly/pose/info"
|
||||
ros_topic: "/sim/world_pose_info"
|
||||
# 类型固定这两条:ros_gz_bridge 原生支持 Pose_V ↔ TFMessage(实测确认,
|
||||
# 见 ros_gz_sim_demos 的 tf_bridge.launch.py)。写错类型不会有任何报错,
|
||||
# 只会"桥接器起来了但一个消息都没有" —— 所以别改。
|
||||
ros_type: "tf2_msgs/msg/TFMessage"
|
||||
gz_type: "gz.msgs.Pose_V"
|
||||
# 漂移判据:**两个**阈值,别合成一个(2026-09-24 的教训)。
|
||||
#
|
||||
# drift_tol_m(2mm)= **告警**线:真值几何已经跟着**实测**位姿走
|
||||
# (见 arm_sim_gazebo/sim_live_pose.py),所以 2mm 不再意味着"几何错了",
|
||||
# 只意味着"这个场景跟 scene.yaml 写的不一样了"。而 2~3mm 是**天天都有**的:
|
||||
# spawn 落地就偏 2.6mm,工件骑在通槽上静置十几分钟还会再挪 1.9mm。
|
||||
# 2mm 的来历:真值投影在相机到工件 0.17m 处,fx≈447 ⇒ 1mm 位姿误差 ≈ 2.6px,
|
||||
# 而 S05 的稳定判据是 2px —— 2mm 是"开始影响二阶段"的量级。
|
||||
# yaw 取 2°:两孔间距 58mm,绕中心转 2° 会让孔横移 1mm(约 2.6px),同量级。
|
||||
drift_tol_m: 0.002
|
||||
drift_tol_deg: 2.0
|
||||
# drift_stop_m 才是**停发**线:到这个量级说明工件掉了/被搬走了,场景前提没了
|
||||
# (再发点等于指挥机械臂去装一个不存在的工件)。20mm 的来历:工件一半宽 70mm、
|
||||
# 掉下台面是 400mm 量级,而"顺手碰一下"是几毫米 —— 20mm 两边都分得开。
|
||||
drift_stop_m: 0.02
|
||||
# 启动复位:开局把被盯的物体 set_pose 回名义位姿(仿真专有,默认关,这里开)。
|
||||
# 为什么还要它:上面两条线是"容差",这一条是**消除源头** ——
|
||||
# 物体是"在名义位姿上方 spawn、再落下来"的,落地那 2.6mm 偏差纯属 spawn
|
||||
# 的落点问题(实测 set_pose 回名义位姿后它就停在那儿不动)。
|
||||
# 只在开局 20s 窗口内、且只做一次 —— 运行中的漂移必须报错,不能悄悄搬回去
|
||||
# (那会抹掉"谁把它撞歪了"的现场,还可能把工件从爪里拽走)。
|
||||
# 实现见 sim_truth_perception._maybe_startup_reset(含完整理由)。
|
||||
reset_on_start: true
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 场景物体
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# ⚠ 本文件由 scripts/sim_load_gauge.py 自动生成(每次仿真上料都覆盖)。
|
||||
# 它是**这次装夹**实测到的工具几何,run_gauge_assembly.sh --sim 会读它
|
||||
# 并作为启动参数覆盖 site.yaml 的 gauge_assembly.tool_tip_below_flange_m。
|
||||
# 为什么必须这样:仿真里规会在指间下滑到落座位置,下滑量随装夹变化
|
||||
# (实测见过 243mm / 254mm 两种)。写成固定值迟早会算错悬停高度。
|
||||
# 真机没有这个文件(现场装夹后把实测值写进 site.yaml)。
|
||||
flange_z_m: 0.763
|
||||
measured_at: '2026-09-24T19:40:08'
|
||||
model: gauge
|
||||
tip_z_m: 0.5198
|
||||
tool_tip_below_flange_m: 0.2432
|
||||
world: assembly
|
||||
@@ -0,0 +1,50 @@
|
||||
# =============================================================================
|
||||
# devices/sim/gazebo/config/wrist_ft.yaml
|
||||
#
|
||||
# 仿真腕部力扭矩传感器(wrist force/torque)的话题与口径。
|
||||
# sim_wrist_ft.py、gazebo_sim.launch.py 从这里读;改这里不用重编译。
|
||||
#
|
||||
# 真机的对应物:Flexiv 的 flexiv_robot_states_broadcaster 自动发
|
||||
# /<sn>/ft_sensor_wrench geometry_msgs/msg/WrenchStamped(法兰系)
|
||||
# 不需要任何配置 —— 它在真机 bringup 里无条件启动。
|
||||
#
|
||||
# -----------------------------------------------------------------------------
|
||||
# 为什么仿真与真机能共用一套判据(这与触觉**相反**)
|
||||
# -----------------------------------------------------------------------------
|
||||
# 手指触觉(tactile.yaml)两边单位不同,所以阈值必须各配一套;腕部 FT 不同:
|
||||
#
|
||||
# 真机 ft_sensor_wrench 单位就是牛顿,坐标系是法兰系。
|
||||
# 仿真 force_torque 传感器单位也是牛顿,挂 joint7 取 child(link7) 系,
|
||||
# 而 link7 的 z 轴 = 工具轴(工具竖直朝下时即竖直方向)。
|
||||
#
|
||||
# 于是两边的"z 分量"都是**轴向力,单位牛顿** —— 公式一致,只有具体阈值
|
||||
# 可以按现场标定分开配(见 site.yaml 的 gauge_assembly.contact_axial_delta)。
|
||||
#
|
||||
# ⚠️ 与触觉一样,这也是一处"选错不报错"的口径:口径错了只会表现为
|
||||
# "判据不触发"或"一碰就炸",看起来像算法问题。
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# gz 侧话题名。**必须与应用层按 sn 推出来的名字一致**:
|
||||
# 应用层算 f"/{sn}/ft_sensor_wrench",仿真臂 sn = "sim"
|
||||
# ⇒ /sim/ft_sensor_wrench
|
||||
# 改 sn(config/site.yaml 的 robot.arms[sim])时必须同步改这里。
|
||||
# 也同时写死在 arm.sim.sensors.xacro 的 wrist_ft_topic 默认值里 —— 两处
|
||||
# 必须一致,check_sim_wrist_ft.py 会核对 xacro 展开结果里有没有这个话题。
|
||||
topic: "/sim/ft_sensor_wrench"
|
||||
|
||||
# 挂载关节。改这里必须同步改 arm.sim.sensors.xacro 里的 <gazebo reference>。
|
||||
# 为什么是 joint7 见 xacro 里那段注释(不能挂 fixed 关节:物理引擎不把它当
|
||||
# 独立关节求解,传感器永远没输出且不报错)。
|
||||
joint: "sim_joint7"
|
||||
|
||||
# 期望消息类型(ROS 侧)。gz.msgs.Wrench → WrenchStamped 由 ros_gz_bridge
|
||||
# 直接支持(实测确认,无需中间适配器节点)。
|
||||
#
|
||||
# 为什么是 WrenchStamped 而不是 Wrench:真机发的就是 WrenchStamped。
|
||||
# 类型一致,应用层才能不加分支地两边通用。
|
||||
ros_type: "geometry_msgs/msg/WrenchStamped"
|
||||
gz_type: "gz.msgs.Wrench"
|
||||
|
||||
# 传感器输出率 [Hz]。与真机 broadcaster 对齐(100Hz)。
|
||||
# 下游按"数据够不够新"判活,率太低会被误判成掉线。
|
||||
publish_hz: 100.0
|
||||
@@ -118,13 +118,24 @@
|
||||
</collision>
|
||||
|
||||
<!-- 夹持段:夹爪夹这里,同样用软接触。
|
||||
摩擦给 0.4:真机夹爪指垫是橡胶/聚氨酯,比金属-金属的 0.25 高。
|
||||
给太低(<0.2)规会在指间打滑自转,而现象看起来像"夹爪没夹紧"。 -->
|
||||
摩擦给 0.8:真机夹爪指垫是橡胶/聚氨酯,对钢件的 µ 在 0.6~1.0 之间。
|
||||
这个数**直接决定"能不能把轴向力传给手指"**:能传的上限 = µ·N
|
||||
(单指),握力 N≈46N → 0.8 时两指合计 ≈74N,0.4 时只有 ≈37N。
|
||||
|
||||
为什么原来是 0.4、为什么必须改(2026-09-24):
|
||||
台肩一落面,接触刚度 5e4 N/m 意味着**过压 0.5mm 就是 25N**。µ=0.4
|
||||
时两指上限 ≈37N,看着够;但一步步进就是 0.5mm,下一步就 50N > 37N
|
||||
→ 规在爪里打滑,力就再也传不到指垫触觉上(读数停在 0,而它是对的)。
|
||||
把 µ 改成接近真实的 0.8,连同"改用腕部 FT 当判据",两条一起才成立:
|
||||
- 腕部 FT 在摩擦上游,打滑也测得准(首选判据);
|
||||
- 摩擦接近真实值,才不会出现"规被顶得往上溜 20mm 顶到掌心"的
|
||||
仿真特有现象(那会让 S07 在 25mm 处顶死、报假原因)。
|
||||
给太低(<0.2)还会让规在指间打滑自转,现象看起来像"夹爪没夹紧"。 -->
|
||||
<collision name="grip">
|
||||
<pose>0 0 0.0555 0 0 0</pose>
|
||||
<geometry><cylinder><radius>0.015</radius><length>0.045</length></cylinder></geometry>
|
||||
<surface>
|
||||
<friction><ode><mu>0.4</mu><mu2>0.4</mu2></ode></friction>
|
||||
<friction><ode><mu>0.8</mu><mu2>0.8</mu2></ode></friction>
|
||||
<contact>
|
||||
<ode><kp>5e4</kp><kd>5e2</kd><max_vel>0.01</max_vel><min_depth>0.0005</min_depth></ode>
|
||||
</contact>
|
||||
@@ -189,5 +200,53 @@
|
||||
</visual>
|
||||
|
||||
</link>
|
||||
|
||||
<!-- =====================================================================
|
||||
抓取锁 GRASP LOCK —— 只存在于仿真,真机不需要
|
||||
=====================================================================
|
||||
做什么:夹爪一旦确认夹住,就用一个**固定关节**把规锁在手指上;
|
||||
夹爪一张开就解锁。规与指垫之间的接触对仍然存在,
|
||||
所以触觉/腕部 FT 的读数照旧是真实接触力。
|
||||
|
||||
为什么必须有它(2026-09-24 用一整轮排查换来的):
|
||||
仿真里的夹持力不是"设"出来的,而是**位置误差 × 接触刚度**
|
||||
(见上面 kp 那段说明)。规站着不动时够用,可机械臂一动就不行:
|
||||
· S01 要绕工具轴快转 150°;
|
||||
· pre_lift / 粗定位是一次 60~100mm 的连续轨迹;
|
||||
这两步里规在指间**打滑**(实测工具长度 0.243m → 0.285m,规歪了 62°),
|
||||
于是 S07 拿到的几何全是错的 —— 现象是"S07 判不到接触"或"规掉了(E1)",
|
||||
而日志里看不到真因。
|
||||
真机不会这样:握力 46N、µ≈0.8、规 0.27kg,摩擦余量两个数量级。
|
||||
|
||||
所以这把锁补的是**Gazebo 缺的那一小块物理**,不是绕过物理:
|
||||
刚性接触求解器给不出静摩擦迟滞,也不会模拟指垫橡胶的贴合,
|
||||
而现实的夹持恰恰靠这两件事。补上之后,仿真才配得上"验证装配逻辑"。
|
||||
|
||||
⚠️ 四处必须对上,否则它**静默失效**(不报错,只是规不跟着走):
|
||||
1. <parent_link> = 本模型(规)里要被抓的那根连杆:body;
|
||||
2. <child_model> = 机器人 spawn 时的 -name(launch 里是 rizon_sim);
|
||||
3. <child_link> = Gazebo 里**真实存在**的连杆名,且必须是**不跟着
|
||||
张爪动**的那一根 —— 这里填的是 `sim_link7`(法兰;夹爪本体
|
||||
`sim_grav_base_link` 在 sdformat 里被合并进去了,见
|
||||
`/sim/world_pose_info` 里的 `sim_link7_fixed_joint_lump__...`)。
|
||||
⚠️ 2026-09-24 实测:填成 `sim_left_finger_mount`(手指)会出事 ——
|
||||
S08 棘轮每轮"松爪 → 回转 → 夹紧",张爪时手指外移 40mm 而规被
|
||||
刚性锁着、又插在孔里,两处约束打架把规**从孔里拽出来**
|
||||
(规尖跳 +17.5mm、偏摆 64°)→ 下一步掉地 → 只报"通止规掉了(E1)"。
|
||||
真名怎么查:
|
||||
ros2 topic echo /sim/world_pose_info (列全部 child_frame_id)
|
||||
4. attach/detach 话题与 sim_gripper_adapter 下发的名字一致;
|
||||
两侧名字对不上时,锁只会"永远不触发"。
|
||||
===================================================================== -->
|
||||
<plugin filename="ignition-gazebo-detachable-joint-system"
|
||||
name="ignition::gazebo::systems::DetachableJoint">
|
||||
<parent_link>body</parent_link>
|
||||
<child_model>rizon_sim</child_model>
|
||||
<child_link>sim_link7</child_link>
|
||||
<attach_topic>/sim/grasp_lock/attach</attach_topic>
|
||||
<detach_topic>/sim/grasp_lock/detach</detach_topic>
|
||||
<output_topic>/sim/grasp_lock/state</output_topic>
|
||||
</plugin>
|
||||
|
||||
</model>
|
||||
</sdf>
|
||||
|
||||
@@ -41,6 +41,8 @@
|
||||
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
|
||||
@@ -313,6 +315,56 @@
|
||||
</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>
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
<link name="top">
|
||||
<pose>0 0 0.375 0 0 0</pose>
|
||||
<!-- ===================================================================
|
||||
台面沿 Y 留一道 90mm 宽的通槽(|y| ≤ 45mm),台面分成南北两块。
|
||||
台面沿 X 留一道 36mm 宽的通槽(|y| ≤ 18mm),台面分成南北两块。
|
||||
|
||||
为什么必须留这道槽(实测踩过,现象非常隐蔽):
|
||||
通止规的**通端**长 30mm,而工件是 12mm 厚的板、板上的孔是
|
||||
@@ -129,15 +129,34 @@
|
||||
力也不是完全没有(顶台面时切向读 30~45N),所以看起来像
|
||||
"阈值调太高",很容易被误判成传感器/阈值问题。
|
||||
|
||||
槽只开在工件孔位所在的 Y 带(孔在 y = ±29mm、φ7 见方),
|
||||
不抬高工件、不动台面高度 → 相机距离、孔检测像素门槛、
|
||||
site.yaml 里的悬停高度全都不用改。
|
||||
南北两块各留 y ∈ [±45, ±600]mm,工件(100mm 宽)跨在槽上,
|
||||
两侧各有 5mm 承压面 —— 板是刚体、只受重力,够用。
|
||||
===================================================================
|
||||
槽宽:36mm(|y| ≤ 18mm),**不是**初版的 90mm(2026-09-24 收窄,
|
||||
代价是一整轮排查)
|
||||
===================================================================
|
||||
初版开的是 90mm 宽(|y| ≤ 45mm),理由是"工件上两个孔所在的 Y 带
|
||||
都盖住"。但工件只有 100mm 宽(y 方向),跨在 90mm 的槽上时
|
||||
**每侧只剩 5mm 承压面** —— 静力学上勉强平衡,一有横向扰动就翻。
|
||||
实际后果(本次先看到的现象):
|
||||
工件掉进槽里、落到地面(z≈0),而 `perception.source=truth`
|
||||
仍然按 scene.yaml 里的**名义位姿**把孔投到像素上 ——
|
||||
于是 S05 拿"名义孔位"的像素去查**真实**深度图:孔位处看到的是
|
||||
槽底/地面(实测 0.6m,而工件面其实只有 0.21m),
|
||||
三角化出来的 k1/k2 间距 169mm(真值 58mm),被间距门槛丢掉,
|
||||
精拍连丢 9 拍 → S05 超时 ABORT。
|
||||
现象像"检测/阈值坏了",根因却是**工件不在它该在的地方**。
|
||||
|
||||
18mm 是怎么定的(只跟"规的通端要能过"有关,与两孔间距无关):
|
||||
通端 φ6(半径 3mm),孔 φ7(半径 3.5mm)→ 规占 |y| ≤ 3mm;
|
||||
留 15mm 给定位误差(S05 对准 tol 0.7mm、工件摆放偏差、yaw 残余)。
|
||||
承压面因此从 5mm 变成 32mm(y ∈ [±18, ±50]mm),工件不再翻得进去。
|
||||
注意**方向**:两孔连线沿工件局部 X(模型里 insert_*_ring 在 x=±29mm),
|
||||
场景里 yaw=0 ⇒ 两孔世界 y 都是 0。所以槽只要在 y=0 附近开一条
|
||||
沿 X 的长槽就够,不需要覆盖整个 ±29mm。
|
||||
`scripts/check_sim_scene.py` 会核对"孔位落在槽的开口范围内"这一条。
|
||||
=================================================================== -->
|
||||
<collision name="collision_south">
|
||||
<pose>0 -0.3225 0 0 0 0</pose>
|
||||
<geometry><box><size>0.8 0.555 0.05</size></box></geometry>
|
||||
<pose>0 -0.309 0 0 0 0</pose>
|
||||
<geometry><box><size>0.8 0.582 0.05</size></box></geometry>
|
||||
<surface>
|
||||
<friction>
|
||||
<ode><mu>0.8</mu><mu2>0.8</mu2></ode>
|
||||
@@ -153,8 +172,8 @@
|
||||
</surface>
|
||||
</collision>
|
||||
<collision name="collision_north">
|
||||
<pose>0 0.3225 0 0 0 0</pose>
|
||||
<geometry><box><size>0.8 0.555 0.05</size></box></geometry>
|
||||
<pose>0 0.309 0 0 0 0</pose>
|
||||
<geometry><box><size>0.8 0.582 0.05</size></box></geometry>
|
||||
<surface>
|
||||
<friction>
|
||||
<ode><mu>0.8</mu><mu2>0.8</mu2></ode>
|
||||
@@ -170,16 +189,16 @@
|
||||
</surface>
|
||||
</collision>
|
||||
<visual name="visual_south">
|
||||
<pose>0 -0.3225 0 0 0 0</pose>
|
||||
<geometry><box><size>0.8 0.555 0.05</size></box></geometry>
|
||||
<pose>0 -0.309 0 0 0 0</pose>
|
||||
<geometry><box><size>0.8 0.582 0.05</size></box></geometry>
|
||||
<material>
|
||||
<ambient>0.45 0.4 0.35 1</ambient>
|
||||
<diffuse>0.55 0.5 0.42 1</diffuse>
|
||||
</material>
|
||||
</visual>
|
||||
<visual name="visual_north">
|
||||
<pose>0 0.3225 0 0 0 0</pose>
|
||||
<geometry><box><size>0.8 0.555 0.05</size></box></geometry>
|
||||
<pose>0 0.309 0 0 0 0</pose>
|
||||
<geometry><box><size>0.8 0.582 0.05</size></box></geometry>
|
||||
<material>
|
||||
<ambient>0.45 0.4 0.35 1</ambient>
|
||||
<diffuse>0.55 0.5 0.42 1</diffuse>
|
||||
|
||||
Reference in New Issue
Block a user