LeRobot documentation
Traga o seu próprio hardware
Traga o seu próprio hardware
Este tutorial explicará como integrar seu próprio design de robô ao ecossistema LeRobot e permitir que ele acesse todas as nossas ferramentas (coleta de dados, pipelines de controle, treinamento e inferência de políticas).
Para isso, fornecemos a classe base Robot no LeRobot, que especifica uma interface padrão para a integração de robôs físicos. Vamos ver como implementá-la.
Pré-requisitos
- Seu próprio robô que expõe uma interface de comunicação (por exemplo, serial, CAN, TCP)
- Uma forma de ler dados de sensores e enviar comandos de motor programaticamente, por exemplo, o SDK ou a API do fabricante, ou sua própria implementação de protocolo.
- LeRobot instalado no seu ambiente. Siga nosso Guia de Instalação.
Escolha seus motores
Se você estiver usando motores Feetech ou Dynamixel, o LeRobot fornece uma interface de barramento integrada, SerialMotorsBus, que controla qualquer uma das famílias: o modelo de cada motor informa qual é. Consulte-a para conhecer sua API. Ela requer o extra serial-motors, que instala o rustypot: pip install 'lerobot[serial-motors]', ou pip install -e ".[serial-motors]" a partir de um checkout de código-fonte.
Para um bom exemplo de como pode ser usado, você pode dar uma olhada na nossa própria implementação de seguidor SO101
Use estes se forem compatíveis. Caso contrário, você precisará encontrar ou escrever uma interface em Python (não abordado neste tutorial):
- Encontre um SDK existente em Python (ou use bindings para C/C++)
- Ou implemente um wrapper básico de comunicação (por exemplo, via pyserial, socket ou CANopen)
Você não está sozinho—muitas contribuições da comunidade usam placas ou firmwares personalizados!
Para Feetech e Dynamixel, atualmente suportamos estes servos:
- Séries STS e SMS da Feetech:
sts3215,sts3250,sm8512bl - Série SCS da Feetech:
scs0009 - Dynamixel série 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
Os modelos podem compartilhar um barramento, servos STS/SMS e SCS ou XL430 e XL330: cada motor é lido e escrito por meio da definição do próprio modelo, incluindo a ordem dos bytes e a codificação do sinal. As diferenças entre as famílias derivam das mesmas definições: qual valor de modo de operação significa controle de posição (bus.set_operating_mode("position")), se o deslocamento de referência (homing offset) é somado à posição ou subtraído dela, e quais registros existem.
Suportar um modelo não significa que todos os auxiliares de calibração funcionem nele: o Feetech SCS0009 não possui nem um modo de operação nem um deslocamento de retorno à origem, portanto set_operating_mode() e set_half_turn_homings() o rejeitam. A receita de calibração abaixo é para servos que possuem ambos, como o STS; um servo SCS é calibrado apenas pelo seu intervalo.
Se você estiver usando servos Feetech ou Dynamixel que não estão nesta lista, adicione-os ao rustypot, que contém as tabelas de controle lidas pelos barramentos: um modelo que compartilha uma tabela de controle existente apenas precisa ter seu número de modelo registrado, enquanto um novo modelo requer uma definição de servo (veja o guia de contribuição de servos). O LeRobot lê os modelos do pacote Python rustypot instalado, portanto, ele deve incluir o novo modelo: uma liberação do rustypot que o contenha, ou o rustypot compilado a partir do código-fonte.
Nas próximas seções, os exemplos usam motores Feetech. Altere os modelos e adapte-os aos seus motores, se necessário.
Etapa 1: Subclassifique a Interface do Robô
Primeiro, você precisa especificar a classe de configuração e um identificador de string (name) para o seu robô. Se o seu robô tiver necessidades especiais que você queira poder alterar facilmente, elas devem ser incluídas aqui (por exemplo, porta/endereço, baudrate).
Aqui, adicionaremos o nome da porta e uma câmera por padrão para o nosso robô:
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âmeras para entender como detectar e adicionar sua câmera.
A seguir, criaremos nossa classe de robô real, que herda de Robot. Esta classe abstrata define um contrato que você deve seguir para que seu robô seja utilizável com as demais ferramentas do LeRobot.
Aqui, criaremos um robô simples de 5-DoF com uma câmera. Ele poderia ser um braço simples, mas observe que a classe abstrata Robot não faz suposições sobre o formato do seu robô. Você pode deixar sua imaginação correr solta ao projetar novos robôs!
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)Etapa 2: Defina os recursos de observação e ação
Essas duas propriedades definem o contrato de interface entre seu robô e as ferramentas que o consomem (como pipelines de coleta de dados ou aprendizado).
Observe que essas propriedades devem ser chamáveis mesmo que o robô ainda não esteja conectado, portanto, evite depender do estado do hardware em tempo de execução para defini-las.
observation_features
Esta propriedade deve retornar um dicionário descrevendo a estrutura das saídas dos sensores do seu robô. As chaves correspondem ao que get_observation() retorna, e os valores descrevem a forma (para arrays/imagens) ou o tipo (para valores simples).
Exemplo para nosso braço de 5-DoF com uma câmera:
@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}Neste caso, as observações consistem em um dicionário simples que armazena a posição de cada motor e uma imagem da câmera.
action_features
Esta propriedade descreve os comandos que seu robô espera via send_action(). Novamente, as chaves devem corresponder ao formato de entrada esperado, e os valores definem a forma/tipo de cada comando.
Aqui, usamos simplesmente os mesmos recursos proprioceptivos das juntas (self._motors_ft) do que com observation_features: a ação enviada será simplesmente a posição de destino para cada motor.
@property
def action_features(self) -> dict:
return self._motors_ftEtapa 3: Gerenciar conexão e desconexão
Esses métodos devem lidar com a abertura e o fechamento da comunicação com seu hardware (por exemplo, portas seriais, interfaces CAN, dispositivos USB, câmeras).
is_connected
Esta propriedade deve simplesmente refletir que a comunicação com o hardware do robô foi estabelecida. Quando esta propriedade é True, deve ser possível ler e gravar no hardware usando get_observation() e 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 deve estabelecer a comunicação com o hardware. Além disso, se o seu robô precisar de calibração e não estiver calibrado, ele deve iniciar um procedimento de calibração por padrão. Se o seu robô precisar de alguma configuração específica, esta também deve ser chamada aqui.
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 deve encerrar a comunicação com o hardware de forma adequada: liberar quaisquer recursos relacionados (threads ou processos), fechar portas, etc.
Aqui, já tratamos disso em nossas classes SerialMotorsBus e Camera, então precisamos apenas chamar seus próprios métodos disconnect():
def disconnect(self) -> None:
self.bus.disconnect()
for cam in self.cameras.values():
cam.disconnect()Etapa 4: Suporte à Calibração e Configuração
O LeRobot suporta o salvamento e o carregamento automáticos de dados de calibração. Isso é útil para deslocamentos de juntas, posições zero ou alinhamento de sensores.
Observe que, dependendo do seu hardware, isso pode não se aplicar. Se for esse o caso, você pode simplesmente deixar esses métodos como no-ops:
@property
def is_calibrated(self) -> bool:
return True
def calibrate(self) -> None:
passis_calibrated
Isso deve refletir se o seu robô tem a calibração necessária carregada.
@property
def is_calibrated(self) -> bool:
return self.bus.is_calibratedcalibrate()
O objetivo da calibração é duplo:
- Conheça a amplitude de movimento física de cada motor para enviar comandos apenas dentro dessa faixa.
- Normalize as posições brutas dos motores para valores contínuos sensatos (por exemplo, porcentagens, graus) em vez de valores discretos arbitrários dependentes do motor específico usado, que não serão replicados em outros contextos.
Ele deve implementar a lógica de calibração (se relevante) e atualizar o dicionário self.calibration. Se você estiver usando motores Feetech ou Dynamixel, o SerialMotorsBus já executa a calibração manualmente:
_keep_calibration_file()oferece a opção de reutilizar o arquivo de calibração deste id. Se você mantiver, ele é gravado nos motores e a calibração termina ali.record_calibration()libera os motores e os coloca no modo de operação que você especificar.- Ele pede que você segure as articulações no meio de sua amplitude de movimento: essa pose se torna meia volta de cada motor.
- Ele registra a amplitude de cada articulação conforme você a move, até que você pressione ENTER. Articulações que giram continuamente ficam em
full_turn_motorse recebem a faixa inteira do codificador; nosso braço de 5-DoF não possui nenhuma. write_calibration()grava o resultado nos motores e_save_calibration()o salva no arquivo de calibração.
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()
Use isso para configurar qualquer configuração para seu hardware (modos de controle dos servos, ganhos do controlador, etc.). Isso geralmente deve ser executado no momento da conexão e 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)Etapa 5: Implementar a leitura de sensores e o envio de ações
Estas são as funções de tempo de execução mais importantes: o loop central de E/S.
get_observation()
Retorna um dicionário de valores de sensores do robô. Esses valores geralmente incluem estados dos motores, quadros da câmera, vários sensores, etc. No framework LeRobot, essas observações são o que será fornecido a uma política para prever as ações a serem executadas. As chaves e a estrutura do dicionário devem corresponder a 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()
Recebe um dicionário que corresponde a action_features e o envia para o seu hardware. Você pode adicionar limites de segurança (clipping, suavização) e retornar o que foi efetivamente enviado.
Por simplicidade, não adicionaremos nenhuma modificação das ações em nosso exemplo aqui.
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 actionAguardando entre as previsões
O RTC local comum continua utilizável sem uma nova capacidade do robô. O rollout remoto, as pausas de linguagem assíncronas planejadas e a inanição de ação recuperável exigem um contrato de espera com alvo retido. Opte por supports_position_hold = True apenas quando atualizar todos os campos de ação por meio de send_action() for seguro para o controlador. Normalmente, esses campos são alvos de posição reais. Um controlador pode, em vez disso, optar por um comando retido validado separadamente, como o token de movimento latente do SONIC, quando repeti-lo mantém o controlador embarcado em equilíbrio. A ativação de torque ou um sufixo .pos sozinho não estabelece isso; comandos de velocidade/esforço e locomoção permanecem inadequados.
Um robô que pode aguardar sem um comando de reenvio, como um simulador que avança apenas em send_action(), define supports_command_hold = True e implementa hold(). Ele é usado no lugar da retenção de posição.
A implementação compartilhada de rollout atualiza a última ação finita completa retornada por send_action(), incluindo o garra e qualquer recorte do driver. Retorne o alvo realmente aplicado, e não a solicitação original, quando o seu driver o modificar. As atualizações subsequentes de observação não substituem esse alvo. Antes que qualquer comando tenha sido aplicado, um instantâneo de posição medida validado o inicializa. O robô pode se estabilizar em direção ao seu alvo retido; esse contrato interrompe o avanço da trajetória em vez de garantir uma parada física instantânea. Valide o suporte de carga, a pegada e o comportamento de atualização de comando no hardware para cada implantação.
Quando o holding está habilitado, os processadores de ações personalizados e as correções de teleoperação devem preservar um comando de posição completo, incluindo o gripper. O driver deve retornar todos os componentes da ação com valores aplicados finitos. Um retorno incompleto/inválido é uma falha de contrato após a aplicação do comando: o rollout é interrompido e o homing é suprimido em vez de reutilizar um alvo anterior. Falhas de comando também suprimem o homing no rollout síncrono.
O rollout remoto verifica automaticamente essa capacidade do driver; não é necessário nenhum flag de CLI adicional para o modo de espera. Falhas genéricas de observação usam o caminho normal de limpeza do rollout. Voltar à posição inicial ainda requer uma observação completa e pode falhar se uma câmera permanecer indisponível.
| Implementação do robô | Suporte à espera de posição-alvo |
|---|---|
| SO followers (SO-100/SO-101), OMX, Koch | Suportado. |
| HopeJr braço e mão | Suportado. |
| Rebot B601 | Suportado em todos os modos de braço/presa de posição absoluta existentes. |
| YAM | Suportado. Se nenhum comando chegar em command_timeout_s, o braço mantém sua posição ou flutua, conforme definido por idle_mode. |
| OpenArm | Suportado com use_velocity_and_torque=false; esquemas de ação mistos são excluídos. |
| BiSo, BiOpenArm, BiRebot B601, BiYAM | Suportado quando ambos os braços subjacentes o suportam; seus alvos aplicados mantêm os prefixos esquerdo/direito. |
| Reachy2 | Suportado apenas com with_mobile_base=false, use_external_commands=false e max_relative_target=null. Os comandos do mobile-base são velocidades, o modo externo não transmite alvos e o caminho de clipping atualmente retorna os valores originais em vez dos aplicados. |
| LeKiwi (incluindo cliente de rede), EarthRover Mini Plus | Não suportado: as ações incluem velocidades da base móvel, que não podem ser mantidas como alvos de posição. |
| Unitree G1 | Suportado apenas com SonicWholeBodyController: a espera repete o último token de movimento 64-D completo enquanto o decodificador embarcado continua o equilíbrio. A locomoção GR00T/Holosoma e o controle bruto .q permanecem sem suporte. |
Essas declarações refletem o contrato de comandos do driver, não a validação física de cada encarnação ou configuração de controlador. O padrão genérico de Robot permanece como falso. As verificações de capacidade não estabelecem compatibilidade de checkpoint, ajuste de controlador, suporte a carga ou integridade do hardware.
Adicionando um Teleoperador
Para a implementação de dispositivos de teleoperação, também fornecemos a classe base Teleoperator. Esta classe é muito semelhante à classe base Robot e também não faz suposições sobre o formato físico.
As principais diferenças estão nas funções de E/S: um teleoperador permite que você produza uma ação via get_action e pode receber ações de feedback via send_feedback. O feedback pode ser qualquer coisa controlável no dispositivo de teleoperação que possa ajudar a pessoa que o controla a entender as consequências das ações enviadas. Pense em feedback de movimento/força em um braço líder, vibrações em um controle de videogame, por exemplo. Para implementar um teleoperador, você pode seguir este mesmo tutorial e adaptá-lo para esses dois métodos.
Usando Seus Próprios Dispositivos LeRobot 🔌
Você pode estender facilmente o lerobot com seu próprio hardware personalizado — seja uma câmera, um robô ou um dispositivo de teleoperação — criando um pacote Python separado e instalável. Se você seguir algumas convenções simples, as ferramentas de linha de comando do lerobot (como lerobot-teleoperate e lerobot-record) descobrirão e integrarão automaticamente suas criações sem exigir nenhuma alteração no código-fonte do lerobot.
Este guia descreve as convenções que seu plugin deve seguir.
As 4 Convenções Principais
Para garantir que seu dispositivo personalizado seja detectável, você deve seguir as quatro regras a seguir.
1 . Crie um pacote instalável com um prefixo específico
Seu projeto deve ser um pacote Python padrão e instalável. Crucialmente, o nome do seu pacote (conforme definido em pyproject.toml ou setup.py) deve começar com um desses prefixos:
lerobot_robot_para um robô.lerobot_camera_para uma câmera.lerobot_teleoperator_para um dispositivo de teleoperação.lerobot_strategy_para uma estratégialerobot-rollout(veja Traga sua própria estratégia).
Esse sistema de prefixo é como o lerobot encontra automaticamente seu plugin no ambiente Python.
2 . Siga o padrão de nomenclatura SomethingConfig / Something
A classe de implementação do seu dispositivo deve ser nomeada com base na sua classe de configuração, simplesmente removendo o sufixo Config.
- Classe de configuração:
MyAwesomeTeleopConfig - Classe de dispositivo:
MyAwesomeTeleop
3. Organize seus arquivos em uma estrutura previsível
A classe de dispositivo (MyAwesomeTeleop) deve estar localizada em um módulo previsível em relação à sua classe de configuração (MyAwesomeTeleopConfig). O lerobot buscará automaticamente nestes locais:
- No mesmo módulo que a classe de configuração.
- Em um submódulo com o nome do dispositivo (por exemplo,
my_awesome_teleop.py).
A estrutura recomendada e mais simples é colocá-los em arquivos separados, com nomes claros, dentro do mesmo diretório.
4 . Expor classes em __init__.py
O arquivo __init__.py do seu pacote deve importar e expor tanto a classe de configuração quanto a classe de dispositivo, tornando-as facilmente acessíveis.
Juntando tudo: um exemplo completo
Vamos criar um novo teleoperador chamado my_awesome_teleop.
Estrutura de Diretórios
É assim que a pasta do projeto deve ficar. O nome do pacote, lerobot_teleoperator_my_awesome_teleop, segue a Convenção #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.pyConteúdo do Arquivo
config_my_awesome_teleop.py: Define a classe de configuração. Observe o sufixoConfig(Convenção #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 o dispositivo. O nome da classeMyAwesomeTeleopcorresponde ao nome da classe de configuração (Convenção #2). Essa estrutura de arquivo segue a Convenção #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: Expõe as classes principais (Convenção #4).from .config_my_awesome_teleop import MyAwesomeTeleopConfig from .my_awesome_teleop import MyAwesomeTeleop
Instalação e Uso
Instale seu novo plugin no seu ambiente Python. Você pode instalar seu pacote de plugin local usando o modo editável do
pipou a partir do 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_teleopUse-o diretamente da linha de comando. Agora, você pode usar seu dispositivo personalizado referenciando seu tipo.
# Add the other arguments you need (robot type, ports, ids, ...) lerobot-teleoperate --teleop.type=my_awesome_teleop ...
E é isso! Seu dispositivo personalizado agora está totalmente integrado.
Procurando um exemplo?
A comunidade mantém dezenas de plugins construídos dessa forma. Navegue por Robôs e Teleoperadores de Terceiros e Câmeras e Sensores de Terceiros para encontrar um próximo ao seu hardware, ou comece por estes dois pequenos pacotes:
- https://github.com/SpesRobotics/lerobot-robot-xarm
- https://github.com/SpesRobotics/lerobot-teleoperator-teleop
Conclusão
Quando sua classe de robô estiver completa, você pode aproveitar o ecossistema do LeRobot:
- Controle seu robô com os teleoperadores disponíveis ou integre diretamente seu dispositivo de teleoperação
- Registre os dados de treinamento e visualize-os
- Integre-o em pipelines de aprendizado por reforço ou aprendizado por imitação
Depois que seu plugin for publicado, abra um PR adicionando-o a Robôs e Teleoperadores de Terceiros ou Câmeras e Sensores de Terceiros para que outros usuários possam encontrá-lo.
Não hesite em procurar ajuda da comunidade em nosso Discord 🤗
Update on GitHub