真机落地前的仿真补齐:夹持/腕力/真值位姿,让仿真能验证 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:
+140
-2
@@ -329,6 +329,134 @@ def check_d435_visibility(scene, root) -> tuple[list[str], str]:
|
||||
return probs, info
|
||||
|
||||
|
||||
def check_table_slot(scene, root) -> tuple[list[str], str]:
|
||||
"""台面通槽 vs 工件:既要"规的通端过得去",又要"工件掉不下去"。
|
||||
|
||||
两个方向都会出事,而且都是**静默**的(2026-09-24 各栽过一次):
|
||||
|
||||
a) 槽开得不够 **窄**(< 规的通端):规下压到 12mm 就顶在台面上,
|
||||
S07 一路压到上限都等不到台肩落面 → 报"没触发接触判据",
|
||||
看起来像传感器/阈值坏了。
|
||||
b) 槽开得**太宽**:工件跨在槽上时承压面太小,一有扰动就翻进槽里、
|
||||
落到地面 —— 于是 `perception.source=truth` 开始撒谎:孔位按名义位姿
|
||||
投到像素上,而孔位正下方已经不是工件(是槽底/地面),
|
||||
深度反投影把 58mm 孔距算成 169mm,S05 连丢 9 拍超时。
|
||||
|
||||
所以这里同时查两件事:孔位必须落在**开口**内,承压宽度必须**够**。
|
||||
"""
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from arm_sim_gazebo.sim_scene import rings_local, world_boxes
|
||||
from arm_sim_gazebo.sim_site import resolve_world
|
||||
|
||||
world_path, _ = resolve_world("", root)
|
||||
try:
|
||||
tree = ET.parse(world_path)
|
||||
except (OSError, ET.ParseError) as exc:
|
||||
return [f"读不出 world({world_path}):{exc}"], ""
|
||||
table = None
|
||||
for m in tree.getroot().iter("model"):
|
||||
if m.get("name") == "work_table":
|
||||
table = m
|
||||
break
|
||||
if table is None:
|
||||
return [f"{world_path} 里没有 <model name=\"work_table\"> —— 台面被改名了?"], ""
|
||||
|
||||
def _pose_xyz(node) -> tuple[float, float, float]:
|
||||
p = node.find("pose")
|
||||
if p is None or not (p.text or "").strip():
|
||||
return (0.0, 0.0, 0.0)
|
||||
v = [float(x) for x in p.text.split()[:3]]
|
||||
while len(v) < 3:
|
||||
v.append(0.0)
|
||||
return (v[0], v[1], v[2])
|
||||
|
||||
my = _pose_xyz(table)[1]
|
||||
spans: list[tuple[float, float]] = []
|
||||
for link in table.findall("link"):
|
||||
ly = my + _pose_xyz(link)[1]
|
||||
for col in link.findall("collision"):
|
||||
box = col.find("geometry/box/size")
|
||||
if box is None or not (box.text or "").strip():
|
||||
continue
|
||||
cy = ly + _pose_xyz(col)[1]
|
||||
half = float(box.text.split()[1]) / 2.0
|
||||
spans.append((cy - half, cy + half))
|
||||
if not spans:
|
||||
return ["work_table 里读不到任何 box 碰撞体 —— 台面几何解析不出来"], ""
|
||||
spans.sort()
|
||||
|
||||
b = scene.table_bounds
|
||||
# 开口 = 台面范围内的"没被碰撞体盖住"的 y 区间。
|
||||
gaps: list[tuple[float, float]] = []
|
||||
cur = b.y_min
|
||||
for lo, hi in spans:
|
||||
if lo > cur + 1e-9:
|
||||
gaps.append((cur, min(lo, b.y_max)))
|
||||
cur = max(cur, hi)
|
||||
if cur < b.y_max - 1e-9:
|
||||
gaps.append((cur, b.y_max))
|
||||
gaps = [(lo, hi) for lo, hi in gaps if hi - lo > 1e-6]
|
||||
|
||||
part = next((o for o in scene.objects if o.name == scene.operating.part_object), None)
|
||||
if part is None:
|
||||
return [f"场景里没有 {scene.operating.part_object!r}"], ""
|
||||
boxes = world_boxes(scene, part, root)
|
||||
if not boxes:
|
||||
return ["读不出工件的碰撞体"], ""
|
||||
wy0 = min(float(v) for bx in boxes for v in (bx[2], bx[3]))
|
||||
wy1 = max(float(v) for bx in boxes for v in (bx[2], bx[3]))
|
||||
|
||||
# 承压宽度 = 工件在 y 方向上被台面碰撞体盖住的长度(区间求交后累加)。
|
||||
supported = 0.0
|
||||
for lo, hi in spans:
|
||||
supported += max(0.0, min(hi, wy1) - max(lo, wy0))
|
||||
|
||||
rings = rings_local(scene, part, root)
|
||||
hole_ys: list[float] = []
|
||||
for r in rings:
|
||||
c, s = math.cos(float(part.yaw)), math.sin(float(part.yaw))
|
||||
hole_ys.append(float(part.y) + s * float(r.x) + c * float(r.y))
|
||||
|
||||
probs: list[str] = []
|
||||
info = (
|
||||
f"台面沿 y 的支撑区间 {[(round(lo, 4), round(hi, 4)) for lo, hi in spans]};"
|
||||
f"开口 {[(round(lo, 4), round(hi, 4)) for lo, hi in gaps]};"
|
||||
f"工件 y∈[{wy0:.4f},{wy1:.4f}] 承压 {supported * 1000:.1f}mm"
|
||||
)
|
||||
|
||||
# (a) 孔位必须落在开口里 —— 否则规的通端顶在台面上,S07 永远等不到台肩落面。
|
||||
for i, hy in enumerate(hole_ys):
|
||||
ok = any(lo < hy < hi for lo, hi in gaps)
|
||||
if not ok:
|
||||
probs.append(
|
||||
f"第 {i + 1} 个孔(世界 y={hy:+.4f})**不在台面开口内** —— "
|
||||
f"规的通端会顶在台面上。\n"
|
||||
f" 后果:S07 下压 12mm 就到底,一路到上限都等不到台肩落面,"
|
||||
f"报「没触发接触判据」,看起来像传感器/阈值坏了。\n"
|
||||
f" 修法:把开口挪到孔位下方(worlds/assembly.world 的 work_table "
|
||||
f"碰撞体 y 区间),或改工件摆放。\n"
|
||||
f" 当前开口:{[(round(lo, 4), round(hi, 4)) for lo, hi in gaps]}"
|
||||
)
|
||||
|
||||
# (b) 承压宽度必须够 —— 否则工件会翻进槽里掉下去,而真值感知会继续按
|
||||
# 名义位姿算,把 58mm 孔距算成 169mm(见 truth_holes.HoleTruth 的说明)。
|
||||
min_support = 0.020
|
||||
if supported < min_support:
|
||||
probs.append(
|
||||
f"工件跨在台面开口上的**承压宽度只有 {supported * 1000:.1f}mm**"
|
||||
f"(要求 ≥ {min_support * 1000:.0f}mm)。\n"
|
||||
f" 后果:板一有扰动就翻进槽里、落到地面;而 "
|
||||
f"perception.source=truth 仍按 scene.yaml 的**名义位姿**投孔 → "
|
||||
f"孔位下方已经不是工件而是槽底/地面 →\n"
|
||||
f" 深度反投影把 58mm 孔距算成 169mm → S05 每拍都丢、最后超时。"
|
||||
f"现象看起来是「检测坏了」,根因却是**工件掉了**。\n"
|
||||
f" 修法:把开口收窄(改 worlds/assembly.world 里 work_table 碰撞体的 "
|
||||
f"y 尺寸/位姿,开口只要够规的通端过就行,不需要覆盖两个孔所在的整个 y 带)。"
|
||||
)
|
||||
return probs, info
|
||||
|
||||
|
||||
def check_gazebo_poses(scene, root) -> tuple[list[str], str]:
|
||||
"""查 Gazebo 里物体的实际位姿,与 scene.yaml 对照。
|
||||
|
||||
@@ -441,7 +569,17 @@ def main() -> int:
|
||||
if p:
|
||||
print("\n".join(f" ✘ {x}" for x in p))
|
||||
|
||||
print("\n[5] 粗定位可见性(工件是否完整落在手外相机画面内)")
|
||||
print("\n[5] 台面通槽 vs 工件(规过得去 + 工件掉不下去)")
|
||||
p, info = check_table_slot(scene, root)
|
||||
problems += p
|
||||
if info:
|
||||
print(f" {info}")
|
||||
if p:
|
||||
print("\n".join(f" ✘ {x}" for x in p))
|
||||
else:
|
||||
print(" ✓ 孔位都在开口内,且承压宽度够")
|
||||
|
||||
print("\n[6] 粗定位可见性(工件是否完整落在手外相机画面内)")
|
||||
p, info = check_d435_visibility(scene, root)
|
||||
problems += p
|
||||
if info:
|
||||
@@ -452,7 +590,7 @@ def main() -> int:
|
||||
print(" ✓ 完整可见")
|
||||
|
||||
if not args.no_poses:
|
||||
print("\n[6] Gazebo 实际位姿核对")
|
||||
print("\n[7] Gazebo 实际位姿核对")
|
||||
p, info = check_gazebo_poses(scene, root)
|
||||
problems += p
|
||||
if info:
|
||||
|
||||
Reference in New Issue
Block a user