接入你自己的硬件

本教程将讲解如何将你自己的机器人设计集成到 LeRobot 生态系统中,并让它使用我们所有的工具(数据采集、控制流水线、policy 训练和 inference)。

为此,我们在 LeRobot 中提供了 Robot 基类,它规定了物理机器人集成的标准接口。让我们看看如何实现它。

前置条件

  • 拥有自己的机器人,且其提供通信接口(例如串口、CAN、TCP)
  • 能够以编程方式读取传感器数据并发送电机命令,例如制造商的 SDK 或 API,或你自己的协议实现。
  • 在你的环境中安装了 LeRobot。请遵循我们的安装指南

选择你的电机

如果你使用 Feetech 或 Dynamixel 电机,LeRobot 提供内置的总线接口:

请参阅 MotorsBus 抽象类以了解其 API。 有关如何使用它的好例子,可以查看我们自己的 SO101 follower arm 实现

如果兼容,请使用这些接口。否则,你需要找到或编写一个 Python 接口(本教程不涉及):

  • 找到现有的 Python SDK(或使用 C/C++ 绑定)
  • 或实现一个基本的通信封装(例如通过 pyserial、socket 或 CANopen)

你并不孤单——许多社区贡献都使用自定义板卡或固件!

对于 Feetech 和 Dynamixel,我们目前支持以下舵机: - Feetech: - STS 和 SMS 系列(协议 0):sts3215sts3250sm8512bl - SCS 系列(协议 1):scs0009 - Dynamixel(仅协议 2.0):xl330-m077xl330-m288xl430-w250xm430-w350xm540-w270xc430-w150

如果你使用的 Feetech 或 Dynamixel 舵机不在此列表中,可以在 Feetech 表Dynamixel 表中添加它们。根据型号,这需要你添加特定于该型号的信息。不过在大多数情况下,要添加的内容并不多。

在接下来的章节中,我们将使用 FeetechMotorsBus 作为示例的电机接口。如有必要,请替换它并适配你的电机。

步骤 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={
            "cam_1": OpenCVCameraConfig(
                index_or_path=2,
                fps=30,
                width=480,
                height=640,
            ),
        }
    )

请参阅相机教程以了解如何检测和添加你的相机。

接下来,我们将创建实际的机器人类,它继承自 Robot。这个抽象类定义了一个契约,你的机器人必须遵循该契约才能与 LeRobot 的其他工具一起使用。

在这里,我们将创建一个带一个相机的简单 5 自由度机器人。它可以是一个简单的机械臂,但请注意 Robot 抽象类不对机器人的形态做任何假设。在设计新机器人时,你可以尽情发挥想象力!

from lerobot.cameras import make_cameras_from_configs
from lerobot.motors import Motor, MotorNormMode
from lerobot.motors.feetech import FeetechMotorsBus
from lerobot.robots import Robot

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

    def __init__(self, config: MyCoolRobotConfig):
        super().__init__(config)
        self.bus = FeetechMotorsBus(
            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 和 action 特征

这两个属性定义了机器人与使用它的工具(如数据采集或学习流水线)之间的接口契约

请注意,即使机器人尚未连接,这些属性也必须是可调用的,因此避免依赖运行时硬件 state 来定义它们。

observation_features

该属性应返回一个字典,描述机器人传感器输出的结构。键与 get_observation() 返回的内容匹配,值描述形状(对于数组/图像)或类型(对于简单值)。

我们的 5 自由度机械臂带一个相机的示例:

@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}

在这种情况下,observation 由一个简单的字典组成,存储每个电机的位置和一张相机图像。

action_features

该属性描述机器人通过 send_action() 期望的命令。同样,键必须与期望的输入格式匹配,值定义每个命令的形状/类型。

在这里,我们简单地使用与 observation_features 相同的关节本体感知特征(self._motors_ft):发送的 action 将只是每个电机的目标位置。

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()

该方法应建立与硬件的通信。此外,如果你的机器人需要 calibrate 但尚未 calibrate,它应默认启动 calibration 过程。如果你的机器人需要某些特定配置,也应在此处调用。

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()

该方法应优雅地终止与硬件的通信:释放任何相关资源(线程或进程)、关闭端口等。

在这里,我们已经在 MotorsBusCamera 类中处理了这一点,因此只需调用它们各自的 disconnect() 方法:

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

步骤 4:支持 calibration 与配置

LeRobot 支持自动保存和加载 calibration 数据。这对于关节偏移、零位置或传感器对齐非常有用。

请注意,根据你的硬件,这可能不适用。如果是这种情况,你可以简单地将这些方法留为空操作:

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

def calibrate(self) -> None:
    pass

is_calibrated

这应反映你的机器人是否已加载所需的 calibration。

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

calibrate()

calibration 的目标有两个:

  • 了解每个电机的物理运动范围,以便只在该范围内发送命令。
  • 将原始电机位置归一化为合理的连续值(例如百分比、度数),而不是依赖于所用特定电机、无法在其他地方复现的任意离散值。

它应实现 calibration 逻辑(如相关)并更新 self.calibration 字典。如果你使用 Feetech 或 Dynamixel 电机,我们的总线接口已包含帮助实现这一点的方法。

def calibrate(self) -> None:
    self.bus.disable_torque()
    for motor in self.bus.motors:
        self.bus.write("Operating_Mode", motor, OperatingMode.POSITION.value)

    input(f"Move {self} to the middle of its range of motion and press ENTER....")
    homing_offsets = self.bus.set_half_turn_homings()

    print(
        "Move all joints sequentially through their entire ranges "
        "of motion.\nRecording positions. Press ENTER to stop..."
    )
    range_mins, range_maxes = self.bus.record_ranges_of_motion()

    self.calibration = {}
    for motor, m in self.bus.motors.items():
        self.calibration[motor] = MotorCalibration(
            id=m.id,
            drive_mode=0,
            homing_offset=homing_offsets[motor],
            range_min=range_mins[motor],
            range_max=range_maxes[motor],
        )

    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.write("Operating_Mode", motor, OperatingMode.POSITION.value)
            self.bus.write("P_Coefficient", motor, 16)
            self.bus.write("I_Coefficient", motor, 0)
            self.bus.write("D_Coefficient", motor, 32)

步骤 5:实现传感器读取与 action 发送

这些是最重要的运行时函数:核心 I/O 循环。

get_observation()

返回机器人传感器值的字典。这些通常包括电机 state、相机帧、各种传感器等。在 LeRobot 框架中,这些 observation 将被馈送给 policy,以预测要采取的 action。字典的键和结构必须与 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()

接收与 action_features 匹配的字典,并将其发送到你的硬件。你可以添加安全限制(裁剪、平滑)并返回实际发送的内容。

为简单起见,我们在此示例中不会对 action 添加任何修改。

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

添加 teleoperator

为了实现 teleoperator,我们还提供了 Teleoperator 基类。该类与 Robot 基类非常相似,也不对形态做任何假设。

主要区别在于 I/O 函数:teleoperator 允许你通过 get_action 产生 action,并可以通过 send_feedback 接收反馈 action。反馈可以是 teleoperator 上任何可控的东西,能够帮助控制它的人理解所发送 action 的后果。例如 leader arm 上的运动/力反馈、游戏手柄控制器上的振动。要实现 teleoperator,你可以遵循本教程并针对这两个方法进行调整。

使用你自己的 LeRobot 设备 🔌

你可以通过创建一个独立的、可安装的 Python 包,轻松地用自己的自定义硬件——无论是相机、机器人还是 teleoperator——扩展 lerobot。如果你遵循几个简单的约定,lerobot 命令行工具(如 lerobot-teleoplerobot-record)将自动发现并集成你的创作,而无需对 lerobot 源代码做任何更改。

本指南概述了你的插件必须遵循的约定。

4 个核心约定

为确保你的自定义设备可被发现,你必须遵守以下四条规则。

1 . 创建具有特定前缀的可安装包

你的项目必须是一个标准的、可安装的 Python 包。关键在于,你的包名(在 pyproject.tomlsetup.py 中定义)必须以以下前缀之一开头:

  • 机器人使用 lerobot_robot_
  • 相机使用 lerobot_camera_
  • teleoperator 使用 lerobot_teleoperator_
  • lerobot-rollout policy 使用 lerobot_strategy_(参见接入你自己的 policy)。

这个前缀系统就是 lerobot 在 Python 环境中自动找到你的插件的方式。

2 . 遵循 SomethingConfig / Something 命名模式

你的设备实现类必须以其配置类命名,只需去掉 Config 后缀。

  • 配置类:MyAwesomeTeleopConfig
  • 设备类:MyAwesomeTeleop

3 . 将文件放在可预测的结构中

设备类(MyAwesomeTeleop)必须位于相对于其配置类(MyAwesomeTeleopConfig)的可预测模块中。lerobot 将自动在以下位置搜索:

  • 与配置类位于同一模块中。
  • 位于以设备命名的子模块中(例如 my_awesome_teleop.py)。

推荐且最简单的结构是将它们放在同一目录下命名清晰的独立文件中。

4 . 在 __init__.py 中暴露类

你包中的 __init__.py 文件应导入并暴露配置类和设备类,使它们易于访问。

综合示例:完整示例

让我们创建一个名为 my_awesome_teleop 的新 teleoperator。

目录结构

项目文件夹应如下所示。包名 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

安装与用法

  1. 在你的 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
  2. 直接从命令行使用它。 现在,你可以通过引用其类型来使用自定义设备。

    lerobot-teleoperate --teleop.type=my_awesome_teleop \
    # other arguments

就是这样\!你的自定义设备现已完全集成。

想要一个示例?

看看社区中的这两个包:

总结

一旦你的机器人类完成,你就可以利用 LeRobot 生态系统:

  • 使用可用的 teleoperator 控制你的机器人,或直接集成你的 teleoperator
  • 录制训练数据并可视化
  • 将其集成到 RL 或模仿学习流水线中

如需帮助,欢迎在我们的 Discord 上联系社区 🤗

在 GitHub 上更新