Saltar al contenido principal
Etapa 3 · Capítulo 17 · Optativo

17. Entrenamiento de doble brazo SO-ARM

Capítulo 17 del curso para principiantes de SO-ARM Physical AI (optativo) — escala de un brazo a dos: cablea cuatro brazos, calibra los pares líder/seguidor izquierdo y derecho, teleopera y graba con bi_so_follower / bi_so_leader, entrena ACT y despliega políticas bimanuales.

Lo que necesitarásDos kits SO-ARM (4 brazos: 2 líderes + 2 seguidores).El mismo entorno de software de los Capítulos 5-16.Una tarea que necesite dos manos — p. ej. pasar un objeto del brazo izquierdo al derecho.

17.1 Por qué doble brazo​

Motivación

17.1 Por qué doble brazo

Muchas tareas reales de manipulación son genuinamente de dos manos: recoger y entregar, plegar cajas, ensamblar dos piezas. El aprendizaje bimanual por imitación es además el escenario donde nació ACT (ALOHA), y el ecosistema SO-ARM lo soporta de forma nativa — LeRobot trae los tipos bi_so_follower y bi_so_leader que tratan dos brazos como un solo robot de 12 dimensiones de acción (6 por brazo).

Todo lo aprendido en los Capítulos 8-16 se transfiere; solo se duplica el hardware y cada comando lleva left_arm_config / right_arm_config.

17.2 Cableado y puertos​

Cableado

17.2 Cableado y puertos

Conecta las cuatro placas y anota el puerto de cada brazo:

RolPuerto
Seguidor izquierdo/dev/ttyACM0
Seguidor derecho/dev/ttyACM1
Líder izquierdo/dev/ttyACM2
Líder derecho/dev/ttyACM3

El tipo del seguidor es so101_follower y el del líder so101_leader para calibrar (en LeRobot, so100_leader y so101_leader comparten implementación).

Permisos para los cuatro puertos:

sudo chmod 666 /dev/ttyACM0 /dev/ttyACM1 /dev/ttyACM2 /dev/ttyACM3

17.3 Calibración (paso clave)​

Calibración

17.3 Calibración (paso clave)

Calibra cada brazo por separado con el procedimiento del Capítulo 10, usando ids con sufijos _left / _right — los tipos bimanuales buscan archivos con exactamente esos sufijos.

Seguidor izquierdo:

lerobot-calibrate \
--robot.type=so101_follower \
--robot.port=/dev/ttyACM0 \
--robot.id=my_awesome_bimanual_follower_left

Seguidor derecho:

lerobot-calibrate \
--robot.type=so101_follower \
--robot.port=/dev/ttyACM1 \
--robot.id=my_awesome_bimanual_follower_right

Líder izquierdo:

lerobot-calibrate \
--teleop.type=so101_leader \
--teleop.port=/dev/ttyACM2 \
--teleop.id=my_awesome_bimanual_leader_left

Líder derecho:

lerobot-calibrate \
--teleop.type=so101_leader \
--teleop.port=/dev/ttyACM3 \
--teleop.id=my_awesome_bimanual_leader_right

Tras calibrar, los archivos quedan en:

~/.cache/huggingface/lerobot/calibration/robots/so101_follower/my_awesome_bimanual_follower_left.json
~/.cache/huggingface/lerobot/calibration/robots/so101_follower/my_awesome_bimanual_follower_right.json
~/.cache/huggingface/lerobot/calibration/robots/so101_leader/my_awesome_bimanual_leader_left.json
~/.cache/huggingface/lerobot/calibration/robots/so101_leader/my_awesome_bimanual_leader_right.json
tip

¿Ya calibraste con otros ids? Copia los archivos en lugar de repetir el procedimiento físico:

CAL_DIR=~/.cache/huggingface/lerobot/calibration/robots

cp $CAL_DIR/so101_follower/my_awesome_follower_arm1.json \
$CAL_DIR/so101_follower/my_awesome_bimanual_follower_left.json

cp $CAL_DIR/so101_follower/my_awesome_follower_arm2.json \
$CAL_DIR/so101_follower/my_awesome_bimanual_follower_right.json

cp $CAL_DIR/so101_leader/my_awesome_leader_arm3.json \
$CAL_DIR/so101_leader/my_awesome_bimanual_leader_left.json

cp $CAL_DIR/so101_leader/my_awesome_leader_arm4.json \
$CAL_DIR/so101_leader/my_awesome_bimanual_leader_right.json

17.4 Teleoperación de doble brazo​

Teleoperación

17.4 Teleoperación de doble brazo

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_awesome_bimanual_follower \
--teleop.type=bi_so_leader \
--teleop.left_arm_config.port=/dev/ttyACM2 \
--teleop.right_arm_config.port=/dev/ttyACM3 \
--teleop.id=my_awesome_bimanual_leader \
--display_data=true

Con cámaras de muñeca (índices con lerobot-find-cameras opencv, añade o quita según necesites):

lerobot-teleoperate \
--robot.type=bi_so_follower \
--robot.left_arm_config.port=/dev/ttyACM0 \
--robot.right_arm_config.port=/dev/ttyACM1 \
--robot.id=my_awesome_bimanual_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_awesome_bimanual_leader \
--display_data=true
aviso

Dos seguidores comparten ahora un mismo espacio de trabajo. Vigila el entorno para evitar colisiones entre ellos — y entre las pinzas en tareas de entrega.

17.5 Grabar un dataset bimanual​

Grabación

17.5 Grabar un dataset bimanual

Guarda localmente (añade --dataset.push_to_hub=false; el repo_id debe contener /, así que seeed/ sirve de prefijo local):

lerobot-record \
--robot.type=bi_so_follower \
--robot.left_arm_config.port=/dev/ttyACM0 \
--robot.right_arm_config.port=/dev/ttyACM1 \
--robot.id=my_awesome_bimanual_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_awesome_bimanual_leader \
--dataset.repo_id=seeed/bimanual_so101_task \
--dataset.push_to_hub=false \
--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 \
--dataset.vcodec=libsvtav1 \
--display_data=true

Los datos van a ~/.cache/huggingface/lerobot/seeed/bimanual_so101_task/ con la misma estructura del Capítulo 13. Para subir al Hub, mantén HF_USER, quita push_to_hub=false y fija --dataset.repo_id=${HF_USER}/bimanual_so101_task.

Continuar una grabación (reanudar)​

Si la grabación termina de forma inesperada o recoges por sesiones, usa --resume:

  • Añade siempre --resume=true; si no, LeRobotDataset.create() fallará porque el directorio existe.
  • --dataset.num_episodes cuenta los episodios de esta sesión (15 de 50 grabados → pon 35).
  • Procura salir durante un episodio o tras un final natural; evita salir en la fase de "reiniciar el entorno" (puede guardar un episodio vacío — el código reciente los salta automáticamente):
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_awesome_bimanual_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_awesome_bimanual_leader \
--dataset.repo_id=seeed/bimanual_so101_task \
--dataset.push_to_hub=false \
--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 \
--dataset.vcodec=libsvtav1 \
--display_data=true

Reproducir y borrar episodios​

Reproduce un episodio concreto en los brazos reales (episode base 0 — 24 significa el 25.º):

lerobot-replay \
--robot.type=bi_so_follower \
--robot.left_arm_config.port=/dev/ttyACM0 \
--robot.right_arm_config.port=/dev/ttyACM1 \
--robot.id=my_awesome_bimanual_follower \
--dataset.repo_id=seeed/bimanual_so101_task \
--dataset.episode=24

Borra un episodio malo (kit del Capítulo 13):

python -m lerobot.scripts.lerobot_edit_dataset \
--repo_id=seeed/bimanual_so101_task \
--operation.type=delete_episodes \
--operation.episode_indices="[24]"

El original queda respaldado en .../bimanual_so101_task_old/; bórralo a mano tras verificar.

17.6 Entrenamiento ACT​

Entrenar

17.6 Entrenamiento ACT

Desde el dataset local:

lerobot-train \
--dataset.repo_id=seeed/bimanual_so101_task \
--policy.type=act \
--policy.device=cuda \
--steps=60000 \
--output_dir=outputs/train/act_bimanual_so101 \
--wandb.enable=false \
--policy.push_to_hub=false

Desde Hugging Face Hub:

export HF_USER=your_hf_username

lerobot-train \
--dataset.repo_id=${HF_USER}/bimanual_so101_task \
--policy.type=act \
--policy.device=cuda \
--steps=100000 \
--output_dir=outputs/train/act_bimanual_so101 \
--wandb.enable=false \
--policy.push_to_hub=false

Se usan los parámetros por defecto de ACT (chunk_size=100, dim_model=512, etc.). La política lee automáticamente estados/acciones de 12 dimensiones (6 por brazo) del dataset — sin configuración extra.

17.7 Despliegue en robot real​

Desplegar

17.7 Despliegue en robot real

Evalúa con la receta del Capítulo 16, ahora con ambos brazos — guarda la evaluación localmente:

lerobot-record \
--robot.type=bi_so_follower \
--robot.left_arm_config.port=/dev/ttyACM0 \
--robot.right_arm_config.port=/dev/ttyACM1 \
--robot.id=my_awesome_bimanual_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.root=seeed_eval/eval_bimanual_so101_task8 \
--dataset.push_to_hub=false \
--dataset.num_episodes=10 \
--dataset.single_task="Pick the cube with left arm and hand it to right arm" \
--dataset.fps=30 \
--dataset.video=true \
--policy.path=outputs/train/act_bimanual_so101/checkpoints/last/pretrained_model \
--display_data=true

O súbelo con --dataset.repo_id=${HF_USER}/eval_bimanual_so101_task y sin dataset.root.

17.8 FAQ​

FAQ

17.8 FAQ

ProblemaCausaSolución
La teleoperación pide recalibrarbi_so_follower no encuentra archivos con sufijo _left / _rightRecalibra con ids con sufijos, o copia los archivos (17.3)
El líder no se puede mover a manoPar del líder no desactivadoRecalibra o revisa los motores
"Directory already exists" al reanudarFalta --resume=trueAñádelo al comando lerobot-record
Brazos izquierdo y derecho intercambiadosPuerto mal configuradoIntercambia left_arm_config.port y right_arm_config.port
Dataset no encontrado al entrenarroot sin especificar para dataset localAñade --dataset.root=./datasets/xxx al entrenar
El dataset se sube automáticamenteFalta push_to_hub=falseAñádelo al grabar
Sale con You must add one or several frames before calling add_episodeSaliste en la fase de reinicio; el episodio no tiene fotogramasLo ya grabado está a salvo; continúa con --resume=true; el código actual salta episodios vacíos
Loading Comments...