Tutorial de doble brazo (doble brazo seguidor) del SO-ARM101
Esta guía presenta el flujo completo para entrenar un sistema robótico SO-ARM de doble brazo con LeRobot, incluyendo la conexión del hardware, la calibración de ambos brazos, la teleoperación de doble brazo, la grabación y la gestión del dataset, el entrenamiento de la política ACT y el despliegue en el robot real. Siguiendo esta guía, podrá recopilar datos de demostración con dos brazos líder y dos brazos seguidores, entrenar una política de aprendizaje por imitación y ejecutarla en brazos robóticos reales.
En primer lugar, realice las conexiones de la siguiente manera:
| Rol | Puerto |
|---|---|
| Brazo seguidor izquierdo | /dev/ttyACM0 |
| Brazo seguidor derecho | /dev/ttyACM1 |
| Brazo líder izquierdo | /dev/ttyACM2 |
| Brazo líder derecho | /dev/ttyACM3 |
El tipo de los brazos seguidores es so101_follower y el de los brazos líderes es so101_leader (en LeRobot, so100_leader y so101_leader comparten la misma implementación).
Preparación previa
Instalar dependencias
Para la instalación del entorno, consulte el tutorial de uso del SO-ARM101.
Permisos USB
sudo chmod 666 /dev/ttyACM0 /dev/ttyACM1 /dev/ttyACM2 /dev/ttyACM31. Calibración (paso clave)
1.1 Calibrar el brazo seguidor izquierdo
lerobot-calibrate \
--robot.type=so101_follower \
--robot.port=/dev/ttyACM0 \
--robot.id=my_so101_bi_follower_left1.2 Calibrar el brazo seguidor derecho
lerobot-calibrate \
--robot.type=so101_follower \
--robot.port=/dev/ttyACM1 \
--robot.id=my_so101_bi_follower_right1.3 Calibrar el brazo líder izquierdo
lerobot-calibrate \
--teleop.type=so101_leader \
--teleop.port=/dev/ttyACM2 \
--teleop.id=my_so101_bi_leader_left1.4 Calibrar el brazo líder derecho
lerobot-calibrate \
--teleop.type=so101_leader \
--teleop.port=/dev/ttyACM3 \
--teleop.id=my_so101_bi_leader_rightUna vez completada la calibración, los archivos se guardan en:
~/.cache/huggingface/lerobot/calibration/robots/so_follower/my_so101_bi_follower_left.json
~/.cache/huggingface/lerobot/calibration/robots/so_follower/my_so101_bi_follower_right.json
~/.cache/huggingface/lerobot/calibration/teleoperators/so_leader/my_so101_bi_leader_left.json
~/.cache/huggingface/lerobot/calibration/teleoperators/so_leader/my_so101_bi_leader_right.jsonNota sobre los nombres de directorio:
so101_followeryso100_follower, así comoso101_leaderyso100_leader, comparten la misma implementación, por lo que los directorios sonso_follower/so_leader; los brazos líderes son teleoperators, así que sus archivos de calibración están enteleoperators/y no enrobots/.
(Opcional) Si ya calibraba con otros ID
Por ejemplo, si antes usaba my_awesome_follower_arm1, my_awesome_follower_arm2, etc., puede copiar los archivos de calibración:
CAL_DIR=~/.cache/huggingface/lerobot/calibration
cp $CAL_DIR/robots/so_follower/my_awesome_follower_arm1.json \
$CAL_DIR/robots/so_follower/my_so101_bi_follower_left.json
cp $CAL_DIR/robots/so_follower/my_awesome_follower_arm2.json \
$CAL_DIR/robots/so_follower/my_so101_bi_follower_right.json
cp $CAL_DIR/teleoperators/so_leader/my_awesome_leader_arm3.json \
$CAL_DIR/teleoperators/so_leader/my_so101_bi_leader_left.json
cp $CAL_DIR/teleoperators/so_leader/my_awesome_leader_arm4.json \
$CAL_DIR/teleoperators/so_leader/my_so101_bi_leader_right.json2. Teleoperación de doble brazo
2.1 Sin cámaras
lerobot-teleoperate \
--robot.type=bi_so_follower \
--robot.left_arm_config.port=/dev/ttyACM0 \
--robot.right_arm_config.port=/dev/ttyACM1 \
--robot.id=my_so101_bi_follower \
--teleop.type=bi_so_leader \
--teleop.left_arm_config.port=/dev/ttyACM2 \
--teleop.right_arm_config.port=/dev/ttyACM3 \
--teleop.id=my_so101_bi_leader \
--display_data=true2.2 Con cámaras
Puede usar lerobot-find-cameras opencv para consultar los índices de las cámaras; también puede añadir o quitar cámaras libremente.
lerobot-teleoperate \
--robot.type=bi_so_follower \
--robot.left_arm_config.port=/dev/ttyACM0 \
--robot.right_arm_config.port=/dev/ttyACM1 \
--robot.id=my_so101_bi_follower \
--robot.left_arm_config.cameras='{
left_wrist: {"type": "opencv", "index_or_path": 2, "width": 640, "height": 480, "fps": 30}
}' \
--robot.right_arm_config.cameras='{
right_wrist: {"type": "opencv", "index_or_path": 4, "width": 640, "height": 480, "fps": 30}
}' \
--teleop.type=bi_so_leader \
--teleop.left_arm_config.port=/dev/ttyACM2 \
--teleop.right_arm_config.port=/dev/ttyACM3 \
--teleop.id=my_so101_bi_leader \
--display_data=trueAviso de seguridad
- Preste atención al entorno para evitar colisiones de los brazos seguidores.
3. Grabación del dataset
3.1 Guardar en local (sin subir a Hub)
Añada --dataset.root (los datos se escriben en ese directorio) y --dataset.push_to_hub=false, además de --dataset.no_stamp=true para mantener estable el nombre del dataset (de lo contrario, a repo_id se le añadirá automáticamente una marca de tiempo y las posteriores reanudaciones, reproducciones o entrenamientos no podrán encontrarlo).
Nota: se recomienda que
repo_idcontenga/(con el formatonombre_de_usuario/nombre_del_dataset); los datasets locales no se suben realmente.
lerobot-record \
--robot.type=bi_so_follower \
--robot.left_arm_config.port=/dev/ttyACM0 \
--robot.right_arm_config.port=/dev/ttyACM1 \
--robot.id=my_so101_bi_follower \
--robot.left_arm_config.cameras='{
left_wrist: {"type": "opencv", "index_or_path": 2, "width": 640, "height": 480, "fps": 30}
}' \
--robot.right_arm_config.cameras='{
right_wrist: {"type": "opencv", "index_or_path": 4, "width": 640, "height": 480, "fps": 30}
}' \
--teleop.type=bi_so_leader \
--teleop.left_arm_config.port=/dev/ttyACM2 \
--teleop.right_arm_config.port=/dev/ttyACM3 \
--teleop.id=my_so101_bi_leader \
--dataset.repo_id=juxi/bi_so101_task \
--dataset.root=./datasets/bi_so101_task \
--dataset.push_to_hub=false \
--dataset.no_stamp=true \
--dataset.single_task="Pick the cube with left arm and hand it to right arm" \
--dataset.num_episodes=50 \
--dataset.fps=30 \
--dataset.episode_time_s=30 \
--dataset.reset_time_s=10 \
--dataset.video=true \
--display_data=trueLa codificación de vídeo ya es
libsvtav1por defecto, no hace falta especificarla; si desea personalizarla, use parámetros anidados como--dataset.rgb_encoder.vcodec=h264.
Los datos se guardan en ./datasets/bi_so101_task/, con esta estructura:
├── meta/
│ ├── info.json # 数据集信息(fps、特征形状等)
│ ├── episodes/ # 每集的元数据(chunk-000/...)
│ ├── stats.json # 各特征归一化统计
│ └── tasks.parquet # 任务文本 → task_index
├── data/ # 每帧特征数据(chunk-*.parquet)
└── videos/ # 每个摄像头一个子目录(chunk-*.mp4)3.2 Subir a Hugging Face Hub
Si desea que se suba automáticamente, conserve HF_USER y elimine root y push_to_hub=false (por defecto se sube). Mantenga los puertos y los índices de las cámaras coherentes con la tabla de conexiones:
export HF_USER=your_hf_username
lerobot-record \
--robot.type=bi_so_follower \
--robot.left_arm_config.port=/dev/ttyACM0 \
--robot.right_arm_config.port=/dev/ttyACM1 \
--robot.id=my_so101_bi_follower \
--robot.left_arm_config.cameras='{
left_wrist: {"type": "opencv", "index_or_path": 2, "width": 640, "height": 480, "fps": 30}
}' \
--robot.right_arm_config.cameras='{
right_wrist: {"type": "opencv", "index_or_path": 4, "width": 640, "height": 480, "fps": 30}
}' \
--teleop.type=bi_so_leader \
--teleop.left_arm_config.port=/dev/ttyACM2 \
--teleop.right_arm_config.port=/dev/ttyACM3 \
--teleop.id=my_so101_bi_leader \
--dataset.repo_id=${HF_USER}/bi_so101_task \
--dataset.no_stamp=true \
--dataset.single_task="Pick the cube with left arm and hand it to right arm" \
--dataset.num_episodes=50 \
--dataset.fps=30 \
--dataset.episode_time_s=30 \
--dataset.reset_time_s=10 \
--dataset.video=true \
--display_data=trueEl nombre del repositorio en el Hub tras la subida será
${HF_USER}/bi_so101_task, que coincide con elrepo_idusado en el entrenamiento desde el Hub de la sección 4.2. La copia local se guarda primero en~/.cache/huggingface/lerobot/${HF_USER}/bi_so101_task/.
3.3 Continuar la recolección (reanudar la grabación)
Si la grabación se interrumpe de forma accidental (por ejemplo, al salir con el botón derecho durante la fase de reset) o desea completar la recolección en varias sesiones, puede usar --resume para seguir añadiendo episodios al mismo dataset.
Nota:
- Es obligatorio añadir
--resume=true; de lo contrario,LeRobotDataset.create()dará error porque el directorio ya existe. - El
--dataset.rooty el--dataset.repo_iddel comando de continuación deben ser exactamente iguales a los de la primera grabación (3.1) (resumeexige unrootexplícito). --dataset.num_episodesindica cuántos episodios grabar en esta sesión, no el objetivo total. Por ejemplo, si ya ha grabado 15 y quiere llegar a 50, escriba35.- Al salir, procure hacerlo durante la grabación de un episodio o justo al terminar; evite salir en la fase "Reset the environment" (provocaría el fallo al guardar un episodio vacío).
lerobot-record \
--resume=true \
--robot.type=bi_so_follower \
--robot.left_arm_config.port=/dev/ttyACM0 \
--robot.right_arm_config.port=/dev/ttyACM1 \
--robot.id=my_so101_bi_follower \
--robot.left_arm_config.cameras='{
left_wrist: {"type": "opencv", "index_or_path": 2, "width": 640, "height": 480, "fps": 30}
}' \
--robot.right_arm_config.cameras='{
right_wrist: {"type": "opencv", "index_or_path": 4, "width": 640, "height": 480, "fps": 30}
}' \
--teleop.type=bi_so_leader \
--teleop.left_arm_config.port=/dev/ttyACM2 \
--teleop.right_arm_config.port=/dev/ttyACM3 \
--teleop.id=my_so101_bi_leader \
--dataset.repo_id=juxi/bi_so101_task \
--dataset.root=./datasets/bi_so101_task \
--dataset.push_to_hub=false \
--dataset.no_stamp=true \
--dataset.single_task="Pick the cube with left arm and hand it to right arm" \
--dataset.num_episodes=35 \
--dataset.fps=30 \
--dataset.episode_time_s=30 \
--dataset.reset_time_s=10 \
--dataset.video=true \
--display_data=true3.4 Reproducción y eliminación de episodios
Reproducir un episodio concreto
lerobot-replay \
--robot.type=bi_so_follower \
--robot.left_arm_config.port=/dev/ttyACM0 \
--robot.right_arm_config.port=/dev/ttyACM1 \
--robot.id=my_so101_bi_follower \
--dataset.repo_id=juxi/bi_so101_task \
--dataset.root=./datasets/bi_so101_task \
--dataset.episode=24
episodees un índice basado en 0;24corresponde al episodio número 25.
Eliminar un episodio concreto
python -m lerobot.scripts.lerobot_edit_dataset \
--repo_id=juxi/bi_so101_task \
--root=./datasets/bi_so101_task \
--operation.type=delete_episodes \
--operation.episode_indices="[24]"Tras la eliminación, el dataset se reescribe en su lugar y los datos originales se respaldan en ./datasets/bi_so101_task_old/. Cuando confirme que el nuevo dataset es correcto, puede eliminar la copia de seguridad manualmente:
rm -rf ./datasets/bi_so101_task_oldEliminar el dataset completo
rm -rf ./datasets/bi_so101_task4. Entrenamiento de ACT
4.1 Entrenar desde un dataset local
lerobot-train \
--dataset.repo_id=juxi/bi_so101_task \
--dataset.root=./datasets/bi_so101_task \
--policy.type=act \
--policy.device=cuda \
--steps=60000 \
--output_dir=outputs/train/act_bi_so101 \
--wandb.enable=false \
--policy.push_to_hub=false
--dataset.rootapunta al directorio del dataset grabado en 3.1 (elrepo_iddebe coincidir con el usado al grabar). Si el directorio--output_dirya existe, se producirá directamente unFileExistsError; use un directorio de salida nuevo o añada--resume=truepara continuar el entrenamiento.
4.2 Entrenar desde Hugging Face Hub
export HF_USER=your_hf_username
lerobot-train \
--dataset.repo_id=${HF_USER}/bi_so101_task \
--policy.type=act \
--policy.device=cuda \
--steps=100000 \
--output_dir=outputs/train/act_bi_so101 \
--wandb.enable=false \
--policy.push_to_hub=falseEl comando anterior usa los parámetros predeterminados de ACT (
chunk_size=100,dim_model=512, etc.).
El
repo_iddebe coincidir con el nombre del repositorio de la subida de 3.2 (en 3.2 ya se añadió--dataset.no_stamp=true, por lo que el nombre queda fijo como${HF_USER}/bi_so101_task). En el entrenamiento no hace falta--dataset.root: se descarga automáticamente desde el Hub.
5. Despliegue en el robot real
Nota:
lerobot-recordsolo se usa para recopilar datos de demostración. Para desplegar una política ya entrenada uselerobot-rollout: en la versión actual,lerobot-recordya no acepta--policy.pathy también rechaza los nombres de dataset con el prefijoeval_.
5.1 Evaluación en el sitio (sin grabar datos)
lerobot-rollout \
--strategy.type=base \
--policy.path=outputs/train/act_bi_so101/checkpoints/last/pretrained_model \
--robot.type=bi_so_follower \
--robot.left_arm_config.port=/dev/ttyACM0 \
--robot.right_arm_config.port=/dev/ttyACM1 \
--robot.id=my_so101_bi_follower \
--robot.left_arm_config.cameras='{
left_wrist: {"type": "opencv", "index_or_path": 2, "width": 640, "height": 480, "fps": 30}
}' \
--robot.right_arm_config.cameras='{
right_wrist: {"type": "opencv", "index_or_path": 4, "width": 640, "height": 480, "fps": 30}
}' \
--task="Pick the cube with left arm and hand it to right arm" \
--duration=60 \
--display_data=true--durationes el número de segundos de ejecución;0significa sin límite de tiempo.- Si necesita tomar el control o detenerlo a mitad, añada
--interactive=truey use comandos como/stopy/reseten la terminal.
5.2 Evaluar y grabar datos (local)
Use la estrategia episodic (se comporta como el antiguo lerobot-record: graba por episodios con fase de reset):
lerobot-rollout \
--strategy.type=episodic \
--policy.path=outputs/train/act_bi_so101/checkpoints/last/pretrained_model \
--robot.type=bi_so_follower \
--robot.left_arm_config.port=/dev/ttyACM0 \
--robot.right_arm_config.port=/dev/ttyACM1 \
--robot.id=my_so101_bi_follower \
--robot.left_arm_config.cameras='{
left_wrist: {"type": "opencv", "index_or_path": 2, "width": 640, "height": 480, "fps": 30}
}' \
--robot.right_arm_config.cameras='{
right_wrist: {"type": "opencv", "index_or_path": 4, "width": 640, "height": 480, "fps": 30}
}' \
--dataset.repo_id=juxi/rollout_bi_so101_task \
--dataset.root=./datasets/rollout_bi_so101_task \
--dataset.no_stamp=true \
--dataset.num_episodes=10 \
--dataset.single_task="Pick the cube with left arm and hand it to right arm" \
--dataset.fps=30 \
--display_data=trueEl nombre del dataset de despliegue debe empezar por
rollout_(convención obligatoria de la versión actual). Al grabar en local se recomienda añadir--dataset.rooty--dataset.no_stamp=truepara evitar que se añada una marca de tiempo al nombre del directorio.
5.3 Subir los datos de evaluación a Hugging Face Hub
export HF_USER=your_hf_username
lerobot-rollout \
--strategy.type=episodic \
--policy.path=outputs/train/act_bi_so101/checkpoints/last/pretrained_model \
--robot.type=bi_so_follower \
--robot.left_arm_config.port=/dev/ttyACM0 \
--robot.right_arm_config.port=/dev/ttyACM1 \
--robot.id=my_so101_bi_follower \
--robot.left_arm_config.cameras='{
left_wrist: {"type": "opencv", "index_or_path": 2, "width": 640, "height": 480, "fps": 30}
}' \
--robot.right_arm_config.cameras='{
right_wrist: {"type": "opencv", "index_or_path": 4, "width": 640, "height": 480, "fps": 30}
}' \
--dataset.repo_id=${HF_USER}/rollout_bi_so101_task \
--dataset.no_stamp=true \
--dataset.num_episodes=10 \
--dataset.single_task="Pick the cube with left arm and hand it to right arm" \
--dataset.fps=30 \
--display_data=true6. Preguntas frecuentes
| Problema | Causa | Solución |
|---|---|---|
| Al teleoperar pide recalibrar | bi_so_follower no encuentra los archivos de calibración con sufijo _left / _right | Vuelva a calibrar con ID que incluyan _left / _right, o copie los archivos de calibración existentes |
| El brazo líder no se puede arrastrar | El par (torque) del leader no está desactivado | Vuelva a calibrar o revise los motores |
| Al continuar la recolección da error de directorio existente | No se añadió --resume=true | Añada --resume=true al comando lerobot-record |
Con --resume=true da error pidiendo root | La continuación exige especificar explícitamente el directorio del dataset | Añada --dataset.root=./datasets/bi_so101_task al comando de continuación, igual que en la primera grabación |
| Al nombre del directorio del dataset se le añade una marca de tiempo y la reproducción o el entrenamiento no lo encuentran | No se configuró no_stamp al grabar y a repo_id se le añadió automáticamente una marca de tiempo | Añada --dataset.no_stamp=true al grabar o continuar |
--dataset.vcodec=... da error de parámetro inexistente | Parámetro de una versión antigua; los parámetros de codificación de vídeo actuales son anidados | Use --dataset.rgb_encoder.vcodec=h264 (por defecto ya es libsvtav1) |
Al desplegar, lerobot-record da errores de --policy.path / eval_ | La versión actual de lerobot-record ya no incluye la capacidad de desplegar políticas | Use lerobot-rollout --strategy.type=episodic para el despliegue, con nombres de dataset que empiecen por rollout_ |
| Los brazos izquierdo y derecho están invertidos | Configuración de puertos incorrecta | Intercambie left_arm_config.port y right_arm_config.port |
| Durante el entrenamiento no encuentra el dataset | No se especificó root para el dataset local | Añada --dataset.root=./datasets/xxx al entrenar |
| El dataset se sube automáticamente | No se configuró push_to_hub=false | Añada --dataset.push_to_hub=false al grabar |
Al salir aparece You must add one or several frames before calling add_episode | Se salió durante la fase de reset y el episodio en curso no tenía fotogramas | No afecta a los datos ya grabados; continúe la recolección con --resume=true |

