LeRobot documentation
Usa tu propio hardware
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_ftPaso 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:
passis_calibrated
Esto debe reflejar si tu robot tiene la calibración requerida cargada.
@property
def is_calibrated(self) -> bool:
return self.bus.is_calibratedcalibrate()
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:
_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í.record_calibration()libera los motores y los pone en el modo de operación que se le indique.- Te pide que mantengas las articulaciones en el medio de su rango de movimiento: esa postura se convierte en media vuelta de cada motor.
- 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_motorsy obtienen el rango completo del codificador en su lugar; nuestro brazo de 5-DoF no tiene ninguna. 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_dictsend_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 actionEspera 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 robots | Soporte para espera de objetivo de posición |
|---|---|
| Seguidores de SO (SO-100/SO-101), OMX, Koch | Soportado. |
| Brazo y mano de HopeJr | Soportado. |
| Rebot B601 | Soportado en los modos de brazo/agarrador de posición absoluta existentes. |
| OpenArm | Compatible con use_velocity_and_torque=false; se excluyen los esquemas de acción mixtos. |
| BiSO, BiOpenArm, BiRebot B601 | Soportado cuando ambos brazos subyacentes lo soportan; sus objetivos aplicados conservan los prefijos izquierdo/derecho. |
| Reachy2 | Solo 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 Plus | No compatible: las acciones incluyen velocidades de la base móvil, que no pueden conservarse como objetivos de posición. |
| Unitree G1 | Soportado 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 estrategialerobot-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.pyContenido del archivo
config_my_awesome_teleop.py: Define la clase de configuración. Fíjate en el sufijoConfig(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 claseMyAwesomeTeleopcoincide 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
Instala tu nuevo plugin en tu entorno de Python. Puedes instalar tu paquete de plugin local usando el modo editable de
pipo 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Ú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:
- https://github.com/SpesRobotics/lerobot-robot-xarm
- https://github.com/SpesRobotics/lerobot-teleoperator-teleop
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