在前面的章节中,我们学习了如何使用LeRobot进行模仿学习、仿真实验以及摄像头配置。然而,真正的机器人研究往往需要使用自定义的硬件平台。每个研究团队或开发者可能都有自己独特的机器人设计,如何将这些自定义硬件无缝集成到LeRobot生态系统中,是实现高效机器人学习的关键。
本章将详细介绍LeRobot的硬件集成框架,帮助读者掌握如何将自己的机器人硬件接入LeRobot系统。通过学习本章内容,你将能够让自定义机器人享受LeRobot提供的完整工具链,包括数据收集、控制管道、策略训练和推理等功能。
LeRobot的硬件集成设计遵循"自带硬件"(Bring Your Own Hardware)的理念,这意味着框架不限制特定的硬件平台,而是提供了标准化的接口,让各种类型的机器人都能够轻松接入。这种设计哲学大大降低了机器人学习的门槛,使得更多的研究者和开发者能够参与到机器人学习的研究中来。
LeRobot的硬件集成架构基于面向对象的设计原则,通过抽象基类定义了标准的机器人接口。这种设计使得不同类型的机器人硬件都能够以统一的方式与LeRobot系统交互。
硬件集成框架的核心是Robot
基类,它定义了所有机器人必须实现的标准接口。这个接口包括连接管理、数据读取、动作执行、校准等基本功能。通过继承这个基类并实现相应的方法,任何机器人硬件都能够成为LeRobot生态系统的一部分。
这种设计的优势在于其灵活性和可扩展性。Robot
基类不对机器人的形态做任何假设,无论是机械臂、移动机器人、人形机器人还是其他特殊设计的机器人,都可以通过相同的接口进行集成。这种形态无关的设计为创新性的机器人设计提供了广阔的空间。
LeRobot使用"接口契约"的概念来确保硬件集成的一致性。每个机器人都必须明确定义其观测特征(observation features)和动作特征(action features),这些特征描述了机器人能够提供什么样的传感器数据以及能够执行什么样的动作。
观测特征定义了机器人传感器输出的数据结构,包括关节位置、摄像头图像、力传感器数据等。动作特征则定义了机器人能够接受的控制命令格式,如目标关节位置、速度指令或力矩命令。这种明确的特征定义使得上层的学习算法能够正确地处理来自不同机器人的数据。
在开始硬件集成之前,需要确保具备必要的条件和资源。
首先,你的机器人必须提供某种形式的通信接口。常见的接口类型包括串行通信(Serial)、CAN总线、TCP/IP网络连接等。这些接口是软件与硬件交互的桥梁,没有通信接口就无法实现程序化控制。
其次,需要有读取传感器数据和发送电机命令的编程接口。这可能是制造商提供的SDK(软件开发工具包)、API(应用程序接口),或者是你自己实现的通信协议。如果使用的是商业机器人平台,通常会有官方的编程接口;如果是自制的机器人,则需要自己开发相应的通信协议。
确保在你的开发环境中已经正确安装了LeRobot。如果还没有安装,请参考官方的安装指南完成环境配置。LeRobot的安装包括核心框架以及各种依赖库,正确的安装是后续开发工作的基础。
LeRobot对常用的电机系统提供了内置支持,这可以大大简化集成工作。
Feetech电机支持:LeRobot内置了FeetechMotorsBus
类,专门用于控制Feetech品牌的舵机。目前支持的型号包括STS和SMS系列(protocol 0)的sts3215、sts3250、sm8512bl,以及SCS系列(protocol 1)的scs0009。
Dynamixel电机支持:通过DynamixelMotorsBus
类,LeRobot支持Dynamixel品牌的舵机。支持的型号包括xl330-m077、xl330-m288、xl430-w250、xm430-w350、xm540-w270、xc430-w150等,这些都使用Dynamixel协议2.0 (protocol 2.0 only)。
如果你使用的电机型号不在支持列表中,但属于Feetech或Dynamixel品牌,可以通过修改相应的配置表来添加支持。大多数情况下,只需要添加少量的型号特定信息即可。
如果使用的是其他品牌的电机或自制的控制系统,则需要自己实现相应的通信接口。这可能涉及到寻找现有的Python SDK,使用C/C++绑定,或者实现基本的通信包装器(如通过pyserial、socket或CANopen)。
硬件集成的第一步是设计机器人的配置类。配置类定义了机器人的基本参数和可配置选项,这些参数在不同的使用场景中可能需要调整。
子类化机器人接口 您首先需要为您的机器人指定配置类和一个字符串标识符(名称)。如果您的机器人有特殊需求,希望能够轻松更改,这里应当包含(例如端口/地址,波特率)。在这里,我们将默认为我们的机器人添加端口名称和一个摄像头:
配置类需要继承自RobotConfig
基类,并定义机器人特有的配置参数。一个典型的配置类可能包括通信端口、波特率、摄像头配置等信息。
以下是一个示例配置类:
from dataclasses import dataclass, field
from lerobot.common.cameras import CameraConfig
from lerobot.common.cameras.opencv import OpenCVCameraConfig
from lerobot.common.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,
),
}
)
这个配置类定义了两个主要参数:port
指定了与机器人通信的端口,cameras
定义了机器人配备的摄像头。摄像头配置使用了字典结构,允许配置多个摄像头,每个摄像头都有自己的标识符和配置参数。
在设计配置参数时,应该遵循以下原则:
易于修改:将经常需要调整的参数暴露在配置类中,如通信端口、设备地址、采样频率等。这样可以在不修改代码的情况下适应不同的硬件环境。
合理默认值:为配置参数提供合理的默认值,使得在大多数情况下不需要手动配置就能正常工作。
类型安全:使用Python的类型注解来明确参数类型,这有助于IDE提供更好的代码提示,也能够在运行时进行类型检查。
接下来,我们将创建实际的机器人类,该类继承自 Robot
。机器人类是硬件集成的核心,它实现了与硬件交互的所有逻辑。这个抽象类定义了您必须遵循的契约,以便您的机器人可以与 LeRobot 的其余工具一起使用。在这里,我们将创建一个简单的 5 自由度机器人,配备一个相机。但请注意,Robot 抽象类并不假设您的机器人的外形。设计新机器人时,您可以让您的想象力自由发挥!
机器人类需要继承自Robot
基类,并实现所有必需的抽象方法。以下是一个5自由度机械臂的示例实现:
from lerobot.common.cameras import make_cameras_from_configs
from lerobot.common.motors import Motor, MotorNormMode
from lerobot.common.motors.feetech import FeetechMotorsBus
from lerobot.common.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)
在这个实现中,config_class
指定了对应的配置类,name
提供了机器人的唯一标识符。初始化方法中创建了电机总线和摄像头对象,这些是与硬件交互的主要组件。
电机配置是机器人类实现的重要部分。在上面的示例中,我们为每个关节创建了一个Motor
对象,指定了电机ID、型号和归一化模式。
电机ID是硬件层面的标识符,用于在总线上区分不同的电机。电机型号决定了通信协议和控制参数。归一化模式定义了如何将物理的电机位置映射到标准化的数值范围,这对于不同型号电机之间的兼容性很重要。
特征定义是LeRobot硬件集成的核心概念,它明确了机器人与上层应用之间的数据交换格式。
观测特征描述了机器人能够提供的传感器数据结构。这些数据将被传递给学习算法,用于决策和控制。上文定义的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}
在这个示例中,观察特征包括五个关节的位置信息和摄像头图像。关节位置用浮点数表示,摄像头图像用三维数组的形状表示(高度、宽度、颜色通道)。
动作特征描述了机器人能够接受的控制命令格式。这些命令将由学习算法生成,并传递给机器人执行。
@property
def action_features(self) -> dict:
return self._motors_ft
在这个简单的示例中,动作特征与关节位置的观察特征相同,表示机器人接受目标关节位置作为控制命令。在更复杂的系统中,动作特征可能包括速度指令、力矩指令或其他类型的控制信号。
在设计特征时,需要考虑以下原则:
一致性:观测特征和动作特征的命名和格式应该保持一致,便于理解和维护。
完整性:特征定义应该包含学习算法所需的所有信息,同时避免冗余数据。
可扩展性:特征定义应该便于扩展,当添加新的传感器或执行器时,能够方便地更新特征定义。
机器人的连接管理是硬件集成中的重要环节,它确保软件能够可靠地与硬件通信。
is_connected
属性用于查询机器人的连接状态。只有在连接状态为真时,才能进行数据读取和命令发送操作。
@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
方法负责优雅地断开与硬件的连接,释放相关资源。
def disconnect(self) -> None:
self.bus.disconnect()
for cam in self.cameras.values():
cam.disconnect()
正确的断开连接对于系统稳定性很重要,它确保硬件资源得到正确释放,避免资源泄漏或冲突。
校准是机器人实现精确控制的关键环节。LeRobot具备自动校准数据管理功能,支持校准数据的自动保存与加载,对关节偏移、零位调整以及传感器对齐等场景尤为适用。
校准系统有两个主要目标:确定每个电机的物理运动范围,以确保控制命令在安全范围内;将原始电机位置标准化为连续的数值,便于不同系统之间的兼容。
@property
def is_calibrated(self) -> bool:
return self.bus.is_calibrated
校准状态检查确保机器人在使用前已经完成必要的校准过程。
请注意,这可能因您的硬件而不适用。如果遇到这种情况,您可以直接将这些方法设置为空操作:
@property
def is_calibrated(self) -> bool:
return True
def calibrate(self) -> None:
pass
校准过程通常包括用户交互和自动测量两个部分:
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)
这个校准过程首先禁用电机扭矩,允许用户手动移动机器人。然后记录中位位置作为归零点,接着记录每个关节的运动范围。最后将校准数据保存到文件中,供后续使用。
这些是最重要的运行时函数:核心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.")
# 读取关节位置
obs_dict = self.bus.sync_read("Present_Position")
obs_dict = {f"{motor}.pos": val for motor, val in obs_dict.items()}
# 捕获摄像头图像
for cam_key, cam in self.cameras.items():
obs_dict[cam_key] = cam.async_read()
return obs_dict
这个方法首先检查连接状态,然后同步读取所有电机的当前位置,接着异步读取所有摄像头的图像数据。返回的字典格式必须与observation_features
定义的格式一致。
send_action
方法负责执行控制命令。
def send_action(self, action: dict[str, Any]) -> dict[str, Any]:
goal_pos = {key.removesuffix(".pos"): val for key, val in action.items()}
# 发送目标位置到机械臂
self.bus.sync_write("Goal_Position", goal_pos)
return action
这个方法接收动作字典,提取目标位置信息,然后同步写入到所有电机。方法返回实际执行的动作,这允许在必要时对动作进行修改(如安全限制、平滑处理等)。
系统配置确保机器人硬件工作在最佳状态。
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)
配置过程包括设置电机的工作模式和PID控制参数。这些参数对机器人的控制性能有重要影响,需要根据具体的硬件特性进行调整。
除了机器人本体,LeRobot还支持遥操作器的集成。遥操作器用于人工控制机器人,是数据收集阶段的重要工具。
遥操作器类似于机器人类,但其I/O功能有所不同。遥操作器通过get_action
方法产生动作,通过send_feedback
方法接收反馈。反馈可以是任何有助于操作者理解动作后果的信息,如力反馈、振动等。
遥操作器的实现可以遵循与机器人类似的模式,主要区别在于需要适配不同的I/O方法。具体的实现细节取决于遥操作设备的类型和功能。
在进行硬件集成时,有一些重要的最佳实践需要遵循。
硬件通信可能出现各种异常情况,如设备断开、通信超时、数据错误等。良好的错误处理机制对于系统稳定性至关重要。在关键方法中应该添加适当的异常检查和处理逻辑。
机器人硬件的安全性是首要考虑因素。在实现动作执行方法时,应该添加必要的安全检查,如位置限制、速度限制、力矩限制等。这些安全措施能够防止硬件损坏和人身伤害。
数据读取和命令发送的频率对系统性能有重要影响。应该根据具体应用需求选择合适的采样频率,在性能和精度之间找到平衡点。
将硬件接口设计为模块化的组件,便于维护和扩展。例如,将电机控制、传感器读取、摄像头管理等功能分别封装为独立的模块。
本章详细介绍了LeRobot的硬件集成框架,从基本概念到具体实现,涵盖了自定义机器人接入LeRobot生态系统的完整流程。通过遵循标准化的接口设计,任何类型的机器人硬件都能够享受LeRobot提供的丰富功能。
硬件集成的关键在于正确实现Robot
基类定义的接口契约,包括特征定义、连接管理、数据交互等核心功能。通过合理的设计和实现,自定义机器人能够无缝集成到LeRobot的数据收集、训练和推理流程中。
LeRobot的"自带硬件"理念为机器人学习研究提供了极大的灵活性,使得研究者能够专注于算法和应用,而不必担心底层的硬件兼容性问题。随着越来越多的硬件平台接入LeRobot生态系统,整个社区将受益于更丰富的硬件选择和更广泛的应用场景。
参考资料:
[1] HuggingFace LeRobot Documentation - Bring Your Own Hardware. https://huggingface.co/docs/lerobot/integrate_hardware