LeRobot documentation
Собственное оборудование
Собственное оборудование
В этом руководстве объясняется, как интегрировать собственный дизайн робота в экосистему LeRobot и обеспечить ему доступ ко всем нашим инструментам (сбор данных, конвейеры управления, обучение и инференс политик).
В этой цели мы предоставляем базовый класс Robot в LeRobot, который определяет стандартный интерфейс для интеграции с физическими роботами. Рассмотрим, как его реализовать.
Предварительные требования
- Ваш собственный робот, предоставляющий интерфейс связи (например, последовательный порт, CAN, TCP)
- Способ программно считывать данные с датчиков и отправлять команды на приводы, например, SDK или API производителя, либо собственная реализация протокола.
- LeRobot установлен в вашем окружении. Следуйте нашему Руководству по установке.
Выберите свои моторы
Если вы используете моторы Feetech или Dynamixel, LeRobot предоставляет встроенный интерфейс шины SerialMotorsBus, который управляет любым из этих типов: модель каждого мотора определяет, к какой семье он относится. Обратитесь к документации, чтобы узнать о его API. Для работы требуется дополнительный пакет serial-motors, который устанавливает rustypot: pip install 'lerobot[serial-motors]' или pip install -e ".[serial-motors]" из исходного кода.
Хороший пример того, как его можно использовать, — наша собственная реализация SO101 follower
Используйте их, если они совместимы. В противном случае вам потребуется найти или написать интерфейс на Python (это не рассматривается в данном руководстве):
- Найдите существующий SDK на Python (или используйте привязки к C/C++)
- Или реализуйте базовый обёрточный модуль для коммуникации (например, с помощью pyserial, socket или CANopen)
Вы не одиноки — многие вносимые сообществом изменения используют кастомные платы или прошивки!
Для Feetech и Dynamixel в настоящее время поддерживаются следующие сервоприводы:
- Серии Feetech STS & SMS:
sts3215,sts3250,sm8512bl - Feetech серия SCS:
scs0009 - Dynamixel X series (протокол 2.0):
xl330-m077,xl330-m288,xc330-t181,xc330-t288,xl430-w250,xl430-w250-2,xc430-w150,xm430-w350,xm540-w270,xh540-w150
Модели могут использовать общий шину, сервоприводы STS/SMS и SCS или XL430 и XL330: каждый мотор читается и записывается через определение своей модели, включая порядок байтов и кодирование знака. Различия между семействами также проистекают из этих определений: какое значение режима работы означает позиционное управление (bus.set_operating_mode("position")), прибавляется ли смещение к позиции или вычитается из неё, какие регистры существуют.
Поддержка модели не означает, что все вспомогательные функции калибровки с ней работают: у Feetech SCS0009 нет ни режима работы, ни смещения точки возврата, поэтому set_operating_mode() и set_half_turn_homings() отклоняют её. Приведённый ниже рецепт калибровки предназначен для сервоприводов, имеющих оба параметра, например STS; сервопривод SCS калибруется только по его диапазону.
Если вы используете сервоприводы Feetech или Dynamixel, которых нет в этом списке, добавьте их в rustypot, где хранятся таблицы управления, которые читают шины: для модели, использующей существующую таблицу управления, достаточно зарегистрировать её номер модели, а для новой модели требуется определение сервопривода (см. руководство по внесению сервоприводов). LeRobot читает модели из установленного Python-пакета rustypot, поэтому он должен включать новую модель: либо релиз rustypot с этой моделью, либо rustypot, собранный из исходного кода.
В следующих разделах в примерах используются моторы Feetech. При необходимости замените модели и адаптируйте код под свои моторы.
Шаг 1: Наследование интерфейса Robot
Сначала необходимо указать класс конфигурации и строковый идентификатор (name) для вашего робота. Если у вашего робота есть особые требования, которые вы хотели бы легко изменять, их следует указать здесь (например, порт/адрес, скорость передачи данных).
Здесь мы по умолчанию добавим имя порта и одну камеру для нашего робота:
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,
),
}
)Руководство по камерам, чтобы узнать, как обнаружить и добавить вашу камеру.
Далее мы создадим наш фактический класс робота, который наследуется от Robot. Этот абстрактный класс определяет контракт, который вы должны соблюдать, чтобы ваш робот был совместим с остальными инструментами LeRobot.
Здесь мы создадим простого робота с 5 степенями свободы и одной камерой. Это может быть простая манипуляторная рука, но обратите внимание, что абстрактный класс Robot не делает никаких предположений о форм-факторе вашего робота. Вы можете дать волю воображению при проектировании новых роботов!
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)Шаг 2: Определите признаки наблюдений и действий
Эти два свойства определяют контракт интерфейса между вашим роботом и инструментами, которые его используют (например, конвейеры сбора данных или обучения).
Обратите внимание, что эти свойства должны быть вызываемыми, даже если робот ещё не подключён, поэтому избегайте использования состояния аппаратного обеспечения во время выполнения для их определения.
observation_features
Это свойство должно возвращать словарь, описывающий структуру выходов датчиков вашего робота. Ключи соответствуют тому, что возвращает get_observation(), а значения описывают либо форму (для массивов/изображений), либо тип (для простых значений).
Пример для нашей 5-DoF руки с одной камерой:
@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}В этом случае наблюдения представляют собой простой dict, хранящий позицию каждого мотора и изображение с камеры.
action_features
Это свойство описывает команды, которые ваш робот ожидает через send_action(). Как и прежде, ключи должны соответствовать ожидаемому формату входных данных, а значения определяют форму/тип каждой команды.
Здесь мы просто используем те же проприоцептивные признаки суставов (self._motors_ft), что и для observation_features: отправляемое действие будет просто целевым положением для каждого мотора.
@property
def action_features(self) -> dict:
return self._motors_ftШаг 3: Обработка подключения и отключения
Эти методы должны управлять открытием и закрытием связи с вашим оборудованием (например, последовательными портами, интерфейсами CAN, USB-устройствами, камерами).
is_connected
Это свойство должно просто отражать факт установления связи с аппаратным обеспечением робота. Когда это свойство равно True, должно быть возможно читать и записывать данные в аппаратное обеспечение с помощью get_observation() и 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()
Этот метод должен установить связь с аппаратным обеспечением. Кроме того, если ваш робот требует калибровки и она ещё не выполнена, по умолчанию следует запустить процедуру калибровки. Если вашему роботу требуется определённая конфигурация, её также следует инициализировать здесь.
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()
Этот метод должен корректно завершать взаимодействие с оборудованием: освобождать связанные ресурсы (потоки или процессы), закрывать порты и т. д.
Здесь мы уже обрабатываем это в наших классах SerialMotorsBus и Camera, поэтому нам достаточно вызвать их собственные методы disconnect():
def disconnect(self) -> None:
self.bus.disconnect()
for cam in self.cameras.values():
cam.disconnect()Шаг 4: Поддержка калибровки и конфигурации
LeRobot поддерживает автоматическое сохранение и загрузку данных калибровки. Это полезно для смещений суставов, нулевых положений или выравнивания датчиков.
Обратите внимание, что в зависимости от вашего оборудования это может не применяться. В таком случае вы можете просто оставить эти методы как no-op:
@property
def is_calibrated(self) -> bool:
return True
def calibrate(self) -> None:
passis_calibrated
Это должно отражать, загружена ли у вашего робота необходимая калибровка.
@property
def is_calibrated(self) -> bool:
return self.bus.is_calibratedcalibrate()
Цель калибровки двойная:
- Определите физический диапазон движения каждого мотора, чтобы отправлять команды только в пределах этого диапазона.
- Нормализуйте сырые значения положения моторов до осмысленных непрерывных величин (например, процентов или градусов), а не до произвольных дискретных значений, зависящих от конкретного используемого мотора и не воспроизводимых в других условиях.
Он должен реализовывать логику калибровки (если это применимо) и обновлять словарь self.calibration. Если вы используете моторы Feetech или Dynamixel, SerialMotorsBus уже выполняет калибровку вручную:
_keep_calibration_file()предлагает переиспользовать файл калибровки с этим идентификатором. Если вы сохраните его, он будет записан в моторы, и калибровка завершится.record_calibration()освобождает моторы и переводит их в указанный вами режим работы.- Система просит удерживать суставы в середине диапазона движения: эта поза становится половиной оборота каждого мотора.
- Он фиксирует диапазон каждого сустава по мере его перемещения, пока вы не нажмёте ENTER. Суставы, вращающиеся бесконечно, указываются в
full_turn_motorsи получают весь диапазон энкодера вместо этого; в нашей 5-DoF руке их нет. write_calibration()записывает результат в моторы, а_save_calibration()сохраняет его в файл калибровки.
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()
Используйте этот метод для настройки любой конфигурации вашего оборудования (режимы управления сервоприводами, коэффициенты усиления контроллера и т. д.). Обычно это следует выполнять при подключении, и операция должна быть идемпотентной.
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)Шаг 5: Реализация чтения данных с датчиков и отправки действий
Это самые важные функции времени выполнения: основной цикл ввода-вывода.
get_observation()
Возвращает словарь значений датчиков робота. Обычно он включает состояния моторов, кадры с камер, различные датчики и т. д. В фреймворке LeRobot эти наблюдения передаются в политику для предсказания действий. Ключи и структура словаря должны соответствовать 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()
Принимает словарь, соответствующий action_features, и отправляет его на ваше оборудование. Можно добавить ограничения безопасности (обрезание, сглаживание) и вернуть то, что было фактически отправлено.
Для простоты в нашем примере мы не будем добавлять никаких модификаций действий.
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Ожидание между предсказаниями
Обычный локальный RTC остаётся доступным без нового роботового функционала. Для удалённого развёртывания, запланированных асинхронных пауз языка и восстанавливаемого голодания действий требуется контракт ожидания с удержанием целевого состояния. Включите опцию supports_position_hold = True только в том случае, если обновление каждого поля действия через send_action() безопасно для контроллера. Как правило, эти поля представляют собой фактические целевые позиции. Контроллер может вместо этого включить опцию с отдельно валидированной удерживаемой командой, например латентным токеном движения SONIC, если её повторение позволяет бортовому контроллеру сохранять баланс. Одного лишь включения момента или суффикса .pos недостаточно для установления этого; команды скорости/усилия и локомоции по-прежнему не подходят.
Реализация общего rollout обновляет последнее полное конечное действие, возвращённое send_action(), включая захват и любое обрезание со стороны драйвера. Если драйвер изменяет целевое значение, возвращайте фактически применённую цель, а не исходный запрос. Последующие обновления наблюдений не заменяют эту цель. До применения любой команды её инициализирует валидированный снимок измеренного положения. Робот может стабилизироваться в направлении удерживаемой цели; данное соглашение останавливает продвижение по траектории, но не гарантирует мгновенной физической остановки. Для каждого развёртывания проверяйте поддержку нагрузки, захват и поведение обновления команд на аппаратном уровне.
Когда удержание включено, пользовательские обработчики действий и корректировки телеуправления должны сохранять полную команду позиции, включая захват. Драйвер должен возвращать каждый компонент действия с конечными применёнными значениями. Неполный или недействительный возврат является нарушением контракта после применения команды: rollout останавливается и подавляет возврат в исходное положение, а не переиспользует более старую цель. Сбои команд также подавляют возврат в исходное положение в синхронном rollout.
Remote rollout автоматически проверяет эту возможность драйвера; дополнительный флаг CLI для режима удержания не требуется. Общие сбои наблюдения используют стандартный путь очистки rollout. Возврат в начальное положение по-прежнему требует полного наблюдения и может завершиться ошибкой, если камера остаётся недоступной.
| Реализация робота | Поддержка ожидания по позиции |
|---|---|
| SO followers (SO-100/SO-101), OMX, Koch | Поддерживается. |
| Рука и кисть HopeJr | Поддерживается. |
| Rebot B601 | Поддерживается во всех существующих режимах абсолютного позиционирования руки/захвата. |
| OpenArm | Поддерживается при use_velocity_and_torque=false; смешанные схемы действий исключены. |
| BiSo, BiOpenArm, BiRebot B601 | Поддерживается, если обе базовые ветви это поддерживают; их применённые цели сохраняют префиксы left/right. |
| Reachy2 | Поддерживается только при with_mobile_base=false, use_external_commands=false и max_relative_target=null. Команды mobile-base — это скорости, внешний режим не передаёт целевые значения, а путь обрезания (clipping) в настоящее время возвращает исходные, а не применённые значения. |
| LeKiwi (включая сетевой клиент), EarthRover Mini Plus | Не поддерживается: действия включают скорости mobile-base, которые не могут быть сохранены как целевые позиции. |
| Unitree G1 | Поддерживается только с SonicWholeBodyController: команда waiting повторяет последний полный 64-мерный токен движения, пока встроенный декодер продолжает балансировку. Локomoция GR00T/Holosoma и прямое управление через .q по-прежнему не поддерживаются. |
Эти объявления отражают контракт команд драйвера, а не физическую валидацию каждого воплощения или конфигурации контроллера. Значение по умолчанию для общего Robot остаётся false. Проверки возможностей не устанавливают совместимость контрольных точек, настройку контроллера, поддержку нагрузки или состояние оборудования.
Добавление телеоператора
Для реализации устройств телеуправления мы также предоставляем базовый класс Teleoperator. Этот класс очень похож на базовый класс Robot и также не предполагает никаких ограничений по форм-фактору.
Основные различия заключаются в функциях ввода-вывода: телеоператор позволяет формировать действие с помощью get_action и получать обратную связь через send_feedback. Обратная связь может включать любые управляемые параметры устройства телеуправления, которые помогают оператору понять последствия отправленных действий. Например, это может быть тактильная или силовая обратная связь на ведущем манипуляторе или вибрация на геймпаде. Чтобы реализовать телеоператора, вы можете следовать этому же учебнику и адаптировать его под эти два метода.
Использование собственных устройств LeRobot 🔌
Вы можете легко расширить lerobot собственным оборудованием — будь то камера, робот или устройство телеуправления — создав отдельный устанавливаемый пакет Python. Если вы соблюдаете несколько простых соглашений, инструменты командной строки lerobot (например, lerobot-teleoperate и lerobot-record) будут автоматически обнаруживать и интегрировать ваши решения без необходимости вносить изменения в исходный код lerobot.
В этом руководстве описаны соглашения, которым должен следовать ваш плагин.
4 основных соглашения
Чтобы ваше собственное устройство было обнаружимо, необходимо соблюдать следующие четыре правила.
1 . Создание устанавливаемого пакета с определённым префиксом
Ваш проект должен представлять собой стандартный устанавливаемый пакет Python. Важно, чтобы имя вашего пакета (определённое в pyproject.toml или setup.py) начиналось с одного из следующих префиксов:
lerobot_robot_для робота.lerobot_camera_для камеры.lerobot_teleoperator_для устройства телеуправления.lerobot_strategy_для стратегииlerobot-rollout(см. Собственная стратегия).
Именно благодаря этой системе префиксов lerobot автоматически находит ваш плагин в Python-среде.
2 . Следуйте шаблону именования SomethingConfig / Something
Класс реализации вашего устройства должен называться в соответствии с его классом конфигурации, просто убрав суффикс Config.
- Класс конфигурации:
MyAwesomeTeleopConfig - Класс устройства:
MyAwesomeTeleop
3. Разместите файлы в предсказуемой структуре
Класс устройства (MyAwesomeTeleop) должен находиться в предсказуемом модуле относительно его класса конфигурации (MyAwesomeTeleopConfig). lerobot будет автоматически искать его в следующих местах:
- В том же модуле, что и класс конфигурации.
- В подмодуле, названном в честь устройства (например,
my_awesome_teleop.py).
Рекомендуемая и самая простая структура — разместить их в отдельных файлах с понятными именами в той же директории.
4. Экспортируйте классы в __init__.py
Файл __init__.py вашего пакета должен импортировать и экспортировать как класс конфигурации, так и класс устройства, чтобы обеспечить к ним простой доступ.
Собираем всё вместе: полный пример
Создадим нового телеоператора my_awesome_teleop.
Структура каталогов
Вот как должен выглядеть каталог проекта. Имя пакета, lerobot_teleoperator_my_awesome_teleop, соответствует Конвенции №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Содержимое файла
config_my_awesome_teleop.py: определяет класс конфигурации. Обратите внимание на суффиксConfig(Соглашение №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: реализует устройство. Имя классаMyAwesomeTeleopсовпадает с именем его класса конфигурации (Соглашение №2). Эта структура файлов соответствует Соглашению №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: Объявляет ключевые классы (Соглашение №4).from .config_my_awesome_teleop import MyAwesomeTeleopConfig from .my_awesome_teleop import MyAwesomeTeleop
Установка и использование
Установите новый плагин в ваше окружение Python. Вы можете установить локальный пакет плагина с помощью редактируемого режима
pipили из 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Используйте его напрямую из командной строки. Теперь вы можете использовать собственное устройство, указывая его тип.
# Add the other arguments you need (robot type, ports, ids, ...) lerobot-teleoperate --teleop.type=my_awesome_teleop ...
И это всё! Ваше собственное устройство теперь полностью интегрировано.
Ищете пример?
Сообщество поддерживает десятки плагинов, созданных таким образом. Просмотрите разделы Third-Party Robots & Teleoperators и Third-Party Cameras & Sensors, чтобы найти вариант, подходящий для вашего оборудования, или начните с этих двух небольших пакетов:
- https://github.com/SpesRobotics/lerobot-robot-xarm
- https://github.com/SpesRobotics/lerobot-teleoperator-teleop
Итоги
Когда ваш класс робота готов, вы можете использовать экосистему LeRobot:
- Управляйте роботом с помощью доступных телеоператоров или напрямую интегрируйте собственное устройство телеуправления
- Запись обучающих данных и их визуализация
- Интегрируйте его в конвейеры обучения с подкреплением или имитационного обучения
После публикации плагина создайте PR, в котором добавьте его в раздел Third-Party Robots & Teleoperators или Third-Party Cameras & Sensors, чтобы другие пользователи могли его найти.
Не стесняйтесь обращаться за помощью к сообществу в нашем Discord 🤗
Update on GitHub