CoolFace
Modelpublic

k-valentin/unitree-g1-mujoco

sourceHugging Faceupdated 1d agoView on Hugging Face
0likes19downloads
Model Card

Unitree G1 MuJoCo model for LeRobot

This fork of lerobot/unitree-g1-mujoco defaults to the 23dof (rev_1_0) body with the Pollen Robotics AmazingHand end effector and a D455 pan/tilt head (one published head camera). The bare 23dof body (END_EFFECTOR: dummy) and the upstream 29dof body with Dex1-1 parallel grippers or Dex3 hands stay selectable, and their URDF, MJCF and meshes remain included. This is the model repository loaded dynamically by LeRobot through env.py.

The existing make_env() entry point, 29 body motor commands, body state DDS topics, simulation stepping and ZMQ image message format are preserved. MuJoCo loads assets/scene_33dof.xml, which includes the gripper MJCF derived from the supplied URDF. The portable URDF is also included at assets/g1_29dof_with_dex1_1.urdf; runtime cameras and actuators are defined in MJCF.

Body variants

config.yaml defaults to BODY: 23dof, END_EFFECTOR: amazing_hand and HEAD: d455_pan_tilt. The 23dof body models the rev10 hardware, which has no waistroll/waistpitch and no wristpitch/wristyaw per arm (6 fewer joints than the 29dof body). It supports END_EFFECTOR: dummy (bare wrists, HEAD: none) or END_EFFECTOR: amazing_hand (see below) -- there is no dex1/dex3 MJCF for the 23dof body.

`BODY`SceneBody motors`mode_machine`
23dof (default)assets/scene_23dof.xml234
29dofsee the end-effector table below292

The DDS LowState_/LowCmd_ messages always carry 35 motor slots. The 23dof body publishes only slots 0-12, 15-19, 22-26; the 6 slots it has no joint for (13, 14, 20, 21, 27, 28, i.e. waistroll/waistpitch and each arm's wristpitch/wristyaw) are left at the SDK default q=dq=tau=0, and any command written to them is ignored. This mapping lives in sim/unitree_sdk2py_bridge.py's JOINT_SLOTS.

To use the 23dof body from LeRobot:

bash
lerobot-record \
  --robot.type=unitree_g1 \
  --robot.is_simulation=true \
  --robot.sim_env_repo_id=k-valentin/unitree-g1-mujoco \
  ...

Direct users of this repository select it with make_env(body="23dof") (or BODY: 23dof in config.yaml).

Choose the end effectors

End effectors are named after the hardware. config.yaml defaults to END_EFFECTOR: amazing_hand (23dof body only). Scene, finger counts, effort limits and the published camera list all follow that selection automatically.

SelectionBodyRuntime sceneActuated jointsCameras
amazing_hand (default)23dofassets/scene_23dof_ah_d455.xml23 body (DDS) + 2 head + 8 fingers per handhead (D455)
dummy23dof or 29dofassets/scene_23dof.xml / assets/scene_29dof.xmlbody onlyhead
dex129dofassets/scene_33dof.xml29 body + 2 fingers per gripperhead + both wrists
dex329dofassets/scene_hands_cameras.xml29 body + 7 joints per handhead + both wrists

dummy is the bare robot with nothing attached to the wrists, so it publishes only head_camera. The earlier names grippers, hands and none are still accepted as aliases for dex1, dex3 and dummy. See "Head + AmazingHand variant" below for amazing_hand.

Direct users of this repository can also call:

python
from env import make_env

if __name__ == "__main__":
    env = make_env(end_effector="dex3")  # Omit the argument for dex1.
    try:
        env.reset()
        while True:
            env.step()  # Body commands continue to arrive over DDS.
    finally:
        env.close()

Head + AmazingHand variant

assets/g1_23dof_ah_d455.xml / assets/scene_23dof_ah_d455.xml extend the bare 23dof body (assets/g1_23dof_no_hand.xml, left unmodified) with a 2-DoF Dynamixel pan/tilt head carrying an Intel RealSense D455, and a Pollen Robotics AmazingHand on each wrist. It is generated by build_g1_ah_model.py (source files stay unchanged; rerun it to regenerate the two files above). config.yaml defaults to BODY: 23dof, END_EFFECTOR: amazing_hand, HEAD: d455_pan_tilt, CAMERAS: ["head_camera"].

  • Head chain: torso_link -> head_servo_link (fixed) -> xl330_link (xl330_joint, pan, range -0.7..0.7 rad) -> d455_link (d455_joint, tilt, range -1.5708..0.8 rad), each a position actuator with kp=5. head_camera sits on d455_link (fovy=42, 480x640). The lab's head meshes have no known license, so all three links are modeled as primitive boxes by default (head_servo_link 20x20x20 mm, xl330_link 20x34x26 mm, d455_link 90x25x25 mm). build_g1_ah_model.py --head-meshes (matching config.yaml's HEAD_MESHES: true) swaps in assets/meshes/head/{head_servo_link,xl330_link, d455_link}.STL instead, but only if all three files exist; none ship by default.
  • Hands: each AmazingHand contributes 8 actuated hinge joints ({left,right}_hand_finger{1..4}_motor{1,2}, matching lerobot.robots.unitree_g1_ah.g1_ah_joints.hand_motor_names), ctrlrange snapped to exactly +-pi/2. Every body/joint/geom/mesh/material/actuator from the source onshape-to-robot export is prefixed left_hand_/right_hand_, including the 24 passive ball/hinge joints per hand that close the finger's parallel four-bar linkage (<equality connect> constraints, carried over renamed). All hand geoms are visual-only (contype="0" conaffinity="0", no collision geoms in the source export). Meshes are copied from AHSimulation/AH_Left|Right/mjcf under assets/amazing_hand/{left,right}/; see THIRD_PARTY_NOTICES.md for the CC-BY-4.0 attribution.
  • Mount transform: the Onshape connector transform from the G1 wrist to the AmazingHand mount is not available yet, so build_g1_ah_model.py uses a placeholder (pos="0.13 0 0", a quaternion rotating the hand's local +z, its finger-reach axis, onto the wrist's local +x, so the hand extends away from the elbow). Replace both the offset and the rotation once the CAD is available.
  • DDS scope: NUM_MOTORS/motor_effort_limit_list stay body-only (23, matching JOINT_SLOTS["23dof"]); the D455 pan/tilt head and the 16 hand actuators are never wired to DDS. sim/base_sim.py's joint scan never adds joints to left_hand_index/right_hand_index when END_EFFECTOR == "amazing_hand" (is_dds_hand is False), so those two lists stay empty and dds_actuator_index covers only the 23 body actuators; the 24 passive linkage joints per hand also carry a left_hand_/right_hand_ prefix but are never actuated, so they would not match this scan even if it ran. mj_data.ctrl for the 18 non-DDS actuators (head + both hands) is left at whatever another writer sets, never overwritten by the body torque loop -- that other writer is the ZMQ bridge described next.
  • Head/hand ZMQ bridge: whenever HEAD != "none" or END_EFFECTOR == "amazing_hand", env.py's make_env() starts sim/head_hand_sim.py's SimHeadHandDevice (a MuJoCo-backed stand-in for lerobot's real HeadHandDevice) behind lerobot.robots.unitree_g1_ah.g1_ah_zmq.HeadHandServer, in a daemon thread bound to 127.0.0.1. Ports default to HEADHAND_STATE_PORT: 6003 / HEADHAND_CMD_PORT: 6002 in config.yaml, overridable with UNITREE_G1_MUJOCO_HEADHAND_STATE_PORT / UNITREE_G1_MUJOCO_HEADHAND_CMD_PORT. A lerobot.robots.unitree_g1_ah.UnitreeG1Ah robot with is_simulation=True talks to this bridge exactly as it would to real Dynamixel/Feetech hardware. The server thread is stopped from env.close().

The original assets/scene_43dof.xml, assets/g1_29dof_with_hand.xml, assets/g1_body29_hand14.urdf and no-hand model are retained unchanged.

Headless / option overrides

LeRobot's make_env(repo_id) cannot forward keyword options, so every make_env option can also be set through the environment as UNITREE_G1_MUJOCO_<OPTION>, e.g. UNITREE_G1_MUJOCO_ONSCREEN=0 (no viewer window), UNITREE_G1_MUJOCO_PUBLISH_IMAGES=0, UNITREE_G1_MUJOCO_CAMERA_PORT=5556, UNITREE_G1_MUJOCO_BODY=29dof, UNITREE_G1_MUJOCO_END_EFFECTOR=dex1, UNITREE_G1_MUJOCO_JOYSTICK_TYPE=xbox. Keyword arguments still win when calling make_env directly. Note that after disconnect() the Python process may not exit on its own because of CycloneDDS finalizers; end the session with Ctrl-C.

Gamepad

The bridge reads a pygame joystick and publishes it as the robot's wireless_remote (JOYSTICK_TYPE in config.yaml, default dualshock4 for a Sony DualShock 4 under SDL's HIDAPI driver; xbox and switch as upstream). Plug the pad in before launching.

Cameras

All three streams are enabled by default on `tcp://127.0.0.1:5555`, with 640 × 480 images and the existing approximately 30 Hz publishing setting.

Stream nameMount
head_cameraExisting head camera
left_wrist_camLeft gripper base / wrist
right_wrist_camRight gripper base / wrist

Each stream is advertised through the same top-level JPEG key and nested images / timestamps entries as head_camera. The existing view_cameras_live.py discovers the names from those messages. They are also listed in env.camera_configs, env.camera_names and env.metadata["cameras"]. The wrist cameras move with their respective wrists, with 95° vertical field of view and the approximate extrinsics from the HIW-500 model builder.

LeRobot's ZMQ cameras require explicit client configuration, just as the head camera does. Pass this dictionary as UnitreeG1Config(..., cameras=cameras):

python
from lerobot.cameras.zmq.configuration_zmq import ZMQCameraConfig

cameras = {
    name: ZMQCameraConfig(
        server_address="127.0.0.1", port=5555, camera_name=name,
        width=640, height=480, fps=30,
    )
    for name in ("head_camera", "left_wrist_cam", "right_wrist_cam")
}

An equivalent camera configuration is in lerobot_cameras.json. make_env(cameras=["head_camera"]) selects a subset; publish_images=False disables publishing. onscreen=False disables the viewer for headless use.

Gripper control

Dex1 fingers use force actuators, driven by the existing bridge's external PD controller. They start open at 0.0245 m. To preserve the simulator's hand transport, the first two motor entries on rt/dex3/left/cmd and rt/dex3/right/cmd command fingers 1 and 2, with matching state topics. Gripper q is in metres and tau is in newtons; each finger is limited to 20 N. Command both fingers to the same position for symmetric opening or closing. Original hand mode retains all seven rotational command entries per side.

The simulation lower limit is -0.023 m, following HIW-500's mesh closure trim. The source and portable URDF retain the official -0.020 m lower limit. Run python build_gripper_model.py --official-limits to use that limit in MJCF too; it leaves approximately 5.88 mm between the supplied finger pads. This model update does not add finger actions to LeRobot's 29-body-motor UnitreeG1 action schema.

Development and checks

Install the existing Unitree SDK2 / CycloneDDS prerequisites, then python -m pip install -r requirements.txt. Regenerate the variants with python build_gripper_model.py.

bash
MUJOCO_GL=egl python -m unittest discover -s tests -v
MUJOCO_GL=egl python tests/smoke_live.py
MUJOCO_GL=egl python tests/smoke_live.py --end-effector dex3

The regression suite uses real MuJoCo for both variants, motor/observation mapping, force-controlled closure, wrist camera motion, rendering and camera publisher shared-memory buffers. It isolates DDS with a test double. smoke_live.py additionally requires the real Gymnasium, Unitree SDK2 and ZMQ/OpenCV dependencies and checks the live environment and transports. See VALIDATION.md for what was executed for this update.

Sources

The original Dex1 source URDF, Apache 2.0 license and attribution notice are in reference/hiw500/. Unitree mesh assets retain their upstream terms.