LeRobot compatibility
How SO101-Nexus stays aligned with the LeRobot ecosystem.
SO101-Nexus treats the LeRobot ecosystem as a first-class peer. When LeRobot ships an abstraction, we use it directly rather than reimplementing one of our own.
What this means in practice
- Processor pipelines. Teleop conversions and optional env-observation transforms are implemented as
lerobot.processor.DataProcessorPipelineinstances composed of steps that subclassActionProcessorStep,ObservationProcessorStep, and friends. Every custom step is registered withProcessorStepRegistry, which means pipelines are serializable viasave_pretrainedand shareable on the Hugging Face Hub. - Observation conventions. When you opt into the wrapper, observations follow LeRobot's canonical keys:
observation.statefor the proprioceptive state vector andobservation.images.<name>for camera frames in CHW float32 form. - Hardware drivers. SO leader and follower configs come straight from
lerobot.teleoperators.so_leaderandlerobot.robots.so_follower. We do not wrap or re-export them. - Datasets. Recordings use
LeRobotDatasetdirectly. The Gradio teleop recorder and the LeRobot CLI adapter both use the simulated SO follower conventions for action/state units and camera keys.
Recording with lerobot-record
Install SO101-Nexus with the teleop extra so lerobot[feetech] is available, then load the adapter explicitly with LeRobot's plugin import hook:
lerobot-record \
--robot.discover_packages_path=so101_nexus.lerobot_adapter \
--robot.type=sim_so_follower \
--robot.env_id=MuJoCoTouch-v1 \
--robot.id=my_robot \
--robot.calibration_dir=~/.cache/huggingface/lerobot/calibration/robots/so_follower \
--robot.use_degrees=true \
--teleop.type=so101_leader \
--teleop.port=/dev/ttyACM0 \
--teleop.id=my_leader \
--dataset.repo_id=user/my_sim_reach \
--dataset.num_episodes=10 \
--dataset.single_task="reach the target"--robot.discover_packages_path is a parser hook, not a RobotConfig field. It imports so101_nexus.lerobot_adapter before config parsing so the LeRobot @register_subclass decorators for sim_so_follower and sim cameras run.
The default is --robot.use_degrees=true, matching LeRobot 0.5 SO follower and SO leader defaults. Percent mode is also supported when both sides opt in:
--robot.use_degrees=false --teleop.use_degrees=falseKeep --robot.use_degrees=true for compatibility with allenai/MolmoAct2-SO100_101. That checkpoint expects action and observation.state to be six-element absolute joint-pose vectors with body joints in LeRobot degree units and the gripper in RANGE_0_100; --robot.use_degrees=false records a valid LeRobot dataset, but in a different body-joint unit space.
Use a real SO follower calibration when you want the same normalized action/state semantics as a physical follower. Record that once with upstream LeRobot, then point --robot.calibration_dir at the directory containing the follower JSON:
lerobot-calibrate \
--robot.type=so101_follower \
--robot.port=/dev/ttyACM1 \
--robot.id=my_robotThe physical leader has its own calibration path. If it is not calibrated yet, calibrate it separately before recording:
lerobot-calibrate \
--teleop.type=so101_leader \
--teleop.port=/dev/ttyACM0 \
--teleop.id=my_leaderFor simulator-only tests, create an explicit synthetic follower calibration file:
from pathlib import Path
from so101_nexus.lerobot_adapter.synthetic_calibration import write_synthetic_calibration
write_synthetic_calibration(Path("calibration"), "my_robot")Synthetic calibration files use LeRobot's normal MotorCalibration schema and the SO101 motor ids 1..6, but they are not a replacement for measured physical calibration data.
The adapter is validated against the MuJoCo backend, reading simulator joint positions and writing actions through it.
Recording with the Gradio teleop app
The Gradio recorder writes the same core schema by default: action and
observation.state are six-element absolute joint vectors, body joints are in
degrees, and the gripper is in RANGE_0_100. Camera features are stored as
observation.images.wrist and observation.images.overhead.
The recorder drives the simulator through SimSOFollower.send_action() and
reads state through SimSOFollower.get_observation(), so observation.state
is follower readback rather than the leader command echo. It creates a
simulator-only calibration file automatically at
$HF_LEROBOT_CALIBRATION/robots/sim_so_follower/teleop_sim.json when missing.
Using LeRobot processors with SO101-Nexus envs
LeRobotEnvWrapper requires a Dict observation space. Configure the env with at least one camera component (or pick another component beyond the default state-only set) before wrapping it.
import so101_nexus.mujoco # noqa: F401
from so101_nexus import JointPositions, PickConfig, WristCamera
from so101_nexus.processors import make_lerobot_env
config = PickConfig(
obs_mode="visual",
observations=[JointPositions(), WristCamera(width=224, height=224)],
)
env = make_lerobot_env("MuJoCoPickLift-v1", config=config, render_mode="rgb_array")
obs, _ = env.reset()
# obs is now {"observation.state": ..., "observation.images.wrist": ..., ...}To customize the pipeline, build one yourself and pass it in:
import gymnasium as gym
from lerobot.processor import DataProcessorPipeline, RenameObservationsProcessorStep
from so101_nexus import JointPositions, PickConfig, WristCamera
from so101_nexus.processors import Hwc2ChwImageObservationStep, LeRobotEnvWrapper
config = PickConfig(
obs_mode="visual",
observations=[JointPositions(), WristCamera(width=224, height=224)],
)
pipeline = DataProcessorPipeline(
steps=[
RenameObservationsProcessorStep(
rename_map={
"state": "observation.state",
"wrist_camera": "observation.images.wrist",
}
),
Hwc2ChwImageObservationStep(image_keys=("observation.images.wrist",)),
]
)
env = LeRobotEnvWrapper(gym.make("MuJoCoPickLift-v1", config=config), pipeline=pipeline)Custom processor pipelines
The default leader pipeline (make_default_leader_action_pipeline) is still available for callers that need the historical conversion path: degrees from the leader, radians on the way to a raw simulator env, with a wrist-roll calibration shift. The Gradio recorder no longer uses that pipeline for dataset recording; it routes through SimSOFollower so stored actions and states stay in LeRobot SO follower units.