#!/usr/bin/env bash # MuJoCo O30i 右手单窗仿真(V2_0819) # 默认使用 V10 纯 URDF:保留对指锚点,调整新零位,保持线性换算。 # O30_RIGHT_URDF 可覆盖;启动姿态对应第五张照片的零位数组。 # 订阅 /cb_right_hand_control_cmd(20 维 SDK 0~255,顺序见 linkerhand-o30-ros2 JOINT_MAP) # 发布 /sim/mujoco/o30/right/joint_state # # 用法: # ./hands/O30/scripts/run_o30_right_mujoco_plan.sh # ./hands/O30/scripts/run_o30_right_mujoco_plan.sh --pd-drive # O30_RIGHT_PREPARE_ONLY=1 ./hands/O30/scripts/run_o30_right_mujoco_plan.sh # 只生成/校验模型 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" O30_DIR="$PKG_SRC/urdf/O30/linker_hand_o30_right" URDF_SRC="${O30_RIGHT_URDF:-$O30_DIR/linkerhand_o30_right_zero_v10_calibrated.urdf}" MESH_SRC="${O30_RIGHT_MESHDIR:-$O30_DIR/meshes}" XML="$O30_DIR/linker_hand_o30_right.xml" URDF_PKG="$O30_DIR/linkerhand_o30_right.urdf" if [[ ! -f "$URDF_SRC" ]]; then echo "[error] missing O30 right URDF: $URDF_SRC" >&2 exit 1 fi if [[ ! -d "$MESH_SRC" ]]; then echo "[error] missing O30 right meshes: $MESH_SRC" >&2 exit 1 fi 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 "[o30-right] sync MJCF from $URDF_SRC" python3 - "$URDF_SRC" "$MESH_SRC" "$O30_DIR" "$XML" "$URDF_PKG" <<'PY' import re import shutil import sys import xml.etree.ElementTree as ET from pathlib import Path import mujoco urdf_src = Path(sys.argv[1]) mesh_src = Path(sys.argv[2]) out_dir = Path(sys.argv[3]) xml_path = Path(sys.argv[4]) urdf_pkg = Path(sys.argv[5]) out_dir.mkdir(parents=True, exist_ok=True) mesh_out = out_dir / "meshes" mesh_out.mkdir(exist_ok=True) for f in mesh_src.iterdir(): if f.is_file() and f.resolve() != (mesh_out / f.name).resolve(): shutil.copy2(f, mesh_out / f.name) text = urdf_src.read_text() text = re.sub(r"package://[^/]+/meshes/", "meshes/", text) urdf_pkg.write_text(text) def attrs_of(chunk: str) -> dict[str, str]: return dict(re.findall(r'(\w+)="([^"]*)"', chunk)) def parse_links(urdf: str) -> dict[str, dict]: links: dict[str, dict] = {} for m in re.finditer(r']*>(.*?)', urdf, re.S): name, body = m.group(1), m.group(2) inertial = re.search(r"(.*?)", body, re.S) pos = "0 0 0" mass = "0.01" fullinertia = "1e-6 1e-6 1e-6 0 0 0" if inertial: ib = inertial.group(1) origin = re.search(r"", ib) mass_m = re.search(r"", ib) inertia = re.search(r"", ib) if origin: pos = attrs_of(origin.group(1)).get("xyz", pos) if mass_m: mass = attrs_of(mass_m.group(1)).get("value", mass) if inertia: ia = attrs_of(inertia.group(1)) fullinertia = " ".join( ia.get(k, "0") for k in ("ixx", "iyy", "izz", "ixy", "ixz", "iyz") ) color = "0.7 0.7 0.7 1" vis = re.search(r"(.*?)", body, re.S) if vis: col = re.search(r'rgba="([^"]+)"', vis.group(1)) if col: color = col.group(1) mesh = None mesh_scale = None mesh_m = re.search(r"]*)/>", body) if mesh_m: ma = attrs_of(mesh_m.group(1)) fn = ma.get("filename", "") mesh = Path(fn).name mesh_scale = ma.get("scale") links[name] = { "pos": pos, "mass": mass, "fullinertia": fullinertia, "color": color, "mesh": mesh, "mesh_scale": mesh_scale, } return links def parse_joints(urdf: str) -> list[dict]: joints = [] for m in re.finditer(r"]*)>(.*?)", urdf, re.S): attrs, body = m.group(1), m.group(2) name = re.search(r'name="([^"]+)"', attrs) jtype = re.search(r'type="([^"]+)"', attrs) if not name or not jtype or jtype.group(1) != "revolute": continue origin = re.search(r"", body) axis = re.search(r"", body) lim = re.search(r"", body) parent = re.search(r']*link="([^"]+)"', body) child = re.search(r']*link="([^"]+)"', body) oa = attrs_of(origin.group(1)) if origin else {} la = attrs_of(lim.group(1)) if lim else {} joints.append( { "name": name.group(1), "xyz": oa.get("xyz", "0 0 0"), "rpy": oa.get("rpy", "0 0 0"), "axis": attrs_of(axis.group(1)).get("xyz", "0 0 1") if axis else "0 0 1", "lo": float(la.get("lower", "0")), "hi": float(la.get("upper", "0")), "parent": parent.group(1) if parent else "", "child": child.group(1) if child else "", } ) return joints def rpy_to_quat(rpy: str) -> str | None: import math r, p, y = [float(x) for x in rpy.split()] if abs(r) + abs(p) + abs(y) < 1e-12: return None cr, sr = math.cos(r / 2), math.sin(r / 2) cp, sp = math.cos(p / 2), math.sin(p / 2) cy, sy = math.cos(y / 2), math.sin(y / 2) w = cr * cp * cy + sr * sp * sy x = sr * cp * cy - cr * sp * sy yq = cr * sp * cy + sr * cp * sy z = cr * cp * sy - sr * sp * cy return f"{w:.12g} {x:.12g} {yq:.12g} {z:.12g}" links = parse_links(text) joints = parse_joints(text) by_parent: dict[str, list[dict]] = {} for j in joints: by_parent.setdefault(j["parent"], []).append(j) root = "hand_base_link" # mesh name -> scale (from first link that uses it) mesh_scale: dict[str, str | None] = {} for info in links.values(): if info["mesh"] and info["mesh"] not in mesh_scale: mesh_scale[info["mesh"]] = info["mesh_scale"] meshes = sorted(mesh_scale.keys()) lines = [ '', ' ', ' ") xml_path.write_text("\n".join(lines) + "\n") model = mujoco.MjModel.from_xml_path(str(xml_path)) from linker_hand_mujoco_ros2.utils.o30_right_joints import configure_o30_right_limits configure_o30_right_limits(xml_path) print(f"[o30-right] wrote {xml_path} njnt={model.njnt} nbody={model.nbody} ngeom={model.ngeom}") PY if [[ "${O30_RIGHT_PREPARE_ONLY:-0}" == "1" ]]; then exit 0 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 # Keep this checkout ahead of installed copies after sourcing ROS workspaces. export PYTHONPATH="$ROOT/src/linkerhand-sim/linker_hand_mujoco_ros2${PYTHONPATH:+:$PYTHONPATH}" echo "[o30-right] urdf=$URDF_PKG" echo "[o30-right] xml=$XML" echo "[o30-right] cmd-topic=/cb_right_hand_control_cmd (20ch SDK 0~255, JOINT_MAP 顺序)" echo "[o30-right] state-topic=/sim/mujoco/o30/right/joint_state" echo "[o30-right] order: thumb_roll,yaw, idx/mid/ring/little_yaw, thumb/idx/mid/ring/little_root1, idx/mid/ring/little_root2, thumb/idx/mid/ring/little_tip" echo "[o30-right] default mode=kinematic; 加 --pd-drive 看接触" exec python3 -m linker_hand_mujoco_ros2.o30_right_plan_bridge \ --xml "$XML" \ --node-name o30_mujoco_plan_bridge_right \ --window-title "O30i right V10 zero calibrated" \ --cmd-topic /cb_right_hand_control_cmd \ --sim-state-topic /sim/mujoco/o30/right/joint_state \ --mapping g20_urdf \ "$@"