SJTU & Evo-Tech
Architecture Overview

| Getting Started | Training Pipeline | Project Info |
|---|---|---|
| ⚡ Quick Start | 4) Value Function Training | Model & Dataset |
| 1) Installation | 5) Value Inference | Community Channels |
| 2) Hardware Setup | 6) Policy Training | Affiliations |
| 3) Data Collection | 7) Closed-loop Rollout and Next Round | Citation / License |
Value Visual Results
Success Case

Failure Case

Policy Rollout Visual Results

Human-in-the-Loop Visual Results

LeRobot-aligned foundation: we use LeRobot as the base of this codebase because its inference and data-collection logic are highly aligned with real-world RL workflows.
git clone https://github.com/MINT-SJTU/Evo-RL.git
cd Evo-RL
conda create -y -n evo-rl python=3.10
conda activate evo-rl
pip install -e .
For setup details and platform-specific dependencies, follow the official LeRobot configuration guide.
For SO-series setup, please follow the official tutorial in detail and complete all installation and configuration steps there before continuing. The examples below use SO101 as the reference configuration.
Recommended path strategy:
/dev/serial/by-id/ (stable across reboots)./dev/v4l/by-id/; if IDs are not unique, use /dev/v4l/by-path/.by-id, camera paths use by-path.You can inspect available stable paths with:
ls -l /dev/serial/by-id/
ls -l /dev/v4l/by-id/
ls -l /dev/v4l/by-path/
For single-arm users, no major changes are required. After setup, run the command below to verify your system is ready for the next stage:
lerobot-teleoperate \
--robot.type=so101_follower \
--robot.port=/dev/serial/by-id/<SO101_FOLLOWER_PORT> \
--robot.id=my_so101_follower \
--teleop.type=so101_leader \
--teleop.port=/dev/serial/by-id/<SO101_LEADER_PORT> \
--teleop.id=my_so101_leader
For dual-arm users, we recommend mirroring the mechanical parts corresponding to servos 4/5/6 on the left leader and left follower arms, which usually provides a more natural bimanual operation feel.
Before running the dual-arm command, make sure calibration files exist under ~/.cache/huggingface/lerobot/calibration/ like:
calibration/
├── robots
│ └── so_follower
│ ├── bi_so101_follower_left.json
│ └── bi_so101_follower_right.json
└── teleoperators
└── so_leader
├── bi_so101_leader_left.json
└── bi_so101_leader_right.json
This layout is slightly different from single-arm setup.
Then run this command to verify dual-arm setup:
lerobot-teleoperate \
--robot.type=bi_so_follower \
--robot.left_arm_config.port=/dev/serial/by-id/<LEFT_FOLLOWER_PORT> \
--robot.right_arm_config.port=/dev/serial/by-id/<RIGHT_FOLLOWER_PORT> \
--robot.id=bi_so101_follower \
--teleop.type=bi_so_leader \
--teleop.left_arm_config.port=/dev/serial/by-id/<LEFT_LEADER_PORT> \
--teleop.right_arm_config.port=/dev/serial/by-id/<RIGHT_LEADER_PORT> \
--teleop.id=bi_so101_leader
Before data collection, validate camera mapping first.
Check whether each camera supports your target setting (for example, 640x480 @ 30):
v4l2-ctl -d /dev/v4l/by-path/<CAM_PATH> --list-formats-ext
Single-arm camera check (example):
lerobot-teleoperate \
--robot.type=so101_follower \
--robot.port=/dev/serial/by-id/<SO101_FOLLOWER_PORT> \
--robot.id=my_so101_follower \
--robot.cameras='{ front: {type: opencv, index_or_path: "/dev/v4l/by-path/<FRONT_CAM>", width: 640, height: 480, fps: 30}}' \
--teleop.type=so101_leader \
--teleop.port=/dev/serial/by-id/<SO101_LEADER_PORT> \
--teleop.id=my_so101_leader \
--display_data=true
Dual-arm camera check (example):
lerobot-teleoperate \
--robot.type=bi_so_follower \
--robot.left_arm_config.port=/dev/serial/by-id/<LEFT_FOLLOWER_PORT> \
--robot.right_arm_config.port=/dev/serial/by-id/<RIGHT_FOLLOWER_PORT> \
--robot.id=my_bi_so101_follower \
--robot.left_arm_config.cameras='{ wrist: {type: opencv, index_or_path: "/dev/v4l/by-path/<LEFT_WRIST_CAM_PATH>", width: 640, height: 480, fps: 30}}' \
--robot.right_arm_config.cameras='{ wrist: {type: opencv, index_or_path: "/dev/v4l/by-path/<RIGHT_WRIST_CAM_PATH>", width: 640, height: 480, fps: 30}, front: {type: opencv, index_or_path: "/dev/v4l/by-path/<FRONT_CAM_PATH>", width: 640, height: 480, fps: 30}}' \
--teleop.type=bi_so_leader \
--teleop.left_arm_config.port=/dev/serial/by-id/<LEFT_LEADER_PORT> \
--teleop.right_arm_config.port=/dev/serial/by-id/<RIGHT_LEADER_PORT> \
--teleop.id=my_bi_so101_leader \
--display_data=true
For dual-arm camera mapping, it is fine to attach front under either the left-arm or right-arm camera config. If you use more camera views, place them under either the left or right arm camera config as well.
If needed, you can also use temporary device paths (for example /dev/ttyACM* and /dev/video*) during initial debugging.
PiPER arms in master/teaching mode cannot receive external control commands, so all arms must be configured to follower/motion-output mode (0xFC), and firmware must be version 1.8.5 or above.
For PiPER-series robots, make sure Git LFS assets are pulled before running teleoperation:
git lfs pull --include="src/lerobot/assets/piper_description/**,src/lerobot/assets/piper_x_description/**" --exclude="*"
git lfs checkout src/lerobot/assets/piper_description src/lerobot/assets/piper_x_description
For PiPER setup, PiPER uses CAN interfaces instead of serial ports.
So first run lerobot-setup-can to confirm CAN interfaces are available:
lerobot-setup-can --mode=setup --interfaces=<LEFT_FOLLOWER_CAN_PORT>,<LEFT_LEADER_CAN_PORT>,<RIGHT_FOLLOWER_CAN_PORT>,<RIGHT_LEADER_CAN_PORT>
For single-arm users, run the command below to verify the system is ready:
lerobot-teleoperate \
--robot.type=piperx_follower \
--robot.port=<FOLLOWER_CAN_PORT> \
--robot.id=my_piperx_follower \
--robot.require_calibration=false \
--teleop.type=piperx_leader \
--teleop.port=<LEADER_CAN_PORT> \
--teleop.id=my_piperx_leader \
--teleop.require_calibration=false
For bimanual users, run this command to verify dual-arm teleoperation:
lerobot-teleoperate \
--robot.type=bi_piperx_follower \
--robot.id=my_bi_piperx_follower \
--robot.left_arm_config.port=<LEFT_FOLLOWER_CAN_PORT> \
--robot.right_arm_config.port=<RIGHT_FOLLOWER_CAN_PORT> \
--robot.left_arm_config.require_calibration=false \
--robot.right_arm_config.require_calibration=false \
--teleop.type=bi_piperx_leader \
--teleop.id=my_bi_piperx_leader \
--teleop.left_arm_config.port=<LEFT_LEADER_CAN_PORT> \
--teleop.right_arm_config.port=<RIGHT_LEADER_CAN_PORT> \
--teleop.left_arm_config.require_calibration=false \
--teleop.right_arm_config.require_calibration=false
For PiPER (non-X), replace bi_piperx_follower/bi_piperx_leader with bi_piper_follower/bi_piper_leader.
Collect rollout data with lerobot-human-inloop-record.
Bimanual template:
lerobot-human-inloop-record \
--robot.type=bi_so_follower \
--robot.left_arm_config.port=/dev/serial/by-id/<LEFT_FOLLOWER_PORT> \
--robot.right_arm_config.port=/dev/serial/by-id/<RIGHT_FOLLOWER_PORT> \
--robot.id=my_bi_so101_follower \
--robot.left_arm_config.cameras='{ wrist: {type: opencv, index_or_path: "/dev/v4l/by-path/<LEFT_WRIST_CAM_PATH>", width: 640, height: 480, fps: 30, fourcc: "MJPG"}}' \
--robot.right_arm_config.cameras='{ wrist: {type: opencv, index_or_path: "/dev/v4l/by-path/<RIGHT_WRIST_CAM_PATH>", width: 640, height: 480, fps: 30, fourcc: "MJPG"}, front: {type: intelrealsense, serial_number_or_name: "<REALSENSE_SN>", width: 640, height: 480, fps: 30, warmup_s: 2}}' \
--teleop.type=bi_so_leader \
--teleop.left_arm_config.port=/dev/serial/by-id/<LEFT_LEADER_PORT> \
--teleop.right_arm_config.port=/dev/serial/by-id/<RIGHT_LEADER_PORT> \
--teleop.id=my_bi_so101_leader \
--dataset.repo_id=<HF_USERNAME_OR_ORG>/<DATASET_NAME> \
--dataset.single_task="<YOUR_TASK_DESCRIPTION>" \
--dataset.num_episodes=<NUM_EPISODES> \
--dataset.episode_time_s=<EPISODE_SECONDS> \
--dataset.reset_time_s=<RESET_SECONDS> \
--dataset.push_to_hub=true \
--display_data=true
Recommendation: use fourcc: "MJPG" for OpenCV and warmup_s for RealSense. In this example front uses RealSense, but you can switch it to OpenCV with the same structure.
Bimanual template (left/right, PiPER-X example):
```bash lerobot-human-inloop-record \ --robot.type=bi_piperx_follower \ --robot.id=my_bi_piperx_follower \ --robot.left_arm_config.port= \ --robot.right_arm_config.port= \ --robot.left_arm_config.require_calibration=false \ --robot.right_arm_config.require_calibration=false \ --teleop.type=bi_piperx_leader \ --teleop.id=my_bi_piperx_leader \ --teleop.left_arm_config.port= \ --teleop.right_arm_config.port= \ --teleop.left_arm_config.require_calibration=false \ --teleop.right_arm_config.require_calibration=false \ --dataset.repo_id=/ \ --dataset.single_task="" \ --dataset.num_episodes= \ --dataset.episode_time_s= \ --dataset.reset_time_s= \ --dataset.push_to_hub=true \ --display_data=tr
$ claude mcp add Evo-RL \
-- python -m otcore.mcp_server <graph>