#!/usr/bin/env python3 """O6 中指 step 实验:真机 / 仿真共用同一套控制时序。 时序(默认,两边完全一致): 3s 张开 → 3s 握紧 → 3s 张开 → 2s 余量录制 用法: export ROS_DOMAIN_ID=31 # 真机(需 can0 UP + SDK,或脚本尝试自动启 SDK) python3 tools/run_middle_step_experiment.py real # 仿真(自带 MuJoCo,无需 run_sim.sh) python3 tools/run_middle_step_experiment.py sim # 两次跑完后出对比图(自动找目录下最新 real/sim csv) python3 tools/run_middle_step_experiment.py compare # 指定 csv python3 tools/run_middle_step_experiment.py compare \\ --real-csv reports/O6_middle_ros/ros_real_....csv \\ --sim-csv reports/O6_middle_ros/ros_sim_....csv """ from __future__ import annotations import argparse import glob import os import signal import subprocess import sys import threading import time from pathlib import Path ROOT = Path(__file__).resolve().parents[1] OUT_DIR = ROOT / "outputs/reports/O6/O6_middle_ros" # 与 middle_step_cmd_pub.py 保持一致 PRE_WAIT = 3.0 HOLD = 3.0 RECORD_AFTER = 2.0 RATE_HZ = 10.0 def record_duration() -> float: return PRE_WAIT + HOLD + HOLD + RECORD_AFTER + 1.5 def setup_ros_env(): os.environ.setdefault("ROS_DOMAIN_ID", "31") os.environ.setdefault("ROS_LOCALHOST_ONLY", "0") def setup_mujoco_pythonpath(): venv_site = ROOT / "venv/lib/python3.12/site-packages" if venv_site.is_dir(): os.environ["PYTHONPATH"] = f"{venv_site}{os.pathsep}{os.environ.get('PYTHONPATH', '')}" def run_real_experiment(args): setup_ros_env() OUT_DIR.mkdir(parents=True, exist_ok=True) dur = record_duration() rec_cmd = [ sys.executable, str(ROOT / "tools/ros_real_record_qvt.py"), "--hand", args.hand, "--finger", args.finger, "--label", "ros_real", "--out", str(OUT_DIR), "--duration", str(dur), ] print(f"[real] 启动录制 {dur:.1f}s ...") rec = subprocess.Popen(rec_cmd, cwd=str(ROOT)) time.sleep(1.0) pub_cmd = [ sys.executable, str(ROOT / "tools/middle_step_cmd_pub.py"), "--hand", args.hand, "--finger", args.finger, "--pre-wait", str(PRE_WAIT), "--hold", str(HOLD), "--rate", str(RATE_HZ), ] print("[real] 发送统一 step 指令 ...") pub = subprocess.run(pub_cmd, cwd=str(ROOT)) if pub.returncode != 0: rec.send_signal(signal.SIGINT) rec.wait() return pub.returncode print("[real] 等待录制结束 ...") try: rec.wait(timeout=dur + 5) except subprocess.TimeoutExpired: rec.send_signal(signal.SIGINT) rec.wait() return rec.returncode def run_sim_experiment(args): setup_ros_env() setup_mujoco_pythonpath() OUT_DIR.mkdir(parents=True, exist_ok=True) dur = record_duration() rec_cmd = [ sys.executable, str(ROOT / "tools/ros_mujoco_record_qvt.py"), "--hand", args.hand, "--finger", args.finger, "--label", "ros_sim", "--out", str(OUT_DIR), "--record-duration", str(dur), ] if args.viewer: rec_cmd.append("--viewer") print(f"[sim] 启动 MuJoCo 录制 {dur:.1f}s ...") rec = subprocess.Popen(rec_cmd, cwd=str(ROOT)) time.sleep(1.0) pub_cmd = [ sys.executable, str(ROOT / "tools/middle_step_cmd_pub.py"), "--hand", args.hand, "--finger", args.finger, "--pre-wait", str(PRE_WAIT), "--hold", str(HOLD), "--rate", str(RATE_HZ), ] print("[sim] 发送统一 step 指令 ...") pub = subprocess.run(pub_cmd, cwd=str(ROOT)) if pub.returncode != 0: rec.send_signal(signal.SIGINT) rec.wait() return pub.returncode print("[sim] 等待录制结束 ...") try: rec.wait(timeout=dur + 10) except subprocess.TimeoutExpired: rec.send_signal(signal.SIGINT) rec.wait() return rec.returncode def latest_csv(pattern: str) -> Path | None: files = sorted(glob.glob(str(OUT_DIR / pattern)), key=os.path.getmtime) return Path(files[-1]) if files else None def run_compare(args): real_csv = Path(args.real_csv) if args.real_csv else latest_csv("ros_real_*middle*.csv") sim_csv = Path(args.sim_csv) if args.sim_csv else latest_csv("ros_sim_*middle*.csv") if not real_csv or not real_csv.is_file(): print("[error] 找不到真机 csv,先跑: python3 tools/run_middle_step_experiment.py real") return 1 if not sim_csv or not sim_csv.is_file(): print("[error] 找不到仿真 csv,先跑: python3 tools/run_middle_step_experiment.py sim") return 1 out = OUT_DIR / "ros_real_vs_sim_middle_left_compare_qvt.png" cmd = [ sys.executable, str(ROOT / "tools/plot_real_sim_compare.py"), str(real_csv), str(sim_csv), "-o", str(out), ] print(f"[compare] real={real_csv.name}") print(f"[compare] sim ={sim_csv.name}") return subprocess.call(cmd, cwd=str(ROOT)) def main(): parser = argparse.ArgumentParser(description="O6 middle step experiment (real/sim/compare)") parser.add_argument( "mode", choices=["real", "sim", "compare"], help="real=真机录制 | sim=MuJoCo录制 | compare=对比图", ) parser.add_argument("--hand", default="left") parser.add_argument("--finger", default="middle") parser.add_argument("--viewer", action="store_true", help="sim 模式打开 MuJoCo 窗口") parser.add_argument("--real-csv", default="") parser.add_argument("--sim-csv", default="") args = parser.parse_args() print("=" * 60) print("O6 middle step 实验时序(真机 / 仿真相同)") print(f" pre_wait={PRE_WAIT}s hold={HOLD}s record_after={RECORD_AFTER}s") print(f" ROS_DOMAIN_ID={os.environ.get('ROS_DOMAIN_ID', '31')}") print("=" * 60) if args.mode == "real": print("\n[提示] 真机请先: sudo ip link set can0 up type can bitrate 1000000") print(" 并运行 ./tools/hardware/run_real_hand.sh(或本脚本前自行启动 SDK)\n") return run_real_experiment(args) or 0 if args.mode == "sim": return run_sim_experiment(args) or 0 return run_compare(args) if __name__ == "__main__": raise SystemExit(main())