diff --git a/example/python/stand_go2.py b/example/python/stand_go2.py index ca52195a..91ce650e 100644 --- a/example/python/stand_go2.py +++ b/example/python/stand_go2.py @@ -1,5 +1,6 @@ import time import sys +import platform import numpy as np from unitree_sdk2py.core.channel import ChannelPublisher, ChannelSubscriber @@ -29,7 +30,8 @@ if __name__ == '__main__': if len(sys.argv) <2: - ChannelFactoryInitialize(1, "lo") + interface = "lo0" if platform.system() == "Darwin" else "lo" + ChannelFactoryInitialize(1, interface) else: ChannelFactoryInitialize(0, sys.argv[1]) diff --git a/readme.md b/readme.md index b6c8d9bd..f0871b26 100644 --- a/readme.md +++ b/readme.md @@ -34,6 +34,82 @@ Note: # Installation + +## macOS (Python Simulator only) + +The C++ simulator cannot run natively on macOS because `unitree_sdk2` ships a pre-compiled Linux static library (`libunitree_sdk2.a`). The Python simulator uses all cross-platform dependencies and works natively on macOS. + +### 1. Prerequisites +```bash +brew install python@3.11 cmake +``` + +### 2. Python dependencies +```bash +pip3 install mujoco pygame numpy cyclonedds +``` + +If `pip install cyclonedds` fails, build from source: +```bash +git clone https://github.com/eclipse-cyclonedds/cyclonedds.git +cd cyclonedds && mkdir build && cd build +cmake .. -DCMAKE_INSTALL_PREFIX=$HOME/cyclonedds-install +make -j4 && make install +export CYCLONEDDS_HOME=$HOME/cyclonedds-install +pip3 install cyclonedds +``` + +If `pip install pygame` fails (SDL2 missing): +```bash +brew install sdl2 +pip3 install pygame +``` + +### 3. Install unitree_sdk2_python +```bash +git clone https://github.com/unitreerobotics/unitree_sdk2_python.git +cd unitree_sdk2_python +pip3 install -e . +``` + +### 4. Configure for G1 +In `simulate_python/config.py`, set: +```python +ROBOT = "g1" +USE_JOYSTICK = 0 # Set to 1 if you have a gamepad +``` + +The loopback interface is auto-detected (`lo0` on macOS, `lo` on Linux). + +### 5. Run + +On macOS, MuJoCo requires `mjpython` (installed with the `mujoco` package) instead of `python3` for GUI rendering. A convenience script is provided: +```bash +cd simulate_python +./run_sim.sh +``` +Or manually: +```bash +cd simulate_python +DYLD_LIBRARY_PATH=$(.venv/bin/python3 -c 'import sysconfig; print(sysconfig.get_config_var("LIBDIR"))') \ + ../.venv/bin/mjpython unitree_mujoco.py +``` +You should see the MuJoCo viewer open with the G1 robot loaded. + +In a new terminal, run the G1 test (uses `unitree_hg` IDL): +```bash +cd simulate_python +../.venv/bin/python3 test/test_g1.py +``` + +The program will output the robot's IMU and position data, and each motor will output 0.5Nm torque. + +**Note:** The existing `test/test_unitree_sdk2.py` uses the `unitree_go` IDL (for Go2/B2/H1). For G1, use `test/test_g1.py` which uses the `unitree_hg` IDL. + +--- + +## Linux + ## C++ Simulator (simulate) ### 1. Dependencies diff --git a/simulate_python/config.py b/simulate_python/config.py index 55ed0431..6f7ad0ba 100644 --- a/simulate_python/config.py +++ b/simulate_python/config.py @@ -1,9 +1,11 @@ -ROBOT = "go2" # Robot name, "go2", "b2", "b2w", "h1", "go2w", "g1" +import platform + +ROBOT = "g1" # Robot name, "go2", "b2", "b2w", "h1", "go2w", "g1" ROBOT_SCENE = "../unitree_robots/" + ROBOT + "/scene.xml" # Robot scene DOMAIN_ID = 1 # Domain id -INTERFACE = "lo" # Interface +INTERFACE = "lo0" if platform.system() == "Darwin" else "lo" # Interface (lo0 on macOS, lo on Linux) -USE_JOYSTICK = 1 # Simulate Unitree WirelessController using a gamepad +USE_JOYSTICK = 0 # Simulate Unitree WirelessController using a gamepad JOYSTICK_TYPE = "xbox" # support "xbox" and "switch" gamepad layout JOYSTICK_DEVICE = 0 # Joystick number diff --git a/simulate_python/run_sim.sh b/simulate_python/run_sim.sh new file mode 100755 index 00000000..770572bb --- /dev/null +++ b/simulate_python/run_sim.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Launch the MuJoCo simulator on macOS using mjpython. +# mjpython is required because macOS needs GUI operations on the main thread. +# +# Usage: ./run_sim.sh + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +VENV_DIR="$SCRIPT_DIR/../.venv" + +if [ ! -f "$VENV_DIR/bin/mjpython" ]; then + echo "Error: mjpython not found. Set up the venv first:" + echo " cd $(dirname "$SCRIPT_DIR") && uv venv --python 3.11 .venv && uv pip install mujoco pygame numpy" + exit 1 +fi + +# mjpython needs libpython on DYLD_LIBRARY_PATH when using uv-managed Python +PYTHON_LIBDIR="$("$VENV_DIR/bin/python3" -c 'import sysconfig; print(sysconfig.get_config_var("LIBDIR"))')" +export DYLD_LIBRARY_PATH="$PYTHON_LIBDIR${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" + +cd "$SCRIPT_DIR" +exec "$VENV_DIR/bin/mjpython" unitree_mujoco.py "$@" diff --git a/simulate_python/test/test_g1.py b/simulate_python/test/test_g1.py new file mode 100644 index 00000000..25fe47a0 --- /dev/null +++ b/simulate_python/test/test_g1.py @@ -0,0 +1,62 @@ +import time +import platform +from unitree_sdk2py.core.channel import ChannelFactoryInitialize +from unitree_sdk2py.core.channel import ChannelPublisher, ChannelSubscriber +from unitree_sdk2py.idl.default import unitree_go_msg_dds__SportModeState_ +from unitree_sdk2py.idl.default import unitree_hg_msg_dds__LowState_ +from unitree_sdk2py.idl.default import unitree_hg_msg_dds__LowCmd_ +from unitree_sdk2py.idl.unitree_go.msg.dds_ import SportModeState_ +from unitree_sdk2py.idl.unitree_hg.msg.dds_ import LowState_ +from unitree_sdk2py.idl.unitree_hg.msg.dds_ import LowCmd_ +from unitree_sdk2py.utils.crc import CRC + +NUM_MOTORS = 29 + + +def HighStateHandler(msg: SportModeState_): + print("Position: ", msg.position) + + +def LowStateHandler(msg: LowState_): + print("IMU state: ", msg.imu_state) + + +if __name__ == "__main__": + interface = "lo0" if platform.system() == "Darwin" else "lo" + ChannelFactoryInitialize(1, interface) + + high_state_suber = ChannelSubscriber("rt/sportmodestate", SportModeState_) + low_state_suber = ChannelSubscriber("rt/lowstate", LowState_) + + high_state_suber.Init(HighStateHandler, 10) + low_state_suber.Init(LowStateHandler, 10) + + low_cmd_puber = ChannelPublisher("rt/lowcmd", LowCmd_) + low_cmd_puber.Init() + crc = CRC() + + cmd = unitree_hg_msg_dds__LowCmd_() + cmd.mode_pr = 0 + cmd.mode_machine = 0 + + for i in range(NUM_MOTORS): + cmd.motor_cmd[i].mode = 0x01 # (PMSM) mode + cmd.motor_cmd[i].q = 0.0 + cmd.motor_cmd[i].kp = 0.0 + cmd.motor_cmd[i].dq = 0.0 + cmd.motor_cmd[i].kd = 0.0 + cmd.motor_cmd[i].tau = 0.0 + + while True: + for i in range(NUM_MOTORS): + cmd.motor_cmd[i].q = 0.0 + cmd.motor_cmd[i].kp = 0.0 + cmd.motor_cmd[i].dq = 0.0 + cmd.motor_cmd[i].kd = 0.0 + cmd.motor_cmd[i].tau = 0.5 + + cmd.crc = crc.Crc(cmd) + + # Publish message + low_cmd_puber.Write(cmd) + time.sleep(0.002) diff --git a/simulate_python/test/test_unitree_sdk2.py b/simulate_python/test/test_unitree_sdk2.py index 771cb4b1..0ad2dcf6 100644 --- a/simulate_python/test/test_unitree_sdk2.py +++ b/simulate_python/test/test_unitree_sdk2.py @@ -1,4 +1,5 @@ import time +import platform from unitree_sdk2py.core.channel import ChannelFactoryInitialize from unitree_sdk2py.core.channel import ChannelPublisher, ChannelSubscriber from unitree_sdk2py.idl.default import unitree_go_msg_dds__SportModeState_ @@ -21,7 +22,8 @@ def LowStateHandler(msg: LowState_): if __name__ == "__main__": - ChannelFactoryInitialize(1, "lo") + interface = "lo0" if platform.system() == "Darwin" else "lo" + ChannelFactoryInitialize(1, interface) hight_state_suber = ChannelSubscriber("rt/sportmodestate", SportModeState_) low_state_suber = ChannelSubscriber("rt/lowstate", LowState_) diff --git a/simulate_python/unitree_mujoco.py b/simulate_python/unitree_mujoco.py index 95b934fe..3add1ef3 100755 --- a/simulate_python/unitree_mujoco.py +++ b/simulate_python/unitree_mujoco.py @@ -1,4 +1,7 @@ import time +import sys +import os +import platform import mujoco import mujoco.viewer from threading import Thread @@ -9,6 +12,11 @@ import config +if platform.system() == "Darwin" and "MJPYTHON_BIN" not in os.environ: + print("Error: On macOS, MuJoCo requires 'mjpython' for GUI rendering.") + print("Run with: mjpython unitree_mujoco.py") + sys.exit(1) + locker = threading.Lock() diff --git a/simulate_python/unitree_sdk2py_bridge.py b/simulate_python/unitree_sdk2py_bridge.py index a9ca97ae..800ca913 100644 --- a/simulate_python/unitree_sdk2py_bridge.py +++ b/simulate_python/unitree_sdk2py_bridge.py @@ -300,8 +300,8 @@ def SetupJoystick(self, device_id=0, js_type="xbox"): self.joystick = pygame.joystick.Joystick(device_id) self.joystick.init() else: - print("No gamepad detected.") - sys.exit() + print("Warning: No gamepad detected. Continuing without joystick support.") + return if js_type == "xbox": self.axis_id = {