LeRobot 文档

自带硬件

Hugging Face's logo
加入 Hugging Face 社区

并获得增强的文档体验

开始使用

自带硬件

本教程将解释如何将您自己的机器人设计集成到 LeRobot 生态系统中,并使其能够使用我们所有的工具(数据收集、控制管道、策略训练和推理)。

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

先决条件

  • 您自己的机器人,它需要提供一个通信接口(例如串行、CAN、TCP)
  • 一种以编程方式读取传感器数据和发送电机命令的方法,例如制造商的 SDK 或 API,或者您自己协议的实现。
  • 在您的环境中安装 LeRobot。请遵循我们的安装指南

选择您的电机

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

请参考 MotorsBus 抽象类以了解其 API。关于如何使用它的一个好例子,您可以查看我们自己的 SO101 跟随者实现

如果兼容,请使用这些接口。否则,您需要找到或编写一个 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_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}

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

action_features

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

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

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

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

在这里,我们已经在 MotorsBusCamera 类中处理了这个问题,所以我们只需要调用它们自己的 disconnect() 方法即可

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

第 4 步:支持校准和配置

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

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

> @property
> def is_calibrated(self) -> bool:
>    return True
>
> def calibrate(self) -> None:
>    pass
> ```

### `is_calibrated`

This should reflect whether your robot has the required calibration loaded.
python

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


### `calibrate()`

The goal of the calibration is twofold:
    - Know the physical range of motion of each motors in order to only send commands within this range.
    - Normalize raw motors positions to sensible continuous values (e.g. percentages, degrees) instead of arbitrary discrete value dependant on the specific motor used that will not replicate elsewhere.

It should implement the logic for calibration (if relevant) and update the `self.calibration` dictionary. If you are using Feetech or Dynamixel motors, our bus interfaces already include methods to help with this.


<!-- prettier-ignore-start -->
```python
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 步:实现传感器读取和动作发送

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

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_dict

send_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

添加遥控操作器

为了实现遥控操作设备,我们还提供了一个 Teleoperator 基类。这个类与 Robot 基类非常相似,也不对形态做任何假设。

主要区别在于 I/O 函数:遥控操作器允许您通过 get_action 产生动作,并可以通过 send_feedback 接收反馈动作。反馈可以是遥控操作设备上任何可控的东西,可以帮助控制者理解所发送动作的后果。例如,主控臂上的运动/力反馈,游戏手柄控制器上的振动。要实现一个遥控操作器,您可以遵循本教程并为这两个方法进行调整。

总结

一旦您的机器人课程完成,您就可以利用 LeRobot 生态系统

  • 使用可用的遥操作器控制您的机器人,或直接集成您的遥操作设备
  • 记录训练数据并进行可视化
  • 将其集成到强化学习或模仿学习管道中

请随时在我们的 Discord 上向社区寻求帮助 🤗

< > 在 GitHub 上更新