0b727785c6
Includes G20 PLAN/cmd_u8 bridge, joint mapping, dual-hand sim scripts, and G20/L20/O6 URDF with USD payloads for Isaac Sim integration. Co-authored-by: Cursor <cursoragent@cursor.com>
1163 lines
44 KiB
Python
1163 lines
44 KiB
Python
#!/usr/bin/env python3
|
||
# SPDX-License-Identifier: Apache-2.0
|
||
"""在 Isaac Sim 中加载 LinkerHand G20 灵巧手,并通过 ROS2 话题控制。
|
||
|
||
默认仅加载 G20(``linkerhand_g20_left`` USD)。
|
||
默认 PD 动力学,以体现交付 USD 中的自碰/掌心接触垫;
|
||
``--position-drive`` 硬写关节(UI 跟手,但会穿模);
|
||
``--hand-gravity`` 开手部重力。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
LINKERHAND_ROOT = Path(__file__).resolve().parent
|
||
URDF_ROOT = LINKERHAND_ROOT / "urdf"
|
||
|
||
DEFAULT_ASSETS = {
|
||
("O6", "left"): URDF_ROOT / "o6/left/linkerhand_o6_left/linkerhand_o6_left.usda",
|
||
("G20", "left"): URDF_ROOT / "g20/left/linkerhand_g20_left/linkerhand_g20_left.usda",
|
||
}
|
||
|
||
TOPICS = {
|
||
"O6": {
|
||
"cmd": "/o6/cb_left_hand_control_cmd",
|
||
"state": "/o6/cb_left_hand_state",
|
||
},
|
||
"G20": {
|
||
"cmd": "/cb_left_hand_control_cmd",
|
||
"state": "/cb_left_hand_state",
|
||
},
|
||
}
|
||
|
||
# 默认 PD:交付 USD 的自碰/掌心垫才会生效。硬写位置用 --position-drive(可穿模)。
|
||
DEFAULT_DRIVE_STIFFNESS = 80.0
|
||
DEFAULT_DRIVE_DAMPING = 25.0
|
||
G20_DRIVE_STIFFNESS = 25.0
|
||
G20_DRIVE_DAMPING = 40.0
|
||
GRAVITY_STIFFNESS_SCALE = 1.0
|
||
GRAVITY_DAMPING_SCALE = 1.2
|
||
DEFAULT_ACTUATED_MAX_EFFORT = 8.0
|
||
# 每物理步最大目标变化(rad)
|
||
MAX_TARGET_STEP_RAD = 0.006
|
||
CMD_SMOOTH_ALPHA = 0.08
|
||
CMD_SETTLE_EPS = 0.5
|
||
# 手掌安装高度(米)——抬高避免手指触地冲飞
|
||
HAND_MOUNT_Z = 0.70
|
||
HAND_MOUNT_Y = 0.45
|
||
# 根节点偏离安装位超过该距离视为爆炸并复位
|
||
ROOT_EXPLODE_DIST_M = 0.50
|
||
# 接触尖峰可达上千 rad/s;硬复位只会更抖。真正爆炸只看 NaN/根漂移。
|
||
DOF_VEL_SOFT_CLAMP = 30.0
|
||
DOF_VEL_HARD_LIMIT = 1.0e9
|
||
|
||
|
||
def parse_args() -> argparse.Namespace:
|
||
parser = argparse.ArgumentParser(description="LinkerHand O6 + G20 dual-hand Isaac Sim ROS2 bridge")
|
||
parser.add_argument("--headless", action="store_true", help="无 GUI 运行")
|
||
parser.add_argument("--kinematic", action="store_true", help="运动学驱动(无物理,不适合测摩擦)")
|
||
parser.add_argument(
|
||
"--hand-gravity",
|
||
action="store_true",
|
||
help="启用手部连杆重力(PD 下生效;硬写位置驱动会忽略)",
|
||
)
|
||
parser.add_argument(
|
||
"--pd-drive",
|
||
action="store_true",
|
||
help="PD 动力学驱动(默认已开启;与 --position-drive 互斥时以位置驱动为准)",
|
||
)
|
||
parser.add_argument(
|
||
"--self-collision",
|
||
action="store_true",
|
||
help="强制走 PD 以便体现 USD 自碰(默认 PD 下已启用 USD 自碰)",
|
||
)
|
||
parser.add_argument(
|
||
"--anti-penetration",
|
||
action="store_true",
|
||
help="兼容旧参数:碰撞/自碰已烘焙进 USD,无需再开",
|
||
)
|
||
parser.add_argument(
|
||
"--no-collision",
|
||
action="store_true",
|
||
help="关闭全部碰撞几何(调试用)",
|
||
)
|
||
parser.add_argument(
|
||
"--position-drive",
|
||
action="store_true",
|
||
help="硬写关节位置(UI 跟手,但会穿模;忽略 USD 自碰)",
|
||
)
|
||
parser.add_argument("--drive-stiffness", type=float, default=DEFAULT_DRIVE_STIFFNESS)
|
||
parser.add_argument("--drive-damping", type=float, default=DEFAULT_DRIVE_DAMPING)
|
||
parser.add_argument("--g20-drive-stiffness", type=float, default=G20_DRIVE_STIFFNESS)
|
||
parser.add_argument("--g20-drive-damping", type=float, default=G20_DRIVE_DAMPING)
|
||
parser.add_argument("--cmd-smooth", type=float, default=CMD_SMOOTH_ALPHA, help="指令低通 0~1,越大越跟手")
|
||
parser.add_argument("--o6-usd", type=Path, default=DEFAULT_ASSETS[("O6", "left")])
|
||
parser.add_argument("--g20-usd", type=Path, default=DEFAULT_ASSETS[("G20", "left")])
|
||
parser.add_argument("--l20-usd", type=Path, dest="g20_usd", help=argparse.SUPPRESS)
|
||
parser.add_argument("--o6-side", choices=["left", "right"], default="left")
|
||
parser.add_argument("--g20-side", choices=["left", "right"], default="left")
|
||
parser.add_argument("--o6-only", action="store_true", help="仅加载 O6")
|
||
parser.add_argument("--g20-only", action="store_true", help="仅加载 G20")
|
||
parser.add_argument("--both", action="store_true", help="同时加载 O6 + G20")
|
||
parser.add_argument("--l20-side", choices=["left", "right"], dest="g20_side", help=argparse.SUPPRESS)
|
||
parser.add_argument("--max-steps", type=int, default=0, help="运行指定步数后退出,0 表示持续运行")
|
||
parser.add_argument(
|
||
"--hold-open",
|
||
action="store_true",
|
||
help="忽略外部 control_cmd,始终保持 SDK 张开姿态(排查漂移/外部节点干扰)",
|
||
)
|
||
parser.add_argument(
|
||
"--stability-test",
|
||
action="store_true",
|
||
help="跑固定步数并打印 G20 状态漂移统计后退出(自动 hold-open)",
|
||
)
|
||
parser.add_argument(
|
||
"--motion-test",
|
||
action="store_true",
|
||
help="开合循环压力测试(内部切指令,不依赖外部 ROS pub)",
|
||
)
|
||
return parser.parse_args()
|
||
|
||
|
||
args = parse_args()
|
||
|
||
from isaacsim import SimulationApp
|
||
|
||
simulation_app = SimulationApp({"headless": args.headless})
|
||
|
||
import numpy as np
|
||
|
||
import isaacsim.core.experimental.utils.app as app_utils
|
||
import isaacsim.core.experimental.utils.prim as prim_utils
|
||
import isaacsim.core.experimental.utils.stage as stage_utils
|
||
from isaacsim.core.experimental.objects import DistantLight, GroundPlane
|
||
from isaacsim.core.experimental.prims import Articulation, XformPrim
|
||
from isaacsim.core.rendering_manager import RenderingManager
|
||
from isaacsim.core.simulation_manager import SimulationManager
|
||
from isaacsim.core.utils.viewports import set_camera_view
|
||
|
||
sys.path.insert(0, str(LINKERHAND_ROOT))
|
||
from joint_mapping import (
|
||
G20_OPEN_CMD,
|
||
O6_SDK_JOINTS,
|
||
mimic_joint_names,
|
||
open_hand_command,
|
||
sdk_range_to_full_urdf_positions,
|
||
sdk_range_to_urdf_positions,
|
||
urdf_positions_to_sdk_range,
|
||
)
|
||
|
||
app_utils.enable_extension("isaacsim.ros2.bridge")
|
||
simulation_app.update()
|
||
|
||
import rclpy
|
||
from rclpy.node import Node
|
||
from sensor_msgs.msg import JointState
|
||
|
||
|
||
class HandBridge:
|
||
"""单只手的 ROS2 ↔ Articulation 桥。"""
|
||
|
||
def __init__(
|
||
self,
|
||
node: Node,
|
||
articulation: Articulation,
|
||
hand: str,
|
||
side: str,
|
||
cmd_topic: str,
|
||
state_topic: str,
|
||
*,
|
||
kinematic: bool,
|
||
cmd_smooth: float,
|
||
hold_open: bool = False,
|
||
use_physics_drive: bool = False,
|
||
) -> None:
|
||
self.node = node
|
||
self.art = articulation
|
||
self.hand = hand
|
||
self.mapping_hand = hand if hand in ("O6", "G20", "L20") else "L20"
|
||
self.side = side
|
||
self.kinematic = kinematic
|
||
self.hold_open = hold_open
|
||
self.use_physics_drive = use_physics_drive
|
||
self.cmd_smooth = float(np.clip(cmd_smooth, 0.0, 1.0))
|
||
self.dof_count = 6 if hand == "O6" else 20
|
||
open_cmd = open_hand_command(hand)
|
||
self.cmd_values = list(open_cmd)
|
||
self.smoothed_cmd = list(open_cmd)
|
||
self._last_applied_key: tuple[float, ...] | None = None
|
||
self._slew_targets: dict[int, float] = {}
|
||
self._mount_position = None # set by main after load
|
||
self.dof_names = list(articulation.dof_names)
|
||
self.dof_index = {name: i for i, name in enumerate(self.dof_names)}
|
||
self.mimic_dofs = mimic_joint_names(self.mapping_hand, side)
|
||
self.actuated_dof_indices = [
|
||
i for i, name in enumerate(self.dof_names) if name not in self.mimic_dofs
|
||
]
|
||
self._actuated_names = {self.dof_names[i] for i in self.actuated_dof_indices}
|
||
|
||
lower, upper = articulation.get_dof_limits()
|
||
self._limit_lo = lower.numpy().flatten()
|
||
self._limit_hi = upper.numpy().flatten()
|
||
|
||
if not hold_open:
|
||
self.sub = node.create_subscription(JointState, cmd_topic, self._on_cmd, 10)
|
||
self.pub = node.create_publisher(JointState, state_topic, 10)
|
||
self._recover_cooldown = 0
|
||
self.explode_count = 0
|
||
self.max_root_drift = 0.0
|
||
mode = "kinematic" if kinematic else "dynamic(PD)"
|
||
hold_note = ", hold-open" if hold_open else ""
|
||
drive_note = "PD" if use_physics_drive else "position"
|
||
node.get_logger().info(
|
||
f"{hand} ({side}) [{mode}{hold_note}, drive={drive_note}]: sub {cmd_topic}, "
|
||
f"actuated={len(self.actuated_dof_indices)} mimic={len(self.mimic_dofs)}"
|
||
)
|
||
|
||
def set_sdk_command(self, values: list[float]) -> None:
|
||
"""内部压力测试:直接写 SDK 指令(绕过 ROS)。"""
|
||
n = min(len(values), self.dof_count)
|
||
self.cmd_values[:n] = [float(v) for v in values[:n]]
|
||
|
||
def _on_cmd(self, msg: JointState) -> None:
|
||
if self.hold_open or not msg.position:
|
||
return
|
||
n = min(len(msg.position), self.dof_count)
|
||
incoming = [float(v) for v in msg.position[:n]]
|
||
if all(abs(incoming[i] - self.cmd_values[i]) < CMD_SETTLE_EPS for i in range(n)):
|
||
return
|
||
self.cmd_values[:n] = incoming
|
||
|
||
def _smooth_commands(self) -> None:
|
||
if self.hold_open:
|
||
self.cmd_values = list(open_hand_command(self.hand))
|
||
self.smoothed_cmd = list(self.cmd_values)
|
||
return
|
||
a = self.cmd_smooth
|
||
if a <= 0.0:
|
||
self.smoothed_cmd = list(self.cmd_values)
|
||
return
|
||
for i in range(self.dof_count):
|
||
self.smoothed_cmd[i] = (1.0 - a) * self.smoothed_cmd[i] + a * self.cmd_values[i]
|
||
if max(abs(self.smoothed_cmd[i] - self.cmd_values[i]) for i in range(self.dof_count)) < CMD_SETTLE_EPS:
|
||
self.smoothed_cmd = list(self.cmd_values)
|
||
|
||
def _resolve_targets(self) -> dict[str, float]:
|
||
# 位置驱动/运动学:写全关节(含 mimic,比率需与 URDF 一致)
|
||
if self.kinematic or not self.use_physics_drive:
|
||
return sdk_range_to_full_urdf_positions(self.smoothed_cmd, self.mapping_hand, self.side)
|
||
return sdk_range_to_urdf_positions(self.smoothed_cmd, self.mapping_hand, self.side)
|
||
|
||
def _targets_to_dof_lists(self, targets: dict[str, float]) -> tuple[list[int], list[float]]:
|
||
dof_indices: list[int] = []
|
||
positions: list[float] = []
|
||
for joint_name, rad in targets.items():
|
||
idx = self._resolve_dof_index(joint_name)
|
||
if idx is None:
|
||
continue
|
||
lo = float(self._limit_lo[idx])
|
||
hi = float(self._limit_hi[idx])
|
||
if hi > lo:
|
||
rad = float(np.clip(rad, lo, hi))
|
||
# 斜坡限制:相对上一帧已下发目标,避免大跳
|
||
prev = self._slew_targets.get(idx, rad)
|
||
delta = rad - prev
|
||
if abs(delta) > MAX_TARGET_STEP_RAD:
|
||
rad = prev + float(np.sign(delta) * MAX_TARGET_STEP_RAD)
|
||
if hi > lo:
|
||
rad = float(np.clip(rad, lo, hi))
|
||
self._slew_targets[idx] = rad
|
||
dof_indices.append(idx)
|
||
positions.append(rad)
|
||
return dof_indices, positions
|
||
|
||
def _is_exploded(self) -> tuple[bool, str]:
|
||
"""返回 (是否需硬复位, 原因)。速度尖峰单独处理,不触发张开复位。"""
|
||
try:
|
||
root = self.art.get_world_poses()[0]
|
||
root = root.numpy().flatten() if hasattr(root, "numpy") else np.asarray(root).flatten()
|
||
if np.isnan(root).any() or np.isinf(root).any():
|
||
return True, "root_nan"
|
||
if self._mount_position is not None:
|
||
mount = np.asarray(self._mount_position, dtype=float)
|
||
drift = float(np.linalg.norm(root[:3] - mount))
|
||
self.max_root_drift = max(self.max_root_drift, drift)
|
||
limit = ROOT_EXPLODE_DIST_M if self.use_physics_drive else 5.0
|
||
if drift > limit:
|
||
return True, f"root_drift={drift:.3f}m"
|
||
elif float(np.max(np.abs(root))) > 5.0:
|
||
return True, "root_far"
|
||
|
||
pos = self.art.get_dof_positions()
|
||
pos = pos.numpy().flatten() if hasattr(pos, "numpy") else np.asarray(pos).flatten()
|
||
if np.isnan(pos).any() or np.isinf(pos).any():
|
||
return True, "dof_pos_nan"
|
||
|
||
if self.use_physics_drive:
|
||
try:
|
||
vel = self.art.get_dof_velocities()
|
||
vel = vel.numpy().flatten() if hasattr(vel, "numpy") else np.asarray(vel).flatten()
|
||
if np.isnan(vel).any() or np.isinf(vel).any():
|
||
return True, "dof_vel_nan"
|
||
max_abs = float(np.max(np.abs(vel))) if len(vel) else 0.0
|
||
if max_abs > DOF_VEL_HARD_LIMIT:
|
||
return True, f"dof_vel={max_abs:.1f}"
|
||
if max_abs > DOF_VEL_SOFT_CLAMP:
|
||
# 自碰接触尖峰:只清速度,不当成爆炸
|
||
try:
|
||
self.art.set_dof_velocities([0.0] * len(vel))
|
||
except Exception:
|
||
pass
|
||
return False, f"vel_spike={max_abs:.1f}_clamped"
|
||
except Exception:
|
||
pass
|
||
except Exception as exc:
|
||
return True, f"exception:{exc}"
|
||
return False, ""
|
||
|
||
def _pin_root(self) -> None:
|
||
"""把掌根钉回安装位(仅硬写位置驱动备份;PD+WorldFixedJoint 时不要调)。"""
|
||
if self._mount_position is None or self.use_physics_drive:
|
||
return
|
||
try:
|
||
self.art.set_world_poses(
|
||
positions=[self._mount_position],
|
||
orientations=[[1.0, 0.0, 0.0, 0.0]],
|
||
)
|
||
if hasattr(self.art, "set_velocities"):
|
||
self.art.set_velocities(
|
||
linear_velocities=[[0.0, 0.0, 0.0]],
|
||
angular_velocities=[[0.0, 0.0, 0.0]],
|
||
)
|
||
except Exception as exc:
|
||
self.node.get_logger().warn(f"{self.hand}: _pin_root failed: {exc}")
|
||
|
||
def recover_if_exploded(self) -> bool:
|
||
"""物理爆炸后复位到张开姿态。返回是否发生了硬复位。"""
|
||
if getattr(self, "_recover_cooldown", 0) > 0:
|
||
self._recover_cooldown -= 1
|
||
if not self.use_physics_drive:
|
||
self._pin_root()
|
||
# PD 下若仍 NaN,必须再 snap 一次,否则冷却期结束后会立刻再爆
|
||
try:
|
||
pos = self.art.get_dof_positions()
|
||
pos = pos.numpy().flatten() if hasattr(pos, "numpy") else np.asarray(pos).flatten()
|
||
if np.isnan(pos).any() or np.isinf(pos).any():
|
||
self.initialize_pose()
|
||
self.art.set_dof_velocities([0.0] * int(self.art.num_dofs))
|
||
except Exception:
|
||
try:
|
||
self.initialize_pose()
|
||
except Exception:
|
||
pass
|
||
return True
|
||
exploded, reason = self._is_exploded()
|
||
if not exploded:
|
||
return False
|
||
try:
|
||
root = self.art.get_world_poses()[0]
|
||
root = root.numpy().flatten() if hasattr(root, "numpy") else np.asarray(root).flatten()
|
||
root_txt = [round(float(x), 3) for x in root[:3]]
|
||
except Exception:
|
||
root_txt = ["?"]
|
||
self.node.get_logger().warn(
|
||
f"{self.hand}: 物理爆炸复位 reason={reason} root={root_txt}"
|
||
)
|
||
self.explode_count += 1
|
||
self.cmd_values = list(open_hand_command(self.hand))
|
||
self.smoothed_cmd = list(self.cmd_values)
|
||
self._slew_targets.clear()
|
||
self._last_applied_key = None
|
||
self._pin_root()
|
||
try:
|
||
n = self.art.num_dofs
|
||
if n > 0:
|
||
self.art.set_dof_velocities([0.0] * n)
|
||
except Exception:
|
||
pass
|
||
self.initialize_pose()
|
||
self._pin_root()
|
||
self._recover_cooldown = 30
|
||
return True
|
||
|
||
def apply_targets(self) -> None:
|
||
if self.recover_if_exploded():
|
||
return
|
||
# 位置驱动:每帧钉掌根(O6 无 WorldFixedJoint;G20 作备份)
|
||
if not self.use_physics_drive:
|
||
self._pin_root()
|
||
self._smooth_commands()
|
||
targets = self._resolve_targets()
|
||
dof_indices, positions = self._targets_to_dof_lists(targets)
|
||
if not dof_indices:
|
||
return
|
||
apply_key = tuple(round(p, 5) for p in positions)
|
||
self._last_applied_key = apply_key
|
||
if self.kinematic or not self.use_physics_drive:
|
||
self.art.set_dof_positions(positions, dof_indices=dof_indices)
|
||
else:
|
||
self.art.set_dof_position_targets(positions, dof_indices=dof_indices)
|
||
|
||
def _read_sdk_state(self) -> list[float]:
|
||
dof_pos = self.art.get_dof_positions()
|
||
if hasattr(dof_pos, "numpy"):
|
||
pos_array = dof_pos.numpy().flatten()
|
||
else:
|
||
pos_array = np.asarray(dof_pos).flatten()
|
||
|
||
joint_pos_map: dict[str, float] = {}
|
||
for name in self._actuated_names:
|
||
idx = self.dof_index.get(name)
|
||
if idx is not None and idx < len(pos_array):
|
||
joint_pos_map[name] = float(pos_array[idx])
|
||
return [float(v) for v in urdf_positions_to_sdk_range(joint_pos_map, self.hand, self.side)]
|
||
|
||
def publish_state(self) -> None:
|
||
sdk_range = self._read_sdk_state()
|
||
msg = JointState()
|
||
msg.header.stamp = self.node.get_clock().now().to_msg()
|
||
if self.hand == "O6":
|
||
msg.name = list(O6_SDK_JOINTS)
|
||
else:
|
||
msg.name = [f"motor_{i}" for i in range(20)]
|
||
msg.position = sdk_range
|
||
self.pub.publish(msg)
|
||
|
||
def _resolve_dof_index(self, joint_name: str) -> int | None:
|
||
if joint_name in self.dof_index:
|
||
return self.dof_index[joint_name]
|
||
for name, idx in self.dof_index.items():
|
||
if name.endswith(joint_name) or name.split("/")[-1] == joint_name:
|
||
return idx
|
||
return None
|
||
|
||
def snap_pose(self) -> None:
|
||
"""强制写入关节角与 PD 目标(初始化/复位)。"""
|
||
if self.hold_open:
|
||
self.smoothed_cmd = list(open_hand_command(self.hand))
|
||
self.cmd_values = list(self.smoothed_cmd)
|
||
# snap 时跳过斜坡,直接到位
|
||
self._slew_targets.clear()
|
||
targets = self._resolve_targets()
|
||
# 直接限幅,不走斜坡字典预填
|
||
dof_indices: list[int] = []
|
||
positions: list[float] = []
|
||
for joint_name, rad in targets.items():
|
||
idx = self._resolve_dof_index(joint_name)
|
||
if idx is None:
|
||
continue
|
||
lo = float(self._limit_lo[idx])
|
||
hi = float(self._limit_hi[idx])
|
||
if hi > lo:
|
||
rad = float(np.clip(rad, lo, hi))
|
||
self._slew_targets[idx] = rad
|
||
dof_indices.append(idx)
|
||
positions.append(rad)
|
||
if not dof_indices:
|
||
return
|
||
self.art.set_dof_positions(positions, dof_indices=dof_indices)
|
||
if not self.kinematic and self.use_physics_drive:
|
||
self.art.set_dof_position_targets(positions, dof_indices=dof_indices)
|
||
self._last_applied_key = tuple(round(p, 5) for p in positions)
|
||
|
||
def initialize_pose(self) -> None:
|
||
self.snap_pose()
|
||
if not self.kinematic and self.use_physics_drive and self.actuated_dof_indices:
|
||
self.art.set_dof_velocities(
|
||
[0.0] * len(self.actuated_dof_indices),
|
||
dof_indices=self.actuated_dof_indices,
|
||
)
|
||
|
||
|
||
def _configure_drives(
|
||
articulation: Articulation,
|
||
hand: str,
|
||
side: str,
|
||
*,
|
||
kinematic: bool,
|
||
stiffness: float,
|
||
damping: float,
|
||
max_effort: float = DEFAULT_ACTUATED_MAX_EFFORT,
|
||
) -> None:
|
||
mapping_hand = "O6" if hand == "O6" else "L20"
|
||
mimics = mimic_joint_names(mapping_hand, side)
|
||
actuated: list[int] = []
|
||
mimic_indices: list[int] = []
|
||
for i, name in enumerate(articulation.dof_names):
|
||
(mimic_indices if name in mimics else actuated).append(i)
|
||
|
||
if kinematic:
|
||
n = articulation.num_dofs
|
||
if n > 0:
|
||
articulation.set_dof_gains(stiffnesses=[0.0] * n, dampings=[0.0] * n)
|
||
return
|
||
|
||
# 位置驱动遥操作:增益必须为 0,否则 PD 与 set_dof_positions 硬刚导致爆炸
|
||
if stiffness <= 0.0 and damping <= 0.0:
|
||
n = articulation.num_dofs
|
||
if n > 0:
|
||
articulation.set_dof_gains(stiffnesses=[0.0] * n, dampings=[0.0] * n)
|
||
return
|
||
|
||
if actuated:
|
||
articulation.set_dof_gains(
|
||
stiffnesses=[stiffness] * len(actuated),
|
||
dampings=[damping] * len(actuated),
|
||
dof_indices=actuated,
|
||
)
|
||
articulation.set_dof_max_efforts(
|
||
[max_effort] * len(actuated),
|
||
dof_indices=actuated,
|
||
)
|
||
if mimic_indices:
|
||
# 不要把 mimic 的 stiffness 设为 0:部分后端会破坏 mimic 约束,连带整手塌陷
|
||
articulation.set_dof_max_efforts(
|
||
[max_effort] * len(mimic_indices),
|
||
dof_indices=mimic_indices,
|
||
)
|
||
|
||
|
||
def _author_self_collisions(hand_path: str, enabled: bool) -> None:
|
||
"""在 ArticulationRoot 上写入 PhysX/Newton 自碰,并配置碰撞体。
|
||
|
||
enabled=True 时:关掉掌心/CMC 肥凸包,只保留手指段碰撞,降低自碰爆炸概率。
|
||
"""
|
||
from pxr import Sdf, Usd, UsdPhysics
|
||
|
||
stage = stage_utils.get_current_stage()
|
||
hand_prim = stage.GetPrimAtPath(hand_path)
|
||
if not hand_prim.IsValid():
|
||
return
|
||
|
||
try:
|
||
from pxr import PhysxSchema
|
||
|
||
has_physx_schema = True
|
||
except ImportError:
|
||
has_physx_schema = False
|
||
|
||
# 这些碰撞体与拇指运动包络严重重叠,开自碰必顶死/NaN
|
||
disable_col_names = {
|
||
"hand_base_link",
|
||
"thumb_metacarpals_base1",
|
||
"thumb_metacarpals_base2",
|
||
"thumb_metacarpals",
|
||
"index_metacarpals",
|
||
"middle_metacarpals",
|
||
"ring_metacarpals",
|
||
"pinky_metacarpals",
|
||
}
|
||
|
||
predicate = Usd.TraverseInstanceProxies(Usd.PrimAllPrimsPredicate)
|
||
n_roots = 0
|
||
n_col_on = 0
|
||
n_col_off = 0
|
||
for prim in Usd.PrimRange(hand_prim, predicate):
|
||
if prim.HasAPI(UsdPhysics.ArticulationRootAPI) or prim.HasAPI("PhysicsArticulationRootAPI"):
|
||
if has_physx_schema:
|
||
PhysxSchema.PhysxArticulationAPI.Apply(prim)
|
||
attr = prim.GetAttribute("physxArticulation:enabledSelfCollisions")
|
||
if not attr:
|
||
attr = prim.CreateAttribute(
|
||
"physxArticulation:enabledSelfCollisions", Sdf.ValueTypeNames.Bool
|
||
)
|
||
attr.Set(bool(enabled))
|
||
nattr = prim.GetAttribute("newton:selfCollisionEnabled")
|
||
if not nattr:
|
||
nattr = prim.CreateAttribute("newton:selfCollisionEnabled", Sdf.ValueTypeNames.Bool)
|
||
nattr.Set(bool(enabled))
|
||
n_roots += 1
|
||
if (
|
||
prim.HasAPI(UsdPhysics.CollisionAPI)
|
||
or prim.HasAPI("PhysicsCollisionAPI")
|
||
or prim.HasAPI("PhysicsMeshCollisionAPI")
|
||
):
|
||
name = prim.GetName()
|
||
col_on = True
|
||
if enabled and name in disable_col_names:
|
||
col_on = False
|
||
try:
|
||
if prim.HasAPI(UsdPhysics.CollisionAPI):
|
||
UsdPhysics.CollisionAPI(prim).CreateCollisionEnabledAttr().Set(col_on)
|
||
else:
|
||
attr = prim.GetAttribute("physics:collisionEnabled")
|
||
if not attr:
|
||
attr = prim.CreateAttribute(
|
||
"physics:collisionEnabled", Sdf.ValueTypeNames.Bool
|
||
)
|
||
attr.Set(col_on)
|
||
if col_on:
|
||
n_col_on += 1
|
||
else:
|
||
n_col_off += 1
|
||
except Exception:
|
||
pass
|
||
print(
|
||
f"[stabilize] authored self_collision={enabled} roots={n_roots} "
|
||
f"col_on={n_col_on} col_off={n_col_off} under {hand_path}"
|
||
)
|
||
if enabled:
|
||
_filter_thumb_palm_collisions(hand_path)
|
||
|
||
|
||
def _filter_thumb_palm_collisions(hand_path: str) -> None:
|
||
"""拇指链与掌根凸包在 CMC 处天然重叠;PhysX 只自动忽略父子连杆。
|
||
|
||
不过滤时自碰会顶住拇指。处理:
|
||
1) 关掉 CMC 中间连杆 + 掌根碰撞体(显示 mesh 仍在;物理不互顶)
|
||
2) palm↔拇指 FilteredPairs
|
||
四指之间自碰仍保留,用来减轻拇指穿其它指。
|
||
"""
|
||
from pxr import Usd, UsdPhysics
|
||
|
||
stage = stage_utils.get_current_stage()
|
||
hand_prim = stage.GetPrimAtPath(hand_path)
|
||
if not hand_prim.IsValid():
|
||
return
|
||
|
||
predicate = Usd.TraverseInstanceProxies(Usd.PrimAllPrimsPredicate)
|
||
palm = None
|
||
thumb_links: list = []
|
||
cmc_disabled = 0
|
||
palm_col_off = 0
|
||
for prim in Usd.PrimRange(hand_prim, predicate):
|
||
name = prim.GetName()
|
||
if prim.HasAPI(UsdPhysics.CollisionAPI) and name in (
|
||
"thumb_metacarpals_base1",
|
||
"thumb_metacarpals_base2",
|
||
"hand_base_link",
|
||
):
|
||
try:
|
||
UsdPhysics.CollisionAPI(prim).CreateCollisionEnabledAttr().Set(False)
|
||
if name == "hand_base_link":
|
||
palm_col_off += 1
|
||
else:
|
||
cmc_disabled += 1
|
||
except Exception as exc:
|
||
print(f"[stabilize] collision disable failed {prim.GetPath()}: {exc}")
|
||
if not prim.HasAPI(UsdPhysics.RigidBodyAPI):
|
||
continue
|
||
if name in ("hand_base_link", "base_link", "palm"):
|
||
palm = prim
|
||
elif name.startswith("thumb_"):
|
||
thumb_links.append(prim)
|
||
|
||
if palm is None or not thumb_links:
|
||
print(
|
||
f"[stabilize] thumb-palm filter skipped "
|
||
f"(palm={palm is not None}, thumbs={len(thumb_links)}, "
|
||
f"cmc_off={cmc_disabled}, palm_col_off={palm_col_off})"
|
||
)
|
||
return
|
||
|
||
api = UsdPhysics.FilteredPairsAPI.Apply(palm)
|
||
rel = api.CreateFilteredPairsRel()
|
||
existing = {str(p) for p in (rel.GetTargets() or [])}
|
||
added = 0
|
||
for link in thumb_links:
|
||
path = link.GetPath()
|
||
if str(path) in existing:
|
||
continue
|
||
rel.AddTarget(path)
|
||
other = UsdPhysics.FilteredPairsAPI.Apply(link)
|
||
other_rel = other.CreateFilteredPairsRel()
|
||
if palm.GetPath() not in (other_rel.GetTargets() or []):
|
||
other_rel.AddTarget(palm.GetPath())
|
||
added += 1
|
||
print(
|
||
f"[stabilize] palm↔thumb filter pairs={added} cmc_off={cmc_disabled} "
|
||
f"palm_col_off={palm_col_off} thumbs={[t.GetName() for t in thumb_links]}"
|
||
)
|
||
|
||
|
||
def _stabilize_articulation(
|
||
articulation: Articulation,
|
||
*,
|
||
enable_gravity: bool,
|
||
disable_collisions: bool = False,
|
||
enable_self_collisions: bool = False,
|
||
) -> None:
|
||
"""动力学仿真稳定性:默认保留碰撞;自碰需配合 PD 位置目标才有效。"""
|
||
try:
|
||
articulation.set_enabled_self_collisions([bool(enable_self_collisions)])
|
||
except Exception as exc:
|
||
print(f"[stabilize] set_enabled_self_collisions failed: {exc}")
|
||
articulation.set_link_enabled_gravities(enable_gravity)
|
||
if disable_collisions:
|
||
from pxr import Usd, UsdPhysics
|
||
|
||
try:
|
||
link_groups = articulation.link_paths
|
||
except Exception:
|
||
link_groups = []
|
||
stage = stage_utils.get_current_stage()
|
||
predicate = Usd.TraverseInstanceProxies(Usd.PrimAllPrimsPredicate)
|
||
n_disabled = 0
|
||
for group in link_groups:
|
||
for path in group:
|
||
prim = stage.GetPrimAtPath(path)
|
||
if not prim.IsValid():
|
||
continue
|
||
for child in Usd.PrimRange(prim, predicate):
|
||
if child.HasAPI(UsdPhysics.CollisionAPI):
|
||
try:
|
||
UsdPhysics.CollisionAPI(child).CreateCollisionEnabledAttr().Set(False)
|
||
n_disabled += 1
|
||
except Exception:
|
||
pass
|
||
print(f"[stabilize] disabled {n_disabled} collision APIs under articulation")
|
||
else:
|
||
print(
|
||
f"[stabilize] collisions ON, self_collision={'ON' if enable_self_collisions else 'OFF'}"
|
||
)
|
||
|
||
|
||
def _patch_joint_max_forces(hand_path: str, max_force: float = DEFAULT_ACTUATED_MAX_EFFORT) -> None:
|
||
"""URDF 转换默认 maxForce=1,直接改 USD 驱动上限。"""
|
||
from pxr import Usd, UsdPhysics
|
||
|
||
stage = stage_utils.get_current_stage()
|
||
root = stage.GetPrimAtPath(hand_path)
|
||
if not root.IsValid():
|
||
return
|
||
for prim in Usd.PrimRange(root):
|
||
for drive_name in ("angular", "linear"):
|
||
if not prim.HasAPI(UsdPhysics.DriveAPI, drive_name):
|
||
continue
|
||
drive = UsdPhysics.DriveAPI(prim, drive_name)
|
||
attr = drive.GetMaxForceAttr()
|
||
if attr:
|
||
attr.Set(float(max_force))
|
||
stiff = drive.GetStiffnessAttr()
|
||
damp = drive.GetDampingAttr()
|
||
# 仅保证属性存在;实际增益仍由 Articulation.set_dof_gains 写入
|
||
if stiff and stiff.Get() is None:
|
||
stiff.Set(0.0)
|
||
if damp and damp.Get() is None:
|
||
damp.Set(0.0)
|
||
|
||
|
||
def _disable_floating_root_joint(hand_path: str) -> None:
|
||
"""禁用资产内无效 root_joint(body0 非刚体)。"""
|
||
from pxr import Usd, UsdPhysics
|
||
|
||
stage = stage_utils.get_current_stage()
|
||
hand_prim = stage.GetPrimAtPath(hand_path)
|
||
if not hand_prim.IsValid():
|
||
return
|
||
for prim in Usd.PrimRange(hand_prim):
|
||
if prim.GetName() == "root_joint" and prim.IsA(UsdPhysics.Joint):
|
||
prim.SetActive(False)
|
||
print(f"[anchor] disabled floating {prim.GetPath()}")
|
||
return
|
||
|
||
|
||
def _find_root_rigid_body(hand_prim) -> object | None:
|
||
"""找掌根刚体:优先 hand_base_link / base_link,否则第一个 RigidBody。"""
|
||
from pxr import Usd, UsdPhysics
|
||
|
||
preferred = None
|
||
first = None
|
||
for prim in Usd.PrimRange(hand_prim):
|
||
if not prim.HasAPI(UsdPhysics.RigidBodyAPI):
|
||
continue
|
||
if first is None:
|
||
first = prim
|
||
name = prim.GetName()
|
||
if name in ("hand_base_link", "base_link", "palm"):
|
||
preferred = prim
|
||
break
|
||
return preferred or first
|
||
|
||
|
||
def _fix_base_to_world(hand_path: str) -> None:
|
||
"""把掌根钉到当前世界位姿:优先改写 USD 内 root_joint 的 localPos0/Rot0。
|
||
|
||
交付 USD 的 root_joint 已是 body0=world、body1=palm;安装位姿由场景 Xform
|
||
决定,因此这里只写入世界坐标,不另造 WorldFixedJoint(避免双固定)。
|
||
"""
|
||
from pxr import Gf, Usd, UsdGeom, UsdPhysics
|
||
|
||
stage = stage_utils.get_current_stage()
|
||
hand_prim = stage.GetPrimAtPath(hand_path)
|
||
if not hand_prim.IsValid():
|
||
return
|
||
|
||
base = _find_root_rigid_body(hand_prim)
|
||
if base is None:
|
||
print(f"[anchor] no RigidBody under {hand_path}")
|
||
return
|
||
|
||
xf = UsdGeom.Xformable(base).ComputeLocalToWorldTransform(Usd.TimeCode.Default())
|
||
pos = xf.ExtractTranslation()
|
||
quat = xf.ExtractRotationQuat()
|
||
pos_f = Gf.Vec3f(float(pos[0]), float(pos[1]), float(pos[2]))
|
||
rot_f = Gf.Quatf(float(quat.GetReal()), *[float(v) for v in quat.GetImaginary()])
|
||
|
||
for prim in Usd.PrimRange(hand_prim):
|
||
if prim.GetName() != "root_joint" or not prim.IsA(UsdPhysics.Joint):
|
||
continue
|
||
joint = UsdPhysics.Joint(prim)
|
||
b1 = [str(t) for t in (joint.GetBody1Rel().GetTargets() or [])]
|
||
b0 = [str(t) for t in (joint.GetBody0Rel().GetTargets() or [])]
|
||
if str(base.GetPath()) not in b1 or len(b0) != 0:
|
||
prim.SetActive(False)
|
||
print(f"[anchor] disabled invalid {prim.GetPath()} body0={b0} body1={b1}")
|
||
continue
|
||
if not prim.IsActive():
|
||
prim.SetActive(True)
|
||
joint.CreateLocalPos0Attr().Set(pos_f)
|
||
joint.CreateLocalRot0Attr().Set(rot_f)
|
||
joint.CreateLocalPos1Attr().Set(Gf.Vec3f(0.0, 0.0, 0.0))
|
||
joint.CreateLocalRot1Attr().Set(Gf.Quatf(1.0, 0.0, 0.0, 0.0))
|
||
print(
|
||
f"[anchor] USD root_joint {prim.GetPath()} -> {base.GetPath()} @ "
|
||
f"({float(pos[0]):.3f},{float(pos[1]):.3f},{float(pos[2]):.3f})"
|
||
)
|
||
return
|
||
|
||
# Fallback if asset has no usable root_joint
|
||
joint_path = f"{hand_path}/WorldFixedJoint"
|
||
if stage.GetPrimAtPath(joint_path).IsValid():
|
||
stage.RemovePrim(joint_path)
|
||
joint = UsdPhysics.FixedJoint.Define(stage, joint_path)
|
||
joint.CreateBody0Rel().SetTargets([])
|
||
joint.CreateBody1Rel().SetTargets([base.GetPath()])
|
||
joint.CreateLocalPos0Attr().Set(pos_f)
|
||
joint.CreateLocalRot0Attr().Set(rot_f)
|
||
joint.CreateLocalPos1Attr().Set(Gf.Vec3f(0.0, 0.0, 0.0))
|
||
joint.CreateLocalRot1Attr().Set(Gf.Quatf(1.0, 0.0, 0.0, 0.0))
|
||
joint.CreateExcludeFromArticulationAttr().Set(False)
|
||
print(
|
||
f"[anchor] WorldFixedJoint {joint_path} -> {base.GetPath()} @ "
|
||
f"({float(pos[0]):.3f},{float(pos[1]):.3f},{float(pos[2]):.3f})"
|
||
)
|
||
|
||
|
||
def _load_hand_reference(usd_path: Path, mount_path: str, position: list[float]) -> str:
|
||
"""加载手资产引用,返回 hand_path(此时尚未创建 Articulation)。"""
|
||
if not usd_path.is_file():
|
||
raise FileNotFoundError(f"USD 不存在: {usd_path}")
|
||
stage_utils.define_prim(mount_path, "Xform")
|
||
XformPrim(mount_path, positions=[position], reset_xform_op_properties=True)
|
||
hand_path = f"{mount_path}/hand"
|
||
stage_utils.add_reference_to_stage(usd_path=str(usd_path), path=hand_path)
|
||
prim_utils.set_prim_variants(hand_path, variants=[("Physics", "physx")])
|
||
_patch_joint_max_forces(hand_path)
|
||
return hand_path
|
||
|
||
|
||
def _print_g20_stability_report(samples: list[list[float]]) -> bool:
|
||
"""Return True if G20 state is stable enough."""
|
||
import statistics
|
||
|
||
open_pose = [float(v) for v in G20_OPEN_CMD]
|
||
if len(samples) < 5:
|
||
print(f"G20 stability: insufficient samples ({len(samples)})")
|
||
return False
|
||
|
||
max_step = 0.0
|
||
for i in range(1, len(samples)):
|
||
for m in range(20):
|
||
max_step = max(max_step, abs(samples[i][m] - samples[i - 1][m]))
|
||
|
||
motor_std = [statistics.pstdev(s[m] for s in samples) for m in range(20)]
|
||
max_dev = max(max(abs(s[m] - open_pose[m]) for s in samples) for m in range(20))
|
||
unstable = [m for m, std in enumerate(motor_std) if std > 1.0]
|
||
|
||
print("=== G20 stability report ===")
|
||
print(f"samples={len(samples)} max_step_delta={max_step:.2f} max_dev_open={max_dev:.2f}")
|
||
print(f"unstable_motors(std>1): {unstable}")
|
||
print(f"first: {[round(v, 1) for v in samples[0]]}")
|
||
print(f"last: {[round(v, 1) for v in samples[-1]]}")
|
||
ok = max_step <= 2.0 and len(unstable) == 0 and max_dev <= 80.0
|
||
print("RESULT:", "PASS" if ok else "FAIL")
|
||
return ok
|
||
|
||
|
||
def main() -> None:
|
||
stage_utils.create_new_stage()
|
||
stage_utils.set_stage_units(meters_per_unit=1.0)
|
||
|
||
GroundPlane("/World/GroundPlane", positions=[0.0, 0.0, 0.0])
|
||
light = DistantLight("/World/DistantLight")
|
||
light.set_intensities(300.0)
|
||
set_camera_view(eye=[0.55, 0.0, 0.55], target=[0.0, 0.0, 0.35], camera_prim_path="/OmniverseKit_Persp")
|
||
|
||
# 默认只加载 G20;--o6-only / --both 可改
|
||
if args.both:
|
||
load_o6 = load_g20 = True
|
||
elif args.o6_only:
|
||
load_o6, load_g20 = True, False
|
||
else:
|
||
# --g20-only 或未指定:仅 G20
|
||
load_o6, load_g20 = False, True
|
||
|
||
o6_path = (
|
||
_load_hand_reference(args.o6_usd, "/World/O6Hand", [0.0, -HAND_MOUNT_Y, HAND_MOUNT_Z])
|
||
if load_o6
|
||
else None
|
||
)
|
||
g20_path = (
|
||
_load_hand_reference(args.g20_usd, "/World/G20Hand", [0.0, HAND_MOUNT_Y, HAND_MOUNT_Z])
|
||
if load_g20
|
||
else None
|
||
)
|
||
|
||
SimulationManager.set_physics_dt(1.0 / 120.0)
|
||
|
||
app_utils.play()
|
||
simulation_app.update()
|
||
|
||
# play 之后再钉世界固定关节(此时世界位姿已正确)
|
||
if o6_path:
|
||
_disable_floating_root_joint(o6_path)
|
||
if g20_path:
|
||
_fix_base_to_world(g20_path)
|
||
|
||
# 碰撞/自碰已写进交付 USD;此处不再运行时改 mesh
|
||
# 仅在显式 --no-collision 时关掉
|
||
want_self_collision_early = False
|
||
if args.anti_penetration:
|
||
print(
|
||
"[stabilize] 碰撞/自碰已烘焙进 USD:"
|
||
f"{args.g20_usd if hasattr(args,'g20_usd') else 'g20'};"
|
||
"请直接加载该资产。运行时勿再改 collision。"
|
||
)
|
||
args.anti_penetration = False
|
||
|
||
app_utils.stop()
|
||
simulation_app.update()
|
||
app_utils.play()
|
||
simulation_app.update()
|
||
|
||
# 不再调用 _author_self_collisions(交付以 USD 为准)
|
||
|
||
app_utils.stop()
|
||
simulation_app.update()
|
||
app_utils.play()
|
||
simulation_app.update()
|
||
|
||
# 必须在最终 play 之后创建 Articulation,否则 tensor 句柄会过期
|
||
o6 = Articulation(o6_path) if o6_path else None
|
||
g20 = Articulation(g20_path) if g20_path else None
|
||
|
||
hand_configs = []
|
||
if load_o6:
|
||
hand_configs.append((o6, "O6", args.o6_side))
|
||
if load_g20:
|
||
hand_configs.append((g20, "G20", args.g20_side))
|
||
|
||
# 硬写 set_dof_positions 会穿透接触;自碰必须用 PD 目标
|
||
# 默认 PD(交付 USD 自碰生效);仅 --position-drive / --kinematic 关闭
|
||
want_self_collision = (not args.no_collision) and (not args.kinematic)
|
||
use_physics_drive_early = (not args.kinematic) and (not args.position_drive)
|
||
if args.position_drive:
|
||
print(
|
||
"[stabilize] --position-drive:硬写关节,USD 自碰无法阻止穿模。"
|
||
"防拇指穿掌请去掉该参数(默认 PD)。"
|
||
)
|
||
if args.self_collision and args.position_drive:
|
||
print("[stabilize] --self-collision 与 --position-drive 冲突,改用 PD")
|
||
use_physics_drive_early = True
|
||
if args.hand_gravity and not use_physics_drive_early:
|
||
print(
|
||
"[warn] --hand-gravity 在硬写位置驱动下已忽略。"
|
||
"测重力/摩擦: 默认 PD + --hand-gravity(不要加 --position-drive)"
|
||
)
|
||
enable_link_gravity = bool(args.hand_gravity) and use_physics_drive_early
|
||
|
||
for art, hand, side in hand_configs:
|
||
if use_physics_drive_early:
|
||
stiff = args.drive_stiffness
|
||
damp = args.drive_damping
|
||
if hand == "G20":
|
||
stiff = args.g20_drive_stiffness
|
||
damp = args.g20_drive_damping
|
||
if enable_link_gravity:
|
||
stiff *= GRAVITY_STIFFNESS_SCALE
|
||
damp *= GRAVITY_DAMPING_SCALE
|
||
effort = DEFAULT_ACTUATED_MAX_EFFORT
|
||
else:
|
||
# 位置驱动:零增益,只靠 set_dof_positions
|
||
stiff = 0.0
|
||
damp = 0.0
|
||
effort = DEFAULT_ACTUATED_MAX_EFFORT
|
||
_configure_drives(
|
||
art,
|
||
hand,
|
||
side,
|
||
kinematic=args.kinematic,
|
||
stiffness=stiff,
|
||
damping=damp,
|
||
max_effort=effort,
|
||
)
|
||
if hand == "G20" and g20_path:
|
||
# 已在 stop/play 前写入;此处再刷一遍 API 状态
|
||
try:
|
||
art.set_enabled_self_collisions([want_self_collision])
|
||
except Exception:
|
||
pass
|
||
elif hand == "O6" and o6_path:
|
||
try:
|
||
art.set_enabled_self_collisions([want_self_collision])
|
||
except Exception:
|
||
pass
|
||
_stabilize_articulation(
|
||
art,
|
||
enable_gravity=enable_link_gravity,
|
||
# 尊重 USD 碰撞;仅 --no-collision 时关掉
|
||
disable_collisions=bool(args.no_collision),
|
||
enable_self_collisions=want_self_collision and use_physics_drive_early,
|
||
)
|
||
|
||
simulation_app.update()
|
||
|
||
rclpy.init()
|
||
ros_node = Node("linker_hand_isaac_sim")
|
||
|
||
# 与上面一致:自碰开启时必须用 PD 目标
|
||
use_physics_drive = use_physics_drive_early
|
||
bridge_kwargs = {
|
||
"kinematic": args.kinematic,
|
||
"cmd_smooth": args.cmd_smooth,
|
||
"hold_open": args.hold_open or args.stability_test,
|
||
"use_physics_drive": use_physics_drive,
|
||
}
|
||
o6_bridge = None
|
||
g20_bridge = None
|
||
if load_o6:
|
||
o6_bridge = HandBridge(
|
||
ros_node, o6, hand="O6", side=args.o6_side,
|
||
cmd_topic=TOPICS["O6"]["cmd"], state_topic=TOPICS["O6"]["state"],
|
||
**bridge_kwargs,
|
||
)
|
||
o6_bridge._mount_position = [0.0, -HAND_MOUNT_Y, HAND_MOUNT_Z]
|
||
if load_g20:
|
||
g20_bridge = HandBridge(
|
||
ros_node, g20, hand="G20", side=args.g20_side,
|
||
cmd_topic=TOPICS["G20"]["cmd"], state_topic=TOPICS["G20"]["state"],
|
||
**bridge_kwargs,
|
||
)
|
||
g20_bridge._mount_position = [0.0, HAND_MOUNT_Y, HAND_MOUNT_Z]
|
||
|
||
if o6_bridge:
|
||
o6_bridge.initialize_pose()
|
||
if g20_bridge:
|
||
g20_bridge.initialize_pose()
|
||
|
||
if use_physics_drive:
|
||
for _ in range(120):
|
||
if o6_bridge:
|
||
o6_bridge.apply_targets()
|
||
if g20_bridge:
|
||
g20_bridge.apply_targets()
|
||
SimulationManager.step()
|
||
simulation_app.update()
|
||
if g20_bridge and args.stability_test:
|
||
ros_node.get_logger().info(f"G20 after settle: {g20_bridge._read_sdk_state()[:8]}")
|
||
else:
|
||
for _ in range(5):
|
||
simulation_app.update()
|
||
|
||
mode_str = (
|
||
"运动学"
|
||
if args.kinematic
|
||
else (
|
||
"PD+自碰"
|
||
if use_physics_drive and want_self_collision
|
||
else ("纯 PD 动力学" if use_physics_drive else "位置驱动(可穿模)")
|
||
)
|
||
)
|
||
grav_str = "开" if enable_link_gravity else ("关(硬写忽略)" if args.hand_gravity else "关")
|
||
ros_node.get_logger().info(
|
||
f"仿真就绪 [{mode_str}, 手部重力={grav_str}]。"
|
||
f"UI: run_sim.sh ;测摩擦: run_sim.sh --pd-drive --hand-gravity"
|
||
)
|
||
|
||
stability_samples: list[list[float]] = []
|
||
max_steps = args.max_steps
|
||
if args.stability_test and max_steps <= 0:
|
||
max_steps = 960
|
||
if args.motion_test and max_steps <= 0:
|
||
max_steps = 720
|
||
|
||
def _motion_pose(phase: int) -> None:
|
||
"""phase 偶数=张开,奇数=半握。"""
|
||
if phase % 2 == 0:
|
||
if o6_bridge:
|
||
o6_bridge.set_sdk_command(open_hand_command("O6"))
|
||
if g20_bridge:
|
||
g20_bridge.set_sdk_command(open_hand_command("G20"))
|
||
else:
|
||
if o6_bridge:
|
||
o6_bridge.set_sdk_command([80.0] * 6)
|
||
if g20_bridge:
|
||
g20_bridge.set_sdk_command([80.0] * 20)
|
||
|
||
try:
|
||
step = 0
|
||
while simulation_app.is_running():
|
||
rclpy.spin_once(ros_node, timeout_sec=0.0)
|
||
if args.motion_test and step % 90 == 0:
|
||
_motion_pose(step // 90)
|
||
if o6_bridge:
|
||
o6_bridge.apply_targets()
|
||
if g20_bridge:
|
||
g20_bridge.apply_targets()
|
||
if step % 6 == 0:
|
||
if o6_bridge:
|
||
o6_bridge.publish_state()
|
||
if g20_bridge:
|
||
g20_bridge.publish_state()
|
||
if args.stability_test and g20_bridge and step >= 360:
|
||
stability_samples.append(g20_bridge._read_sdk_state())
|
||
SimulationManager.step()
|
||
if not use_physics_drive:
|
||
if o6_bridge:
|
||
o6_bridge._pin_root()
|
||
if g20_bridge:
|
||
g20_bridge._pin_root()
|
||
RenderingManager.render()
|
||
simulation_app.update()
|
||
# update() 可能再推物理,渲染前最后再钉一次
|
||
if not use_physics_drive:
|
||
if o6_bridge:
|
||
o6_bridge._pin_root()
|
||
if g20_bridge:
|
||
g20_bridge._pin_root()
|
||
step += 1
|
||
if max_steps > 0 and step >= max_steps:
|
||
if args.stability_test:
|
||
_print_g20_stability_report(stability_samples)
|
||
if args.motion_test:
|
||
for br in (o6_bridge, g20_bridge):
|
||
if br is None:
|
||
continue
|
||
print(
|
||
f"[motion-test] {br.hand}: explode_count={br.explode_count} "
|
||
f"max_root_drift={br.max_root_drift:.4f}m"
|
||
)
|
||
ok = all(
|
||
br is None or br.explode_count == 0
|
||
for br in (o6_bridge, g20_bridge)
|
||
)
|
||
print("[motion-test] RESULT:", "PASS" if ok else "FAIL")
|
||
else:
|
||
ros_node.get_logger().info(f"已完成 {max_steps} 步,退出。")
|
||
break
|
||
except KeyboardInterrupt:
|
||
pass
|
||
finally:
|
||
ros_node.destroy_node()
|
||
rclpy.shutdown()
|
||
app_utils.stop()
|
||
simulation_app.close()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|