English | 中文
Distribution. The primary public distribution point for this repository is GitHub: https://github.com/limxdynamics/tron2_mujoco_sim. The internal LimX GitLab is a mirror; open issues, PRs, and security reports on the GitHub repository.
MuJoCo simulator for the TRON2 robot family. It bridges the LimX low-level SDK
(RobotCmd / RobotState carrying q / dq / tau / Kp / Kd, plus
ImuData and gripper messages) to mujoco.MjData, so the same controller wire
format that drives a physical robot can be exercised against a simulated one.
A robot variant is declared, not hard-coded: it is a set of joint channels plus
optional modules, assembled in tron2_sim/variants/. Every channel of every
supported variant runs over the SDK's *ForSim simulator-side API — there is no
second messaging stack.
Apache License, Version 2.0. See LICENSE; SPDX identifier
Apache-2.0.
NOTICE— required attribution notice.THIRD_PARTY_NOTICES.md— per-submodule and per-dependency provenance.SECURITY.md— how to report a vulnerability, and the simulation-versus-hardware boundary.CONTRIBUTING.md— development workflow, submodule procedure, DCO sign-off.CHANGELOG.md— release notes and items blocked on upstream.
Included: simulator.py, the tron2_sim/ package (including the shipped
gripper calibration under tron2_sim/config/), and documentation. Submodules
are declared (pinned by commit) but not vendored:
robot-description/— URDF / MJCF models and meshes.robot-joystick/— gamepad helper binary used to drive a controller by hand (see §3a Gamepad control).limxsdk-lowlevel/— LimX low-level SDK with pre-built wheels.
Excluded by design: trained control policies (.onnx, .pt, .pth,
.ckpt), SDK binaries or wheels committed into this tree, calibration values,
firmware, bag captures, and hard-coded private network addresses. Command
examples use the placeholder <robot-ip>; the default endpoint is 127.0.0.1.
| Dependency | Version | Notes |
|---|---|---|
| Python | >= 3.10 | verified on 3.10 |
mujoco |
>= 3.2.2 | physics and passive viewer |
PyYAML |
>= 6.0 | reads tron2_sim/config/gripper_config.yaml |
limxsdk |
4.8+ | architecture-specific wheel inside the limxsdk-lowlevel submodule; installed separately, always with --no-deps |
Clone first, either way:
git clone --recurse-submodules https://github.com/limxdynamics/tron2_mujoco_sim.git
cd tron2-mujoco-simIf you already cloned without submodules, run
git submodule update --init --recursive before continuing.
uv installs the right Python, resolves everything
from the committed uv.lock, and needs no manual virtualenv:
curl -LsSf https://astral.sh/uv/install.sh | sh # install uv
uv sync --extra sdk # physics + SDK
export ROBOT_TYPE=SF_TRON2A
uv run simulator.py # with viewer
uv run simulator.py --headless # no graphics (CI / remote)
uv run simulator.py --headless --duration 30 # exit after 30 s
uv run simulator.py --headless --duration 30 --no-grasper # DACH without the 2F gripperuv sync requires the limxsdk-lowlevel submodule to be checked out, since
that is where the SDK wheel lives. A later plain uv sync (without
--extra sdk) removes limxsdk again.
python3 -m venv .venv && source .venv/bin/activate
pip install -U pip
pip install "mujoco>=3.2.2" "pyyaml>=6.0"
# SDK wheel matching your architecture. --no-deps is required: the wheel's own
# metadata pulls in onnxruntime/pygame/scipy/pandas and pins numpy<1.26.4.
pip install --no-deps limxsdk-lowlevel/python3/amd64/limxsdk-*.whl # x86_64
# pip install --no-deps limxsdk-lowlevel/python3/aarch64/limxsdk-*.whl # aarch64
export ROBOT_TYPE=SF_TRON2A
python3 simulator.py # with viewer
python3 simulator.py --headless --duration 30| Variable | Default | Effect |
|---|---|---|
ROBOT_TYPE |
(required) | Selects the variant, e.g. SF_TRON2A. |
ROBOT_IP |
127.0.0.1 |
SDK endpoint. |
GRIPPER_CONFIG |
tron2_sim/config/gripper_config.yaml |
Loads a different DACH gripper calibration file. |
DACH_GRASPER |
1 |
0 loads robot.xml instead of robot_grasper.xml. Same as --no-grasper. |
Ten types: five base names in a TRON2A and a TRON2B variant. Joint names and
wire order are identical across the two families, so a controller needs no
changes to talk to either.
| Base | TRON2A | TRON2B | Description |
|---|---|---|---|
| SF | SF_TRON2A |
SF_TRON2B |
Sole-foot biped (10 joints) |
| WF | WF_TRON2A |
WF_TRON2B |
Wheel-foot biped (10 joints) |
| DA | DA_TRON2A |
DA_TRON2B |
Dual arm (14 joints) |
| DACH | DACH_TRON2A |
DACH_TRON2B |
Dual arm + 2-DOF head (16 joints), optional 2F gripper |
| DASF | DASF_TRON2A |
DASF_TRON2B |
Humanoid composite: SF legs + DACH arms/head (26 joints) |
- Space — pause / resume. Paused means manual mode: the Control sliders on the right pose joints directly. (The DACH 2F gripper idles in manual mode and tracks neither sliders nor commands.)
- R — reset the floating-base pose only; joint angles are kept. Fixed-base models print a notice and ignore it.
- Backspace / Reset button — full reset (using the selected keyframe, when one is selected).
- Double-click to select, then Ctrl+drag — apply a perturbation force.
For the controller-side gamepad workflow (SF/WF variants running the
sibling tron2-rl-deploy-python deployment stack), initialize the
robot-joystick submodule and run the helper binary alongside the
controller:
./robot-joystick/robot-joystickDefault bindings:
L1 + Y— switch to WALK.L1 + X— switch back to IDLE.R1— clear velocity commands.
Wire-order contract (cmd/state index order, identical on TRON2A and TRON2B): the DACH head is pitch then yaw; the DASF head is yaw then pitch. Where a model's XML declares actuators in a different order, the simulator absorbs it by resolving joints by name — never by position.
Behavioural notes:
- Commands on Centaur channels (DASF) should carry joint names
(
RobotCmd.motor_names). The native layer validates them against the published state and rejects mismatches withERROR: Centaur ... RobotCmd does not match the corresponding RobotState. The simulator always publishes state with real joint names. - DACH 2F gripper calibration lives in
tron2_sim/config/gripper_config.yaml. - On DACH grasper models,
grasper_base_{L,R}_Joint_ctrlare driven by the linkage rather than by a channel, so theirctrlstays 0 and the simulator prints anINFO: unowned actuatorsline at startup. This is expected.
simulator.py entry point: ROBOT_TYPE -> variant registry -> SimCore
tron2_sim/
spec.py wire order + variant specs (pure data)
core.py physics/render dual-thread, dual-MjData snapshots,
manual mode, resets
channels.py JointChannel: name-based joint map, MIT control law,
state/IMU publishing
transports.py SdkBus (limxsdk *ForSim) and the capability guard
config/ YAML loading + the shipped gripper_config.yaml
modules/ optional capabilities; dach_grasper (2F linkage gripper)
variants/ one file per base name; __init__.py is the registry
doc/gif/ demo animations (see the gallery below)
Threading: the physics thread owns the physics MjData; the render thread owns
render_data. UI resets and drag forces are handed over through flags and
buffers, so MjData always has exactly one writer.
Joint names, ordering and topics are byte-identical, so controller code needs no changes. But 2B is a hardware revision: policies and gains tuned for 2A are not directly transferable.
| Difference | TRON2A | TRON2B |
|---|---|---|
| Hip pitch/roll and knee torque | ±150 N·m | ±200 N·m (armature updated to match) |
| Hip yaw, ankle pitch, elbow torque | ±60 / ±70 N·m | ±70 N·m |
| Wrist and head torque | ±20 N·m | ±15 N·m |
base_Link mass (SF/WF/DA/DACH) |
12.57 kg | 13.4 kg (CoM and inertia updated) |
| SF knee / hip-yaw limits | knee: [-2.618, 0.262] |
sign-flipped knee: [-0.262, 2.618], link geometry mirrored |
| DA / DACH base | floating (near-rigid freejoint) | fixed (no freejoint; R ignores the reset with a notice) |
Recorded from this simulator, one per variant.
| Variant | Demo |
|---|---|
SF_TRON2A |
![]() |
WF_TRON2A |
![]() |
DACH_TRON2A |
![]() |
DASF_TRON2A |
![]() |
See doc/gif/README.md for the media rules CI enforces.
Error: Please set the ROBOT_TYPE ... — export ROBOT_TYPE first; the
message lists every supported value.
uv sync fails on a missing limxsdk-*.whl — the limxsdk-lowlevel
submodule is not checked out. Run git submodule update --init --recursive.
limxsdk ... does not support the Centaur simulator side — the installed
SDK predates the Centaur API, so DASF_* cannot run. Update the
limxsdk-lowlevel submodule to a release that exposes RobotType.Centaur and
the LowerBody/UpperBody *ForSim methods.
Error: none of ['robot.xml'] exists under ... — the robot-description
submodule is missing, or that variant's assets are not published yet.
No module named limxsdk — the environment was created without the extra.
Run uv sync --extra sdk. Note that a later plain uv sync removes it again.
On a pip install, the wheel was not installed, or it was installed into a
different interpreter than the one running simulator.py.
python3 -m venv fails with an ensurepip error — on Debian and Ubuntu the
standard library venv module is packaged separately: sudo apt install python3-venv. Or use option A, which needs no system package.
The robot loads but never moves — no controller is publishing, or (on
DASF_*) its commands lack motor_names and the native layer is rejecting
them.
Simulator output disappears when piped — fixed; if you see it on an older
checkout, set PYTHONUNBUFFERED=1.



