lerobot设备标定

why calibrate

先来看看标定后的数据

{
    "shoulder_pan": {                       #肩部旋转关节
        "id": 1,
        "drive_mode": 0,
        "homing_offset": -1620,
        "range_min": 1142,
        "range_max": 2931
    },
    "shoulder_lift": {                          #肩部升降关节
        "id": 2,
        "drive_mode": 0,
        "homing_offset": 2025,
        "range_min": 844,
        "range_max": 3053
    },
    "elbow_flex": {                           #肘部弯曲关节
        "id": 3,
        "drive_mode": 0,
        "homing_offset": -1208,
        "range_min": 963,
        "range_max": 3078
    },
    "wrist_flex": {                            #腕部弯曲关节
        "id": 4,
        "drive_mode": 0,
        "homing_offset": 2021,
        "range_min": 884,
        "range_max": 3222
    },
    "wrist_roll": {                             #腕部旋转关节
        "id": 5,
        "drive_mode": 0,
        "homing_offset": -777,
        "range_min": 142,
        "range_max": 3961
    },
    "gripper": {                                #夹爪关节
        "id": 6,
        "drive_mode": 0,
        "homing_offset": 909,
        "range_min": 1978,
        "range_max": 3522
    }
}

上面数据是每个舵机的相关参数,一共有6个舵机,每个舵机都有一个id来标识,可以对应实物来看实际是从下到上。

  • id: 电机的唯一标识符,用于在总线通信时精准定位和控制特定电机。
  • drive_mode: 电机的驱动模式,取值为 0 表示特定的驱动模式,不同驱动模式会影响电机的运动特性与控制方式。
  • homing_offset:归位偏移量,指电机从物理零点位置到校准零点位置的偏移量。此参数能保证电机在每次启动时都能回到准确的零点位置,从而提升运动精度。
  • range_min 和 range_max:电机运动范围的最小值和最大值,以数值形式呈现。这两个参数限定了电机的运动边界,避免因超出范围而导致硬件损坏或者运动异常。range_min 和 range_max:电机运动范围的最小值和最大值,以数值形式呈现。这两个参数限定了电机的运动边界,避免因超出范围而导致硬件损坏或者运动异常。

上面的参数信息在代码中lerobot/src/lerobot/robots/koch_follower/koch_follower.py 中回进行配置。

从上面的配置信息可知,之所以要标定,就是要获取电机的一些物理特性,主要是几个方面:明确电机物理运动范围、归一化电机位置、确保机器人运行精度等。

明确电机范围

机器的电机要有特定的物理运动范围,因为发送要是超过这个范围,回造成硬件损坏。使用calibrate操作可以记录每个电机的最小和最大位置限制,保证后续发送的指令都在安全范围内。

lerobot/src/lerobot/robots/so101_follower/so101_follower.py

def calibrate(self) -> None:
    # ...已有代码...
    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],
            )
    # ...已有代码...

归一化电机位置

不同电机的原始位置值可能都离散的任意值,这些依赖于具体的电机型号,不具备通用性。calibrate 操作能将这些原始位置值归一化为有意义的连续值,像百分比、角度等,方便后续处理和控制。

lerobot/src/lerobot/motors/motors_bus.py

    def _normalize(self, id_, val, min_, max_, drive_mode):
        motor = self._id_to_name(id_)
        if self.motors[motor].norm_mode is MotorNormMode.RANGE_M100_100:
            norm = (((val - min_) / (max_ - min_)) * 200) - 100
            normalized_values[id_] = -norm if drive_mode else norm
        # ...已有代码...

确保机器运行精度

lerobot/src/lerobot/robots/so100_leader/so100_leader.py 中的 calibrate

def calibrate(self) -> None:
    # ...已有代码...
    input(f"Move {self} to the middle of its range of motion and press ENTER....")
    homing_offsets = self.bus.set_half_turn_homings()
    # ...已有代码...

通过校准,可以确定电机的归位偏移量(homing offset),这有助于提高机器人运动的精度和重复性。在实际应用中,准确的位置控制对机器人完成任务至关重要。

最后经过校准后的数据,后续无需在重复校准,校准保存的路径一般在~/.cache/huggingface/lerobot/calibration,实际运行过程中,回进行加载。

保存
def _save_calibration(self, fpath: Path | None = None) -> None:
        draccus.dump(self.calibration, f, indent=4)

加载
def _load_calibration(self, fpath: Path | None = None) -> None:
        self.calibration = draccus.load(dict[str, MotorCalibration], f)


校准流程

python -m lerobot.calibrate \
    --robot.type=so101_follower \
    --robot.port=/dev/ttyACM0 \
    --robot.id=R12252801

上面是执行命令示例,接下来个跟踪一下流程。Python 解释器接收到 python -m lerobot.calibrate 命令后,会将 lerobot.calibrate 当作一个可执行模块来处理。按照 Python 模块查找机制,会在 sys.path 所包含的路径里寻找 lerobot/calibrate.py 文件,对应的文件路径为lerobot/src/lerobot/calibrate.py。

@draccus.wrap()
def calibrate(cfg: CalibrateConfig):
    init_logging()
    logging.info(pformat(asdict(cfg)))

    if isinstance(cfg.device, RobotConfig):
        device = make_robot_from_config(cfg.device)
    elif isinstance(cfg.device, TeleoperatorConfig):
        device = make_teleoperator_from_config(cfg.device)

    device.connect(calibrate=False)
    device.calibrate()
    device.disconnect()


if __name__ == "__main__":
    calibrate()

调用calibrate含税,从calibrate可以看到主要的步骤为:

  • 参数解析与配置初始化
  • 创建设备实例
  • 连接设备
  • 设备校准
  • 设备断开连接

参数解析与配置初始化

calibrate函数被@draccus.wrap()装饰,draccus是一个用于解析命令行参数并创建配置对象的库。

@dataclass
class CalibrateConfig:
    teleop: TeleoperatorConfig | None = None
    robot: RobotConfig | None = None

    def __post_init__(self):
        if bool(self.teleop) == bool(self.robot):
            raise ValueError("Choose either a teleop or a robot.")

        self.device = self.robot if self.robot else self.teleop

draccus会把命令行参数--robot.type=so101_follower --robot.port=/dev/ttyACM0 --robot.id=R12252801解析成CalibrateConfig类的实例。robot属性会被初始化为RobotConfig类型的对象,如果是--teleop.type=so101_leader 将会初始化为TeleoperatorConfig类型的对象。RobotConfig对象在config.py中定义的。

@dataclass(kw_only=True)
class RobotConfig(draccus.ChoiceRegistry, abc.ABC):
    # Allows to distinguish between different robots of the same type
    id: str | None = None
    # Directory to store calibration file
    calibration_dir: Path | None = None

    def __post_init__(self):
        if hasattr(self, "cameras") and self.cameras:
            for _, config in self.cameras.items():
                for attr in ["width", "height", "fps"]:
                    if getattr(config, attr) is None:
                        raise ValueError(
                            f"Specifying '{attr}' is required for the camera to be used in a robot"
                        )

    @property
    def type(self) -> str:
        return self.get_choice_name(self.__class__)

其会根据命令行参数或配置文件中的类型字段,动态选择并创建对应的配置子类实例。

如当前的参数有type,port,id分别设置为so101_follower、/dev/ttyACM0 和 R12252801。

创建设备实例

根据cfg.device类型,调用对应工厂含税创建设备实例。

if isinstance(cfg.device, RobotConfig):
    device = make_robot_from_config(cfg.device)
elif isinstance(cfg.device, TeleoperatorConfig):
    device = make_teleoperator_from_config(cfg.device)

由于 cfg.device 是 RobotConfig 类型,所以会调用 make_robot_from_config(cfg.device)。该函数可能定义在 /home/laumy/lerobot/src/lerobot/robots/init.py 或者相关的工厂模块中,依据 cfg.device.type 的值(即 so101_follower),会创建 SO101Follower 类的实例。

连接设备

调用 device.connect(calibrate=False) 方法连接设备,lerobot/src/lerobot/robots/so101_follower/so101_follower.py 文件中,其 connect 方法可能如下:

def connect(self, calibrate: bool = True) -> None:
    if self.is_connected:
        raise DeviceAlreadyConnectedError(f"{self} already connected")

    # 连接串口总线
    self.bus = DynamixelBus(port=self.config.port, baudrate=1000000)
    self.bus.connect()

    if not self.is_calibrated and calibrate:
        self.calibrate()

    # 连接摄像头
    for cam in self.cameras.values():
        cam.connect()

    self.configure()
    logger.info(f"{self} connected.")

因为传入的 calibrate=False,所以不会触发自动校准。在连接过程中,会先连接串口总线,再连接摄像头,最后进行设备配置。

标定动作

调用 device.calibrate() 方法对设备进行校准,SO101Follower 类的 calibrate 方法可能如下:

def calibrate(self) -> None:
    logger.info(f"\nRunning calibration of {self}")

    # 禁用扭矩
    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()

校准过程包含禁用扭矩、设置电机工作模式、手动操作提示、记录关节运动范围、保存校准数据等步骤。

设备断开连接

调用 device.disconnect() 方法断开设备连接,SO101Follower 类的 disconnect 方法如下:

def disconnect(self):
    if not self.is_connected:
        raise DeviceNotConnectedError(f"{self} is not connected.")

    # 断开串口总线连接
    self.bus.disconnect(self.config.disable_torque_on_disconnect)

    # 断开摄像头连接
    for cam in self.cameras.values():
        cam.disconnect()

    logger.info(f"{self} disconnected.")