Add project sources for Mujoco LinkerHand ROS2

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
sunxianghui
2026-07-23 17:55:52 +08:00
parent 346a5c50d6
commit 1647241649
400 changed files with 35707 additions and 0 deletions
+239
View File
@@ -0,0 +1,239 @@
#!/usr/bin/env python3
"""O6 单指位置控制:从最小扫到最大,记录并绘制 角度/速度/力矩。"""
from __future__ import annotations
import argparse
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import mujoco
import mujoco.viewer
import numpy as np
ROOT = Path(__file__).resolve().parents[2]
DEFAULT_XML = (
ROOT
/ "src/linkerhand-sim/linker_hand_mujoco_ros2/linker_hand_mujoco_ros2"
/ "urdf/O6/linker_hand_o6_left/linker_hand_o6_left.xml"
)
# 四指:根节 mcp_pitch + 远端 dip(mimic 0.89)
# ROS 6 维索引:0拇指弯 1拇指横摆 2食指 3中指 4无名指 5小指
FINGER_CFG = {
"index": {
"mcp": "index_mcp_pitch",
"dip": "index_dip",
"mcp_act": "index_mcp_pitch_pos",
"dip_act": "index_dip_pos",
"mimic": 0.89,
"ros_idx": 2,
},
"middle": {
"mcp": "middle_mcp_pitch",
"dip": "middle_dip",
"mcp_act": "middle_mcp_pitch_pos",
"dip_act": "middle_dip_pos",
"mimic": 0.89,
"ros_idx": 3,
},
"ring": {
"mcp": "ring_mcp_pitch",
"dip": "ring_dip",
"mcp_act": "ring_mcp_pitch_pos",
"dip_act": "ring_dip_pos",
"mimic": 0.89,
"ros_idx": 4,
},
"pinky": {
"mcp": "pinky_mcp_pitch",
"dip": "pinky_dip",
"mcp_act": "pinky_mcp_pitch_pos",
"dip_act": "pinky_dip_pos",
"mimic": 0.89,
"ros_idx": 5,
},
}
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--xml", type=Path, default=DEFAULT_XML)
parser.add_argument("--hand", choices=["left", "right"], default="left")
parser.add_argument(
"--finger",
choices=list(FINGER_CFG.keys()),
default="middle",
help="要扫的手指(默认 middle)",
)
parser.add_argument("--duration", type=float, default=8.0, help="单向扫过时长 (s)")
parser.add_argument("--out", type=Path, default=None)
parser.add_argument("--viewer", action="store_true", help="打开 MuJoCo viewer")
args = parser.parse_args()
cfg = FINGER_CFG[args.finger]
if args.out is None:
args.out = ROOT / f"reports/O6_{args.finger}_sweep"
if args.hand == "right":
args.xml = (
ROOT
/ "src/linkerhand-sim/linker_hand_mujoco_ros2/linker_hand_mujoco_ros2"
/ "urdf/O6/linker_hand_o6_right/linker_hand_o6_right.xml"
)
args.out.mkdir(parents=True, exist_ok=True)
model = mujoco.MjModel.from_xml_path(str(args.xml))
data = mujoco.MjData(model)
mcp_jnt = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_JOINT, cfg["mcp"])
dip_jnt = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_JOINT, cfg["dip"])
mcp_act = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_ACTUATOR, cfg["mcp_act"])
dip_act = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_ACTUATOR, cfg["dip_act"])
if min(mcp_jnt, dip_jnt, mcp_act, dip_act) < 0:
raise RuntimeError(f"找不到 {args.finger} joint/actuator,请检查 MJCF")
mcp_qadr = int(model.jnt_qposadr[mcp_jnt])
mcp_dadr = int(model.jnt_dofadr[mcp_jnt])
mcp_lo, mcp_hi = map(float, model.jnt_range[mcp_jnt])
dip_lo, dip_hi = map(float, model.jnt_range[dip_jnt])
mimic = float(cfg["mimic"])
dt = float(model.opt.timestep)
half = args.duration
total_t = 2.0 * half
n_steps = int(total_t / dt)
t_arr = np.arange(n_steps) * dt
def cmd_at(t: float) -> float:
if t <= half:
return mcp_lo + (mcp_hi - mcp_lo) * (t / half)
u = (t - half) / half
return mcp_hi + (mcp_lo - mcp_hi) * u
data.ctrl[:] = 0.0
mujoco.mj_forward(model, data)
logs = {
"t": [],
"cmd_mcp": [],
"q_mcp": [],
"v_mcp": [],
"tau_mcp": [],
"q_dip": [],
"tau_dip": [],
}
viewer = None
if args.viewer:
viewer = mujoco.viewer.launch_passive(model, data)
for i in range(n_steps):
t = t_arr[i]
q_cmd = cmd_at(t)
dip_cmd = float(np.clip(q_cmd * mimic, dip_lo, dip_hi))
data.ctrl[mcp_act] = q_cmd
data.ctrl[dip_act] = dip_cmd
mujoco.mj_step(model, data)
logs["t"].append(t)
logs["cmd_mcp"].append(q_cmd)
logs["q_mcp"].append(float(data.qpos[mcp_qadr]))
logs["v_mcp"].append(float(data.qvel[mcp_dadr]))
logs["tau_mcp"].append(float(data.actuator_force[mcp_act]))
logs["q_dip"].append(float(data.qpos[int(model.jnt_qposadr[dip_jnt])]))
logs["tau_dip"].append(float(data.actuator_force[dip_act]))
if viewer is not None:
viewer.sync()
if viewer is not None:
viewer.close()
for k in logs:
logs[k] = np.asarray(logs[k], dtype=float)
csv_path = args.out / f"{args.finger}_sweep_{args.hand}.csv"
header = "t_s,cmd_mcp_rad,q_mcp_rad,v_mcp_rad_s,tau_mcp_Nm,q_dip_rad,tau_dip_Nm"
np.savetxt(
csv_path,
np.column_stack(
[
logs["t"],
logs["cmd_mcp"],
logs["q_mcp"],
logs["v_mcp"],
logs["tau_mcp"],
logs["q_dip"],
logs["tau_dip"],
]
),
delimiter=",",
header=header,
comments="",
)
fig, axes = plt.subplots(3, 1, figsize=(10, 8), sharex=True)
fig.suptitle(
f"O6 {args.hand} {cfg['mcp']} position sweep "
f"range=[{mcp_lo:.2f}, {mcp_hi:.2f}] rad",
fontsize=13,
)
axes[0].plot(logs["t"], np.rad2deg(logs["cmd_mcp"]), "k--", lw=1.2, label="cmd")
axes[0].plot(logs["t"], np.rad2deg(logs["q_mcp"]), "C0", lw=1.8, label="q")
axes[0].set_ylabel("angle (deg)")
axes[0].legend(loc="best")
axes[0].grid(True, alpha=0.3)
axes[1].plot(logs["t"], np.rad2deg(logs["v_mcp"]), "C1", lw=1.8, label="v")
axes[1].set_ylabel("velocity (deg/s)")
axes[1].legend(loc="best")
axes[1].grid(True, alpha=0.3)
axes[2].plot(logs["t"], logs["tau_mcp"], "C3", lw=1.8, label="tau (actuator_force)")
axes[2].set_ylabel("torque (N·m)")
axes[2].set_xlabel("time (s)")
axes[2].legend(loc="best")
axes[2].grid(True, alpha=0.3)
fig.tight_layout()
png_path = args.out / f"{args.finger}_sweep_{args.hand}_qvt.png"
fig.savefig(png_path, dpi=140)
plt.close(fig)
# ROS 示例:只动该指
pos = [255.0] * 6
pos_open = pos.copy()
pos_close = pos.copy()
pos_close[cfg["ros_idx"]] = 0.0
print("=" * 60)
print(f"finger: {args.finger} joint: {cfg['mcp']}")
print(f"model: {args.xml}")
print(
f"range: [{mcp_lo:.3f}, {mcp_hi:.3f}] rad "
f"= [{np.rad2deg(mcp_lo):.1f}, {np.rad2deg(mcp_hi):.1f}] deg"
)
print(f"mimic: dip = mcp * {mimic}")
print(f"sweep: {mcp_lo:.3f} -> {mcp_hi:.3f} -> {mcp_lo:.3f} total {total_t:.1f}s")
print(f"CSV: {csv_path}")
print(f"plot: {png_path}")
print(
"peak |q|={:.3f} rad |v|={:.3f} rad/s |tau|={:.3f} N·m".format(
np.max(np.abs(logs["q_mcp"])),
np.max(np.abs(logs["v_mcp"])),
np.max(np.abs(logs["tau_mcp"])),
)
)
print("=" * 60)
print(f"ROS 只弯{args.finger}(索引 {cfg['ros_idx']})握紧示例 position:")
print(f" {pos_close}")
print(f"张开示例: {pos_open}")
if __name__ == "__main__":
main()