LeRobot documentation

Usa tu propio hardware

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v0.6.1).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Usa tu propio hardware

En este tutorial se explica cómo integrar tu propio diseño de robot en el ecosistema de LeRobot y darle acceso a todas nuestras herramientas (recopilación de datos, pipelines de control, entrenamiento e inferencia de políticas).

Con ese fin, proporcionamos la clase base Robot en LeRobot, que especifica una interfaz estándar para la integración de robots físicos. Veamos cómo implementarla.

Prerrequisitos

  • Tu propio robot que expone una interfaz de comunicación (p. ej., serial, CAN, TCP)
  • Una forma de leer datos de sensores y enviar comandos de motores de manera programática, por ejemplo, el SDK o la API del fabricante, o una implementación de protocolo propia.
  • LeRobot instalado en tu entorno. Sigue nuestra Guía de instalación.

Elige tus motores

Si estás usando motores Feetech o Dynamixel, LeRobot proporciona una interfaz de bus integrada, SerialMotorsBus, que controla cualquiera de las dos familias: el modelo de cada motor le indica cuál es. Consulta su documentación para conocer su API. Requiere el extra serial-motors, que instala rustypot: pip install 'lerobot[serial-motors]', o pip install -e ".[serial-motors]" desde un checkout de código fuente. Para un buen ejemplo de cómo se puede usar, puedes echar un vistazo a nuestra propia implementación de seguidor SO101

Utilice estos si son compatibles. De lo contrario, deberá buscar o escribir una interfaz de Python (no se aborda en este tutorial):

  • Busque un SDK existente en Python (o use bindings para C/C++)
  • O implemente un envoltorio básico de comunicación (por ejemplo, mediante pyserial, socket o CANopen)

No está solo: muchas contribuciones de la comunidad utilizan placas o firmware personalizados.

Para Feetech y Dynamixel, actualmente admitimos estos servos:

  • Serie STS y SMS de Feetech: sts3215, sts3250, sm8512bl
  • Serie SCS de Feetech: scs0009
  • Dynamixel serie X (protocolo 2.0): xl330-m077, xl330-m288, xc330-t181, xc330-t288, xl430-w250, xl430-w250-2, xc430-w150, xm430-w350, xm540-w270, xh540-w150

Los modelos pueden compartir un bus, servos STS/SMS y SCS o XL430 y XL330: cada motor se lee y escribe a través de la definición de su propio modelo, incluido el orden de bytes y la codificación de signo. Las diferencias entre familias provienen de las mismas definiciones: qué valor de modo de operación significa control de posición (bus.set_operating_mode("position")), si el desplazamiento de homing se suma a la posición o se resta de ella, qué registros existen.

Que un modelo sea compatible no significa que todas las utilidades de calibración funcionen con él: el Feetech SCS0009 no tiene ni un modo de operación ni un desplazamiento de retorno a casa, por lo que set_operating_mode() y set_half_turn_homings() lo rechazan. La receta de calibración siguiente es para servos que tienen ambos, como el STS; un servo SCS se calibra solo por su rango.

Si está utilizando servos Feetech o Dynamixel que no figuran en esta lista, añádalos a rustypot, que contiene las tablas de control que leen los buses: un modelo que comparta una tabla de control existente solo necesita registrar su número de modelo, mientras que uno nuevo requiere una definición de servo (consulte la guía de contribución de servos). LeRobot lee los modelos desde el paquete Python rustypot instalado, por lo que este debe incluir el nuevo modelo: una versión de rustypot que lo contenga, o rustypot compilado desde el código fuente.

En las siguientes secciones, los ejemplos utilizan motores Feetech. Cambie los modelos y adáptelos a sus motores si es necesario.

Paso 1: Subclase de la interfaz Robot

Primero, debe especificar la clase de configuración y un identificador de cadena (name) para su robot. Si su robot tiene necesidades especiales que desee poder cambiar fácilmente, debe incluirlas aquí (p. ej., puerto/dirección, baudrate).

Aquí, añadiremos el nombre del puerto y una cámara por defecto para nuestro robot:

from dataclasses import dataclass, field

from lerobot.cameras import CameraConfig
from lerobot.cameras.opencv import OpenCVCameraConfig
from lerobot.robots import RobotConfig


@RobotConfig.register_subclass("my_cool_robot")
@dataclass
class MyCoolRobotConfig(RobotConfig):
    port: str
    cameras: dict[str, CameraConfig] = field(
        default_factory=lambda: {
            "cam_1": OpenCVCameraConfig(
                index_or_path=2,
                fps=30,
                width=480,
                height=640,
            ),
        }
    )

Tutorial de cámaras para entender cómo detectar y añadir tu cámara.

A continuación, crearemos nuestra clase de robot real, que hereda de Robot. Esta clase abstracta define un contrato que debes seguir para que tu robot sea utilizable con el resto de las herramientas de LeRobot.

Aquí crearemos un robot simple de 5-DoF con una cámara. Podría ser un brazo simple, pero fíjate en que la clase abstracta Robot no asume nada sobre el factor de forma de tu robot. ¡Puedes dejar volar tu imaginación al diseñar nuevos robots!

from typing import Any

from lerobot.cameras import make_cameras_from_configs
from lerobot.motors import Motor, MotorCalibration, MotorNormMode, SerialMotorsBus
from lerobot.robots import Robot

class MyCoolRobot(Robot):
    config_class = MyCoolRobotConfig
    name = "my_cool_robot"

    def __init__(self, config: MyCoolRobotConfig):
        super().__init__(config)
        self.config = config
        self.bus = SerialMotorsBus(
            port=self.config.port,
            motors={
                "joint_1": Motor(1, "sts3250", MotorNormMode.RANGE_M100_100),
                "joint_2": Motor(2, "sts3215", MotorNormMode.RANGE_M100_100),
                "joint_3": Motor(3, "sts3215", MotorNormMode.RANGE_M100_100),
                "joint_4": Motor(4, "sts3215", MotorNormMode.RANGE_M100_100),
                "joint_5": Motor(5, "sts3215", MotorNormMode.RANGE_M100_100),
            },
            calibration=self.calibration,
        )
        self.cameras = make_cameras_from_configs(config.cameras)

Paso 2: Definir las características de observación y acción

Estas dos propiedades definen el contrato de interfaz entre tu robot y las herramientas que lo consumen (como las tuberías de recopilación de datos o de aprendizaje).

Tenga en cuenta que estas propiedades deben ser invocables incluso si el robot aún no está conectado, por lo que evite depender del estado del hardware en tiempo de ejecución para definirlos.

observation_features

Esta propiedad debe devolver un diccionario que describa la estructura de las salidas de los sensores de tu robot. Las claves coinciden con lo que devuelve get_observation(), y los valores describen la forma (para matrices/imagenes) o el tipo (para valores simples).

Ejemplo para nuestro brazo de 5-DoF con una cámara:

@property
def _motors_ft(self) -> dict[str, type]:
    return {
        "joint_1.pos": float,
        "joint_2.pos": float,
        "joint_3.pos": float,
        "joint_4.pos": float,
        "joint_5.pos": float,
    }

@property
def _cameras_ft(self) -> dict[str, tuple]:
    return {
        cam: (self.cameras[cam].height, self.cameras[cam].width, 3) for cam in self.cameras
    }

@property
def observation_features(self) -> dict:
    return {**self._motors_ft, **self._cameras_ft}

En este caso, las observaciones consisten en un simple dict que almacena la posición de cada motor y una imagen de la cámara.

action_features

Esta propiedad describe los comandos que tu robot espera a través de send_action(). Nuevamente, las claves deben coincidir con el formato de entrada esperado, y los valores definen la forma/tipo de cada comando.

Aquí, simplemente usamos las mismas características proprioceptivas de las articulaciones (self._motors_ft) que con observation_features: la acción enviada será simplemente la posición objetivo para cada motor.

@property
def action_features(self) -> dict:
    return self._motors_ft

Paso 3: Manejar la conexión y la desconexión

Estos métodos deben gestionar la apertura y el cierre de la comunicación con su hardware (p. ej., puertos serie, interfaces CAN, dispositivos USB, cámaras).

is_connected

Esta propiedad debe reflejar simplemente que se ha establecido la comunicación con el hardware del robot. Cuando esta propiedad es True, debe ser posible leer y escribir en el hardware usando get_observation() y send_action().

@property
def is_connected(self) -> bool:
    return self.bus.is_connected and all(cam.is_connected for cam in self.cameras.values())

connect()

Este método debe establecer la comunicación con el hardware. Además, si su robot necesita calibración y no está calibrado, debe iniciar un procedimiento de calibración de forma predeterminada. Si su robot requiere alguna configuración específica, esta también debe llamarse aquí.

def connect(self, calibrate: bool = True) -> None:
    self.bus.connect()
    if not self.is_calibrated and calibrate:
        self.calibrate()

    for cam in self.cameras.values():
        cam.connect()

    self.configure()

disconnect()

Este método debe finalizar la comunicación con el hardware de manera ordenada: liberar los recursos relacionados (hilos o procesos), cerrar puertos, etc.

Aquí ya lo manejamos en nuestras clases SerialMotorsBus y Camera, por lo que solo necesitamos llamar a sus propios métodos disconnect():

def disconnect(self) -> None:
    self.bus.disconnect()
    for cam in self.cameras.values():
        cam.disconnect()

Paso 4: Soporte para calibración y configuración

LeRobot permite guardar y cargar datos de calibración de forma automática. Esto resulta útil para compensaciones de articulaciones, posiciones cero o alineación de sensores.

Tenga en cuenta que, dependiendo de su hardware, esto puede no aplicarse. Si ese es el caso, simplemente puede dejar estos métodos como no-ops:

@property
def is_calibrated(self) -> bool:
    return True

def calibrate(self) -> None:
    pass

is_calibrated

Esto debe reflejar si tu robot tiene la calibración requerida cargada.

@property
def is_calibrated(self) -> bool:
    return self.bus.is_calibrated

calibrate()

El objetivo de la calibración es doble:

  • Conoce el rango de movimiento físico de cada motor para enviar comandos únicamente dentro de ese rango.
  • Normalizar las posiciones crudas de los motores a valores continuos coherentes (por ejemplo, porcentajes, grados) en lugar de valores discretos arbitrarios que dependen del motor específico utilizado y que no se replicarán en otros contextos.

Debe implementar la lógica de calibración (si es relevante) y actualizar el diccionario self.calibration. Si está utilizando motores Feetech o Dynamixel, SerialMotorsBus ya ejecuta la calibración manualmente:

  1. _keep_calibration_file() ofrece reutilizar el archivo de calibración de este id. Si lo conservas, se escribe en los motores y la calibración termina ahí.
  2. record_calibration() libera los motores y los pone en el modo de operación que se le indique.
  3. Te pide que mantengas las articulaciones en el medio de su rango de movimiento: esa postura se convierte en media vuelta de cada motor.
  4. Registra el rango de cada articulación mientras la mueves, hasta que presionas ENTER. Las articulaciones que giran de forma continua se incluyen en full_turn_motors y obtienen el rango completo del codificador en su lugar; nuestro brazo de 5-DoF no tiene ninguna.
  5. write_calibration() escribe el resultado en los motores y _save_calibration() lo guarda en el archivo de calibración.
def calibrate(self) -> None:
    if self._keep_calibration_file(self.bus):
        return

    self.calibration = self.bus.record_calibration("position", full_turn_motors=[])
    self.bus.write_calibration(self.calibration)
    self._save_calibration()
    print("Calibration saved to", self.calibration_fpath)

configure()

Utilice esto para configurar cualquier parámetro de su hardware (modos de control de servos, ganancias del controlador, etc.). Normalmente, esto debe ejecutarse en el momento de la conexión y ser idempotente.

def configure(self) -> None:
    with self.bus.torque_disabled():
        self.bus.configure_motors()
        for motor in self.bus.motors:
            self.bus.set_operating_mode("position", motor)
            self.bus.write("P_Coefficient", motor, 16)
            self.bus.write("I_Coefficient", motor, 0)
            self.bus.write("D_Coefficient", motor, 32)

Paso 5: Implementar la lectura de sensores y el envío de acciones

Estas son las funciones de tiempo de ejecución más importantes: el bucle de E/S principal.

get_observation()

Devuelve un diccionario de valores de sensores del robot. Estos suelen incluir estados de motores, fotogramas de cámaras, varios sensores, etc. En el marco LeRobot, estas observaciones son las que se proporcionan a una política para predecir las acciones a realizar. Las claves y la estructura del diccionario deben coincidir con observation_features.

def get_observation(self) -> dict[str, Any]:
    if not self.is_connected:
        raise ConnectionError(f"{self} is not connected.")

    # Read arm position
    obs_dict = self.bus.sync_read("Present_Position")
    obs_dict = {f"{motor}.pos": val for motor, val in obs_dict.items()}

    # Capture images from cameras
    for cam_key, cam in self.cameras.items():
        obs_dict[cam_key] = cam.async_read()

    return obs_dict

send_action()

Toma un diccionario que coincide con action_features y lo envía a tu hardware. Puedes añadir límites de seguridad (recorte, suavizado) y devolver lo que se envió realmente.

Por simplicidad, no añadiremos ninguna modificación de las acciones en nuestro ejemplo.

def send_action(self, action: dict[str, Any]) -> dict[str, Any]:
    goal_pos = {key.removesuffix(".pos"): val for key, val in action.items()}

    # Send goal position to the arm
    self.bus.sync_write("Goal_Position", goal_pos)

    return action

Espera entre predicciones

El RTC local ordinario sigue siendo utilizable sin una nueva capacidad del robot. El despliegue remoto, las pausas de lenguaje asíncronas planificadas y el agotamiento de acciones recuperables requieren un contrato de espera con objetivo retenido. Opte por supports_position_hold = True solo cuando sea seguro para el controlador actualizar cada campo de acción a través de send_action(). Por lo general, esos campos son objetivos de posición reales. Un controlador puede optar por ello con un comando retenido validado por separado, como el token de movimiento latente de SONIC, cuando repetirlo mantiene al controlador a bordo en equilibrio. Habilitar el par o un sufijo .pos por sí solo no establece esto; los comandos de velocidad/esfuerzo y de locomoción siguen siendo inadecuados.

La implementación compartida de rollout actualiza la última acción finita completa devuelta por send_action(), incluyendo el agarre y cualquier recorte del controlador. Devuelve el objetivo realmente aplicado, no la solicitud original, cuando tu controlador lo modifica. Las actualizaciones de observación posteriores no reemplazan ese objetivo. Antes de que se haya aplicado cualquier comando, una instantánea de posición medida y validada lo inicializa. El robot puede estabilizarse hacia su objetivo retenido; este contrato detiene el avance de la trayectoria en lugar de garantizar una parada física instantánea. Valida el soporte de carga, el agarre y el comportamiento de actualización de comandos en el hardware para cada despliegue.

Cuando el modo de retención está habilitado, los procesadores de acciones personalizados y las correcciones de teleoperación deben preservar un comando de posición completo, incluido el agarre. El controlador debe devolver cada componente de la acción con valores aplicados finitos. Un retorno incompleto o no válido constituye un incumplimiento del contrato tras la aplicación del comando: el despliegue se detiene y suprime el retorno a la posición inicial en lugar de reutilizar un objetivo anterior. Los fallos de comando también suprimen el retorno a la posición inicial en el despliegue síncrono.

Remote rollout verifica automáticamente esta capacidad del controlador; no se necesita ninguna bandera de CLI adicional para el modo de retención. Las fallas de observación genéricas utilizan la ruta de limpieza de rollout normal. Volver a la posición inicial aún requiere una observación completa y puede fallar si una cámara sigue estando no disponible.

Implementación de robotsSoporte para espera de objetivo de posición
Seguidores de SO (SO-100/SO-101), OMX, KochSoportado.
Brazo y mano de HopeJrSoportado.
Rebot B601Soportado en los modos de brazo/agarrador de posición absoluta existentes.
OpenArmCompatible con use_velocity_and_torque=false; se excluyen los esquemas de acción mixtos.
BiSO, BiOpenArm, BiRebot B601Soportado cuando ambos brazos subyacentes lo soportan; sus objetivos aplicados conservan los prefijos izquierdo/derecho.
Reachy2Solo se admite con with_mobile_base=false, use_external_commands=false y max_relative_target=null. Los comandos de mobile-base son velocidades, el modo externo no transmite objetivos y la ruta de recorte actualmente devuelve los valores originales en lugar de los aplicados.
LeKiwi (incluido el cliente de red), EarthRover Mini PlusNo compatible: las acciones incluyen velocidades de la base móvil, que no pueden conservarse como objetivos de posición.
Unitree G1Soportado solo con SonicWholeBodyController: waiting repite el último token de movimiento de 64-D completo mientras el decodificador a bordo continúa equilibrando. La locomoción de GR00T/Holosoma y el control crudo de .q siguen sin ser compatibles.

Estas declaraciones reflejan el contrato de comandos del controlador, no la validación física de cada encarnación o configuración de controlador. El valor predeterminado genérico de Robot sigue siendo falso. Las comprobaciones de capacidad no establecen la compatibilidad de puntos de control, el ajuste del controlador, el soporte de carga ni la salud del hardware.

Añadiendo un teleoperador

Para implementar dispositivos de teleoperación, también proporcionamos una clase base Teleoperator. Esta clase es muy similar a la clase base Robot y tampoco asume nada sobre el factor de forma.

Las principales diferencias están en las funciones de E/S: un teleoperador permite generar una acción mediante get_action y puede recibir acciones de retroalimentación mediante send_feedback. La retroalimentación puede ser cualquier elemento controlable en el dispositivo de teleoperación que ayude a la persona que lo controla a comprender las consecuencias de las acciones enviadas. Por ejemplo, la retroalimentación de movimiento/fuerza en un brazo líder o las vibraciones en un controlador de videojuegos. Para implementar un teleoperador, puede seguir este mismo tutorial y adaptarlo para estos dos métodos.

Uso de tus propios dispositivos LeRobot 🔌

Puedes extender fácilmente lerobot con tu propio hardware personalizado, ya sea una cámara, un robot o un dispositivo de teleoperación, creando un paquete de Python separado e instalable. Si sigues algunas convenciones simples, las herramientas de línea de comandos de lerobot (como lerobot-teleoperate y lerobot-record) detectarán e integrarán automáticamente tus creaciones sin necesidad de realizar cambios en el código fuente de lerobot.

Esta guía describe las convenciones que debe seguir su plugin.

Las 4 convenciones principales

Para garantizar que su dispositivo personalizado sea detectable, debe cumplir con las siguientes cuatro reglas.

1 . Crear un paquete instalable con un prefijo específico

Su proyecto debe ser un paquete de Python estándar e instalable. Es fundamental que el nombre de su paquete (tal como se define en pyproject.toml o setup.py) comience con uno de estos prefijos:

  • lerobot_robot_ para un robot.
  • lerobot_camera_ para una cámara.
  • lerobot_teleoperator_ para un dispositivo de teleoperación.
  • lerobot_strategy_ para una estrategia lerobot-rollout (ver Usa tu propia estrategia).

Este sistema de prefijos es la forma en que lerobot encuentra automáticamente tu plugin en el entorno de Python.

2 . Sigue el patrón de nomenclatura SomethingConfig / Something

La clase de implementación de su dispositivo debe nombrarse según su clase de configuración, simplemente eliminando el sufijo Config.

  • Clase de configuración: MyAwesomeTeleopConfig
  • Clase de dispositivo: MyAwesomeTeleop

3. Coloque sus archivos en una estructura predecible

La clase de dispositivo (MyAwesomeTeleop) debe estar ubicada en un módulo predecible relativo a su clase de configuración (MyAwesomeTeleopConfig). lerobot buscará automáticamente en estas ubicaciones:

  • En el mismo módulo que la clase de configuración.
  • En un submódulo con el nombre del dispositivo (p. ej., my_awesome_teleop.py).

La estructura recomendada y más sencilla es colocarlos en archivos separados y claramente nombrados dentro del mismo directorio.

4 . Exponer clases en __init__.py

El archivo __init__.py de su paquete debe importar y exponer tanto la clase de configuración como la de dispositivo, para que sean fácilmente accesibles.

Poniéndolo todo en práctica: un ejemplo completo

Creemos un nuevo teleoperador llamado my_awesome_teleop.

Estructura del directorio

Así es como debería verse la carpeta del proyecto. El nombre del paquete, lerobot_teleoperator_my_awesome_teleop, sigue la Convención #1.

lerobot_teleoperator_my_awesome_teleop/
├── pyproject.toml # (or setup.py) lists lerobot as a dependency
└── lerobot_teleoperator_my_awesome_teleop/
    ├── __init__.py
    ├── config_my_awesome_teleop.py
    └── my_awesome_teleop.py

Contenido del archivo

  • config_my_awesome_teleop.py: Define la clase de configuración. Fíjate en el sufijo Config (Convención #2).

    from dataclasses import dataclass
    
    from lerobot.teleoperators.config import TeleoperatorConfig
    
    @TeleoperatorConfig.register_subclass("my_awesome_teleop")
    @dataclass
    class MyAwesomeTeleopConfig(TeleoperatorConfig):
        # Your configuration fields go here
        port: str = "192.168.1.1"
  • my_awesome_teleop.py: Implementa el dispositivo. El nombre de la clase MyAwesomeTeleop coincide con el nombre de su clase de configuración (Convención #2). Esta estructura de archivos cumple con la Convención #3.

    from lerobot.teleoperators.teleoperator import Teleoperator
    
    from .config_my_awesome_teleop import MyAwesomeTeleopConfig
    
    class MyAwesomeTeleop(Teleoperator):
        config_class = MyAwesomeTeleopConfig
        name = "my_awesome_teleop"
    
        def __init__(self, config: MyAwesomeTeleopConfig):
            super().__init__(config)
            self.config = config
    
        # Your device logic (e.g., connect) goes here
  • __init__.py: Expone las clases principales (Convención #4).

    from .config_my_awesome_teleop import MyAwesomeTeleopConfig
    from .my_awesome_teleop import MyAwesomeTeleop

Instalación y uso

  1. Instala tu nuevo plugin en tu entorno de Python. Puedes instalar tu paquete de plugin local usando el modo editable de pip o desde PyPi.

    # Locally
    # Navigate to your plugin's root directory and install it
    cd lerobot_teleoperator_my_awesome_teleop
    pip install -e .
    
    # From PyPi
    pip install lerobot_teleoperator_my_awesome_teleop
  2. Úsalo directamente desde la línea de comandos. Ahora, puedes usar tu dispositivo personalizado haciendo referencia a su tipo.

    # Add the other arguments you need (robot type, ports, ids, ...)
    lerobot-teleoperate --teleop.type=my_awesome_teleop ...

¡Y eso es todo! Tu dispositivo personalizado ahora está completamente integrado.

¿Busca un ejemplo?

La comunidad mantiene decenas de plugins construidos de esta manera. Explora Third-Party Robots & Teleoperators y Third-Party Cameras & Sensors para encontrar uno cercano a tu hardware, o comienza con estos dos paquetes pequeños:

Conclusión

Una vez que tu clase de robot esté completa, puedes aprovechar el ecosistema de LeRobot:

  • Controla tu robot con los teleoperadores disponibles o integra directamente tu dispositivo de teleoperación
  • Registrar datos de entrenamiento y visualizarlos
  • Integrarlo en pipelines de aprendizaje por refuerzo o aprendizaje por imitación

Una vez que tu plugin esté publicado, abre un PR para añadirlo a Third-Party Robots & Teleoperators o Third-Party Cameras & Sensors para que otros usuarios puedan encontrarlo.

No dudes en pedir ayuda a la comunidad en nuestro Discord 🤗

Update on GitHub