#!/usr/bin/env python3 """ MOTRIXLAB_UNTRIEE_GO1_SIM2SIM source /opt/mujoco/venv/bin/activate cd /opt/unitree_mujoco python demo/go1_sim2sim_mujoco.py """ import numpy as np import mujoco from mujoco import viewer import os import threading import signal import queue import argparse import time g_exit_requested = False def signal_handler(signum, frame): global g_exit_requested g_exit_requested = True signal.signal(signal.SIGINT, signal_handler) # ============================================================ # 配置 # ============================================================ _PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DEFAULT_ONNX_PATH = os.path.join(_PROJECT_DIR, "exports_go1_flat", "policy.onnx") MOTRIX_XML_DIR = os.path.join(_PROJECT_DIR, "motrix_envs", "src", "motrix_envs", "locomotion", "go1", "xmls") XML_PATH = f"{MOTRIX_XML_DIR}/go1_motor_actuator.xml" TERRAIN = "none" # ============================================================ # MotrixLab 参数 (来自 cfg.py) # ============================================================ NUM_OBS = 45 # 去掉线速度观测 (原来是48) NUM_ACTIONS = 12 OBS_SCALES = {'lin_vel': 2.0, 'ang_vel': 0.25, 'dof_pos': 1.0, 'dof_vel': 0.05} ACTION_SCALE = 0.05 KP = 80.0 KD = 1.0 CLIP_ACTIONS = 23.7 CLIP_OBSERVATIONS = 100.0 MAX_LIN_VEL_X = 1.0 MAX_LIN_VEL_Y = 1.0 MAX_ANG_VEL = 1.0 # 匹配训练时的角速度命令范围 [-1.0, 1.0] # ============================================================ # 关节名称和顺序 # ============================================================ POLICY_JOINT_NAMES = [ "FR_hip", "FR_thigh", "FR_calf", "FL_hip", "FL_thigh", "FL_calf", "RR_hip", "RR_thigh", "RR_calf", "RL_hip", "RL_thigh", "RL_calf", ] DEFAULT_JOINT_ANGLES = np.array([ -0.0, 0.9, -1.8, # FR_hip, FR_thigh, FR_calf 0.0, 0.9, -1.8, # FL_hip, FL_thigh, FL_calf -0.0, 0.9, -1.8, # RR_hip, RR_thigh, RR_calf 0.0, 0.9, -1.8, # RL_hip, RL_thigh, RL_calf ], dtype=np.float32) MUJOCO_TO_POLICY = np.arange(12, dtype=np.int64) POLICY_TO_MUJOCO = np.arange(12, dtype=np.int64) # ============================================================ # 键盘输入 # ============================================================ from pynput import keyboard class KeyboardReader: def __init__(self): self._event_queue = queue.Queue() self.running = True self.shared_keys_held = set() self.shared_one_shot = set() self._reader_thread = None self._listener = None def _normalize_key(self, key): try: if hasattr(key, 'char') and key.char is not None: return key.char.lower() except: pass key_str = str(key) if key_str == 'Key.esc': return 'escape' elif key_str == 'Key.space': return 'space' elif key_str.startswith('Key.'): return key_str.lower() return key_str.lower() def _reader_worker(self): while self.running: try: event_type, key = self._event_queue.get(timeout=0.05) k = self._normalize_key(key) if event_type == 'press': self.shared_keys_held.add(k) self.shared_one_shot.discard(k) elif event_type == 'release': self.shared_keys_held.discard(k) self.shared_one_shot.discard(k) except queue.Empty: pass def init(self): def on_press(key): self._event_queue.put(('press', key)) def on_release(key): self._event_queue.put(('release', key)) try: self._listener = keyboard.Listener(on_press=on_press, on_release=on_release) self._listener.start() self._reader_thread = threading.Thread(target=self._reader_worker, daemon=True) self._reader_thread.start() print("[INFO] 键盘监听已启动") except Exception as e: print(f"[WARN] 无法初始化键盘监听: {e}") def is_key_pressed(self, key): k = self._normalize_key(key) if isinstance(key, str) else self._normalize_key(key) if k not in self.shared_keys_held or k in self.shared_one_shot: return False self.shared_one_shot.add(k) return True def is_key_held(self, key): k = self._normalize_key(key) if isinstance(key, str) else self._normalize_key(key) return k in self.shared_keys_held def restore(self): self.running = False if self._listener: self._listener.stop() # ============================================================ # Sensor 读取 # ============================================================ def get_sensor(model, data, name): sid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SENSOR, name) if sid < 0: return None adr = model.sensor_adr[sid] dim = model.sensor_dim[sid] return data.sensordata[adr:adr + dim].copy() def compute_observations_motrix(model, data, commands, last_actions): """计算 45 维观测 (去掉局部线速度,策略仅靠命令+关节信息+陀螺仪来推理) 布局 (45 dims): [0:3] 陀螺仪 (ang_vel * 0.25) [3:6] 重力向量 (躯干坐标系,无缩放) [6:18] 关节位置偏差 (dof_pos * 1.0) [18:30] 关节速度 (dof_vel * 0.05) [30:42] 上一步动作 (原始值) [42:45] 命令 [vx*2.0, vy*2.0, wz*0.25] """ obs = np.zeros(NUM_OBS, dtype=np.float32) # 陀螺仪 gyro = get_sensor(model, data, "gyro") if gyro is not None: obs[0:3] = gyro * OBS_SCALES['ang_vel'] else: obs[0:3] = data.qvel[3:6] * OBS_SCALES['ang_vel'] # 重力向量 (躯干坐标系) base_rot = data.xmat[1].reshape(3, 3) gravity_world = np.array([0., 0., -1.], dtype=np.float64) local_gravity = base_rot.T @ gravity_world obs[3:6] = local_gravity.astype(np.float32) # 关节位置偏差 joint_pos = data.qpos[7:19] dof_pos_rel = (joint_pos - DEFAULT_JOINT_ANGLES) * OBS_SCALES['dof_pos'] obs[6:18] = dof_pos_rel # 关节速度 joint_vel = data.qvel[6:18] obs[18:30] = joint_vel * OBS_SCALES['dof_vel'] # 上一步动作 obs[30:42] = last_actions # 命令 obs[42:45] = commands * np.array([OBS_SCALES['lin_vel'], OBS_SCALES['lin_vel'], OBS_SCALES['ang_vel']], dtype=np.float32) # 限幅 obs = np.clip(obs, -CLIP_OBSERVATIONS, CLIP_OBSERVATIONS) return obs def main(): import re import onnxruntime as ort parser = argparse.ArgumentParser(description="MotrixLab Go1 Policy Inference in MuJoCo") parser.add_argument("--onnx", type=str, default=DEFAULT_ONNX_PATH) parser.add_argument("--terrain", type=str, default=TERRAIN, choices=["none", "rough", "stairs"]) args = parser.parse_args() os.chdir(MOTRIX_XML_DIR) if args.terrain == "rough": xml_file = f"{MOTRIX_XML_DIR}/scene_rough_terrain.xml" elif args.terrain == "stairs": xml_file = f"{MOTRIX_XML_DIR}/scene_stairs_terrain.xml" else: xml_file = f"{MOTRIX_XML_DIR}/scene_motor_actuator.xml" # flat floor with open(xml_file, 'r') as f: xml_content = f.read() model = mujoco.MjModel.from_xml_string(xml_content) data = mujoco.MjData(model) print(f"[INFO] Model: {model.nbody} bodies, {model.nq} DoF, {model.nu} actuators") print(f"[INFO] MuJoCo timestep: {model.opt.timestep}") print(f"[INFO] 关节顺序 (qpos[7:19]): {[mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_JOINT, i) for i in range(1, 13)]}") for sensor_name in ["gyro", "local_linvel"]: sid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SENSOR, sensor_name) print(f"[SENSOR] {sensor_name}: {'存在' if sid >= 0 else '不存在'}") # 初始化 data.qpos[0:3] = np.array([0.0, 0.0, 0.42]) data.qpos[3:7] = np.array([1.0, 0.0, 0.0, 0.0]) # 默认四元数 data.qpos[7:19] = DEFAULT_JOINT_ANGLES data.qvel[:] = 0.0 data.ctrl[:] = 0.0 mujoco.mj_forward(model, data) print(f"[INIT] qpos[2]={data.qpos[2]:.3f}") print(f"[INIT] qpos[7:19]={data.qpos[7:19]}") print(f"[INIT] DEFAULT_JOINT_ANGLES={DEFAULT_JOINT_ANGLES}") # 加载onnx session = ort.InferenceSession(args.onnx, providers=['CPUExecutionProvider']) print(f"[INFO] loaded") # 主循环 ctrl_dt = 0.01 # 100Hz num_steps_per_inference = int(ctrl_dt / model.opt.timestep) print(f"[INFO] 每 {num_steps_per_inference} 步推理一次 ") step_count = 0 inference_step = 0 x_vel_cmd = 0.0 y_vel_cmd = 0.0 yaw_vel_cmd = 0.0 commands = np.array([x_vel_cmd, y_vel_cmd, yaw_vel_cmd], dtype=np.float32) last_actions = np.zeros(NUM_ACTIONS, dtype=np.float32) action = np.zeros(NUM_ACTIONS, dtype=np.float32) # 键盘 keyboard_reader = KeyboardReader() keyboard_reader.init() view = viewer.launch_passive(model, data) print("[INFO] 已启动!") loop_start_time = time.time() while view.is_running() and not g_exit_requested: # 键盘命令 if keyboard_reader.is_key_pressed(' '): x_vel_cmd = 0.0 y_vel_cmd = 0.0 yaw_vel_cmd = 0.0 if keyboard_reader.is_key_held('w'): x_vel_cmd = MAX_LIN_VEL_X elif keyboard_reader.is_key_held('s'): x_vel_cmd = -MAX_LIN_VEL_X else: x_vel_cmd = 0.0 if keyboard_reader.is_key_held('q'): y_vel_cmd = MAX_LIN_VEL_Y elif keyboard_reader.is_key_held('e'): y_vel_cmd = -MAX_LIN_VEL_Y else: y_vel_cmd = 0.0 if keyboard_reader.is_key_held('a'): yaw_vel_cmd = MAX_ANG_VEL elif keyboard_reader.is_key_held('d'): yaw_vel_cmd = -MAX_ANG_VEL else: yaw_vel_cmd = 0.0 if keyboard_reader.is_key_pressed('r'): # 重置机器人到初始位置 data.qpos[0:3] = np.array([0.0, 0.0, 0.42]) data.qpos[3:7] = np.array([1.0, 0.0, 0.0, 0.0]) data.qpos[7:19] = DEFAULT_JOINT_ANGLES data.qvel[:] = 0.0 data.ctrl[:] = 0.0 last_actions = np.zeros(NUM_ACTIONS, dtype=np.float32) mujoco.mj_forward(model, data) print("[RESET] 机器人已重置") if keyboard_reader.is_key_pressed('escape'): break # 推理 (每 N 步一次) if inference_step == 0: commands[0] = x_vel_cmd commands[1] = y_vel_cmd commands[2] = yaw_vel_cmd obs = compute_observations_motrix(model, data, commands, last_actions) # 推理 action = session.run(None, {'observations': obs.reshape(1, -1).astype(np.float32)})[0][0] action = np.clip(action, -CLIP_ACTIONS, CLIP_ACTIONS) last_actions = action.copy() # PD 控制 # joint_targets = action * action_scale + default_angles joint_targets = DEFAULT_JOINT_ANGLES + action * ACTION_SCALE current_pos = data.qpos[7:19] current_vel = data.qvel[6:18] torques = KP * (joint_targets - current_pos) - KD * current_vel torques = np.clip(torques, -CLIP_ACTIONS, CLIP_ACTIONS) data.ctrl[:] = torques mujoco.mj_step(model, data) view.sync() expected_time = step_count * ctrl_dt elapsed = time.time() - loop_start_time sleep_time = expected_time - elapsed if sleep_time > 0: time.sleep(sleep_time) step_count += 1 inference_step = (inference_step + 1) % num_steps_per_inference if step_count % 200 == 0: trunk_z = data.qpos[2] lin_vel = np.linalg.norm(data.qvel[0:3]) print(f"\n========== Step {step_count} ==========") print(f"[CMD] x={x_vel_cmd:.2f}, y={y_vel_cmd:.2f}, yaw={yaw_vel_cmd:.2f}") print(f"[OBS] gyro={obs[0:3]}, grav={obs[3:6]}") print(f"[ACTION] raw={action[:4]}... scaled={action[:4]*ACTION_SCALE}...") print(f"[TARGET] {joint_targets[:4]}...") print(f"[TORQUE] {torques[:4]}...") print(f"[STATE] z={trunk_z:.3f}m, vel={lin_vel:.3f}m/s") print(f"==========================================\n") keyboard_reader.restore() view.close() if __name__ == "__main__": main()