Files
mujoco_linkerbot/hands/G20/scripts/run_g20_right_dual_compare.sh

338 lines
12 KiB
Bash
Executable File
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.
#!/usr/bin/env bash
# 同时开两个 MuJoCo:G20 右手标定 vs L20 原版 URDF(未标定)
#
# 标定: 包内 linker_hand_g20_right.xml(启动前从 linkerhand_G20_RIGHT_tag.urdf 仅 patch 限位/侧摆轴;保留 _col 碰撞体与 body quat)
# 未标定: /home/lxqs/linkerhand_L20_V10 (2).1--0728/linkerhand_L20_V10.1_right.urdf → 运行时生成 *_stock.xml
#
# 默认运动学硬写(与 run_dual_mujoco_compare.sh / O6·L6 双开一致):稳定、跟 0~255 真机命令。
# 需要 PD+自碰时手动加 --pd-drive(两窗均加)。
#
# 两个窗口共用 /cb_right_hand_control_cmd(20 维 0~255)。
#
# 用法:
# ./hands/G20/scripts/run_g20_right_dual_compare.sh
# ./hands/G20/scripts/run_g20_right_dual_compare.sh --cmd-topic /cb_right_hand_control_cmd --speed 1.0
# ./hands/G20/scripts/run_g20_right_dual_compare.sh --pd-drive
set -eo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
cd "$ROOT"
PKG_SRC="$ROOT/src/linkerhand-sim/linker_hand_mujoco_ros2/linker_hand_mujoco_ros2"
G20_DIR="$PKG_SRC/urdf/G20/linker_hand_g20_right"
URDF_CALIBRATED="${G20_RIGHT_URDF:-$ROOT/hands/G20/reference/right_calibrated.urdf}"
MESHDIR_CALIB="$G20_DIR/meshes"
URDF_UNCALIBRATED="$PKG_SRC/urdf/L20_V10.1/linker_hand_l20v101_right/linkerhand_L20v10.1_right.urdf"
MESHDIR_UNCAL="$PKG_SRC/urdf/L20_V10.1/linker_hand_l20v101_right/meshes"
CALIB_XML="$G20_DIR/linker_hand_g20_right.xml"
CALIB_URDF_PKG="$G20_DIR/linkerhand_g20_right.urdf"
L20_TEMPLATE="$PKG_SRC/urdf/L20_V10.1/linker_hand_l20v101_left/linker_hand_l20v101_left.xml"
STOCK_XML="$PKG_SRC/urdf/L20_V10.1/linker_hand_l20v101_right/linker_hand_l20v101_right.xml"
if [[ ! -f "$URDF_CALIBRATED" ]]; then
echo "[error] missing calibrated URDF: $URDF_CALIBRATED" >&2
exit 1
fi
if [[ ! -f "$CALIB_XML" ]]; then
echo "[error] missing calibrated MJCF template: $CALIB_XML" >&2
exit 1
fi
if [[ ! -f "$URDF_UNCALIBRATED" ]]; then
echo "[error] missing uncalibrated URDF: $URDF_UNCALIBRATED" >&2
exit 1
fi
if [[ ! -d "$MESHDIR_UNCAL" ]]; then
echo "[error] missing uncalibrated meshes: $MESHDIR_UNCAL" >&2
exit 1
fi
if [[ ! -f "$L20_TEMPLATE" ]]; then
echo "[error] missing L20 V10.1 left MJCF template: $L20_TEMPLATE" >&2
exit 1
fi
set +u
# shellcheck disable=SC1091
source /opt/ros/jazzy/setup.bash
if [[ -f "$ROOT/install/setup.bash" ]]; then
# shellcheck disable=SC1091
source "$ROOT/install/setup.bash"
fi
set -u
VENV_SITE="$ROOT/venv/lib/python3.12/site-packages"
if [[ -d "$VENV_SITE" ]]; then
export PYTHONPATH="$VENV_SITE${PYTHONPATH:+:$PYTHONPATH}"
fi
export PYTHONPATH="$ROOT/src/linkerhand-sim/linker_hand_mujoco_ros2${PYTHONPATH:+:$PYTHONPATH}"
echo "[g20-right-dual] sync calibrated MJCF from G20_RIGHT_tag URDF -> $CALIB_XML"
python3 - "$URDF_CALIBRATED" "$MESHDIR_CALIB" "$CALIB_XML" "$CALIB_URDF_PKG" <<'PY'
import re
import shutil
import sys
from pathlib import Path
import mujoco
urdf_cal = Path(sys.argv[1])
meshdir_cal = Path(sys.argv[2])
calib_xml = Path(sys.argv[3])
calib_urdf_pkg = Path(sys.argv[4])
# 包内留一份 Linkerbot URDF 副本,便于对照
shutil.copy2(urdf_cal, calib_urdf_pkg)
# 只同步限位/轴;不要改 body quat(会破坏 MJCF 运动链,_col 与 visual 错位→对指穿模)
MCP_ROLL_JOINTS = (
"index_mcp_roll",
"middle_mcp_roll",
"ring_mcp_roll",
"pinky_mcp_roll",
)
def parse_urdf(path: Path) -> dict[str, dict]:
text = path.read_text()
joints: dict[str, dict] = {}
for m in re.finditer(r"<joint\b([^>]*)>(.*?)</joint>", text, re.S):
attrs, body = m.group(1), m.group(2)
name_m = re.search(r'name="([^"]+)"', attrs)
if not name_m:
continue
name = name_m.group(1)
if 'revolute' not in attrs:
continue
origin = re.search(r'<origin[^>]*xyz="([^"]+)"[^>]*rpy="([^"]+)"', body)
lim = re.search(r'lower="([^"]+)"[^>]*upper="([^"]+)"', body)
# URDF 常写成 <axis\n xyz="..."/>,不能写死 "axis xyz="
axis = re.search(r'<axis[^>]*xyz="([^"]+)"', body)
rpy = [float(x) for x in origin.group(2).split()] if origin else [0.0, 0.0, 0.0]
joints[name] = {
"lo": float(lim.group(1)),
"hi": float(lim.group(2)),
"rpy": rpy,
"axis": axis.group(1) if axis else "1 0 0",
}
return joints
def apply_urdf_to_mjcf(template: str, joints: dict[str, dict]) -> str:
xml = template
for jname, info in joints.items():
lo, hi = info["lo"], info["hi"]
pat = rf'(<joint name="{re.escape(jname)}"[^>]*\srange=")([^"]+)(")'
xml = re.sub(
pat,
lambda m, lo=lo, hi=hi: f"{m.group(1)}{lo} {hi}{m.group(3)}",
xml,
count=1,
)
act_pat = rf'(<position name="{re.escape(jname)}_pos"[^>]*\sctrlrange=")([^"]+)(")'
xml = re.sub(
act_pat,
lambda m, lo=lo, hi=hi: f"{m.group(1)}{lo} {hi}{m.group(3)}",
xml,
count=1,
)
if jname in MCP_ROLL_JOINTS:
ax = info["axis"].strip()
ax_pat = rf'(<joint name="{re.escape(jname)}"[^>]*\saxis=")([^"]+)(")'
xml = re.sub(ax_pat, lambda m, ax=ax: f"{m.group(1)}{ax}{m.group(3)}", xml, count=1)
return xml
joints = parse_urdf(urdf_cal)
xml = apply_urdf_to_mjcf(calib_xml.read_text(), joints)
calib_xml.write_text(xml)
mujoco.MjModel.from_xml_path(str(calib_xml))
for j in MCP_ROLL_JOINTS:
ax = re.search(rf'<joint name="{j}"[^>]*\saxis="([^"]+)"', calib_xml.read_text())
print(f"[g20-right-dual] synced {j} axis={ax.group(1) if ax else '?'}")
print(f"[g20-right-dual] calibrated URDF: {urdf_cal}")
print(f"[g20-right-dual] calibrated MJCF: {calib_xml} (package template + _col)")
if meshdir_cal.is_dir():
print(f"[g20-right-dual] calibrated meshes: {meshdir_cal} (package _col meshes)")
PY
echo "[g20-right-dual] build uncalibrated L20 V10.1 right MJCF from template + $URDF_UNCALIBRATED"
python3 - "$URDF_UNCALIBRATED" "$MESHDIR_UNCAL" "$L20_TEMPLATE" "$STOCK_XML" <<'PY'
import re
import shutil
import sys
from pathlib import Path
import mujoco
import numpy as np
urdf_path = Path(sys.argv[1])
meshdir = Path(sys.argv[2])
template_path = Path(sys.argv[3])
out_path = Path(sys.argv[4])
def rpy_to_quat(roll, pitch, yaw):
cr, sr = np.cos(roll / 2), np.sin(roll / 2)
cp, sp = np.cos(pitch / 2), np.sin(pitch / 2)
cy, sy = np.cos(yaw / 2), np.sin(yaw / 2)
return np.array([
cr * cp * cy + sr * sp * sy,
sr * cp * cy - cr * sp * sy,
cr * sp * cy + sr * cp * sy,
cr * cp * sy - sr * sp * cy,
])
def parse_joints(path: Path) -> dict:
text = path.read_text()
joints = {}
for m in re.finditer(r"<joint\b([^>]*)>(.*?)</joint>", text, re.S):
attrs, body = m.group(1), m.group(2)
name = re.search(r'name="([^"]+)"', attrs).group(1)
jtype = re.search(r'type="([^"]+)"', attrs).group(1)
if jtype != "revolute":
continue
origin = re.search(r'<origin[^>]*xyz="([^"]+)"[^>]*rpy="([^"]+)"', body)
lim = re.search(r'lower="([^"]+)"[^>]*upper="([^"]+)"', body)
axis = re.search(r'<axis[^>]*xyz="([^"]+)"', body)
joints[name] = {
"xyz": tuple(map(float, origin.group(1).split())),
"rpy": tuple(map(float, origin.group(2).split())),
"lo": float(lim.group(1)),
"hi": float(lim.group(2)),
"axis": tuple(map(float, axis.group(1).split())),
}
if "thumb_dip" in joints and "thumb_ip" not in joints:
joints["thumb_ip"] = dict(joints["thumb_dip"])
return joints
def fmt_axis(ax):
return " ".join(f"{v:g}" for v in ax)
def fmt_range(lo, hi):
return f"{lo:g} {hi:g}"
out_path.parent.mkdir(parents=True, exist_ok=True)
mesh_out = out_path.parent / "meshes"
mesh_out.mkdir(exist_ok=True)
for f in meshdir.glob("*"):
shutil.copy2(f, mesh_out / f.name)
shutil.copy2(urdf_path, out_path.parent / urdf_path.name)
l20 = parse_joints(urdf_path)
xml = template_path.read_text()
xml = xml.replace('model="linker_hand_l20v101_left"', 'model="linker_hand_l20v101_right"')
joint_specs = {n: (j["lo"], j["hi"], j["axis"]) for n, j in l20.items() if n != "thumb_dip"}
for name, (lo, hi, axis) in joint_specs.items():
pat = re.compile(rf'<joint name="{re.escape(name)}"[^>]*>')
m = pat.search(xml)
if not m:
raise SystemExit(f"missing joint {name}")
new = (
f'<joint name="{name}" pos="0 0 0" axis="{fmt_axis(axis)}" '
f'range="{fmt_range(lo, hi)}" actuatorfrcrange="-100 100"/>'
)
xml = xml[: m.start()] + new + xml[m.end() :]
for name, (lo, hi, _) in joint_specs.items():
pat = re.compile(
rf'(<position name="{re.escape(name)}_pos"[^>]*\bctrlrange=")[^"]+(")'
)
if not pat.search(xml):
raise SystemExit(f"missing actuator {name}_pos")
xml = pat.sub(rf"\g<1>{lo:g} {hi:g}\2", xml, count=1)
body_from_joint = {
"thumb_cmc_roll": "thumb_metacarpals_base1",
"thumb_cmc_yaw": "thumb_metacarpals_base2",
"thumb_cmc_pitch": "thumb_metacarpals",
"thumb_mcp": "thumb_proximal",
"thumb_ip": "thumb_distal",
"index_mcp_roll": "index_metacarpals",
"index_mcp_pitch": "index_proximal",
"index_pip": "index_middle",
"index_dip": "index_distal",
"middle_mcp_roll": "middle_metacarpals",
"middle_mcp_pitch": "middle_proximal",
"middle_pip": "middle_middle",
"middle_dip": "middle_distal",
"ring_mcp_roll": "ring_metacarpals",
"ring_mcp_pitch": "ring_proximal",
"ring_pip": "ring_middle",
"ring_dip": "ring_distal",
"pinky_mcp_roll": "pinky_metacarpals",
"pinky_mcp_pitch": "pinky_proximal",
"pinky_pip": "pinky_middle",
"pinky_dip": "pinky_distal",
}
for jname, bname in body_from_joint.items():
j = l20[jname]
xyz = j["xyz"]
rpy = j["rpy"]
q = rpy_to_quat(*rpy)
pos = f"{xyz[0]:g} {xyz[1]:g} {xyz[2]:g}"
pat = re.compile(rf'<body name="{re.escape(bname)}"[^>]*>')
m = pat.search(xml)
if not m:
raise SystemExit(f"missing body {bname}")
if np.allclose(q, [1, 0, 0, 0], atol=1e-9):
new = f'<body name="{bname}" pos="{pos}">'
else:
quat = "{:.6g} {:.6g} {:.6g} {:.6g}".format(*q)
new = f'<body name="{bname}" pos="{pos}" quat="{quat}">'
xml = xml[: m.start()] + new + xml[m.end() :]
out_path.write_text(xml)
mujoco.MjModel.from_xml_path(str(out_path))
print(f"[g20-right-dual] uncalibrated URDF: {urdf_path}")
print(f"[g20-right-dual] uncalibrated meshes: {meshdir} -> {mesh_out}")
print(f"[g20-right-dual] uncalibrated MJCF -> {out_path} (L20v101 template + visual/collision split)")
PY
EXTRA=("$@")
cleanup() {
[[ -n "${PID_G20:-}" ]] && kill "$PID_G20" 2>/dev/null || true
[[ -n "${PID_L20:-}" ]] && kill "$PID_L20" 2>/dev/null || true
}
trap cleanup EXIT INT TERM
echo "[g20-right-dual] G20 calibrated xml=$CALIB_XML"
"$ROOT/hands/G20/scripts/run_g20_right_mujoco_plan.sh" \
--xml "$CALIB_XML" \
--mapping-urdf "$URDF_CALIBRATED" \
--node-name g20_calibrated_right \
--window-title "G20 right 标定 (G20_RIGHT_tag)" \
--cmd-topic /cb_right_hand_control_cmd \
--sim-state-topic /sim/mujoco/g20/right/calibrated/joint_state \
--mapping g20_urdf \
"${EXTRA[@]}" &
PID_G20=$!
sleep 1
echo "[g20-right-dual] L20 stock xml=$STOCK_XML (L20v101 template, URDF-native mapping)"
"$ROOT/hands/G20/scripts/run_g20_right_mujoco_plan.sh" \
--xml "$STOCK_XML" \
--mapping-urdf "$URDF_UNCALIBRATED" \
--node-name l20_stock_right \
--window-title "L20 V10.1 right 未标定" \
--cmd-topic /cb_right_hand_control_cmd \
--sim-state-topic /sim/mujoco/l20/right/stock/joint_state \
--mapping l20_urdf \
"${EXTRA[@]}" &
PID_L20=$!
echo "[g20-right-dual] both started. cmd topic: /cb_right_hand_control_cmd"
echo "[g20-right-dual] mode: kinematic(运动学硬写,与左手双开一致;稳定跟 0~255)"
echo "[g20-right-dual] 标定 mapping=g20_urdf(仅按URDF,不查JSON);未标定 mapping=l20_urdf + collision_project=on"
echo "[g20-right-dual] 需要 PD+自碰: ./hands/G20/scripts/run_g20_right_dual_compare.sh --pd-drive"
echo "[g20-right-dual] Ctrl+C stops both."
wait "$PID_G20" "$PID_L20"