LeRobot documentation

独自のハードウェアを持ち込む

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

独自のハードウェアを持ち込む

このチュートリアルでは、独自のロボットデザインをLeRobotエコシステムに統合し、当社のすべてのツール(データ収集、制御パイプライン、ポリシーの学習と推論)にアクセスする方法を説明します。

その目的のため、LeRobotでは物理ロボット統合のための標準インターフェースを指定するRobotベースクラスを提供しています。その実装方法を見ていきましょう。

前提条件

  • 通信インターフェース(例:シリアル、CAN、TCP)を公開する独自のロボット
  • センサーデータを読み取り、モーターコマンドをプログラムで送信するための方法。例えば、メーカーのSDKやAPI、または独自のプロトコル実装など。
  • 環境にLeRobotがインストールされています。インストールガイドに従ってください。

モーターを選択

Feetech または Dynamixel モーターを使用している場合、LeRobot はどちらのモーターファミリーも駆動できる組み込みバスインターフェース SerialMotorsBus を提供しています。各モーターのモデルがどのファミリーに属するかを指定します。API については、そのドキュメントを参照してください。この機能には serial-motors エクストラが必要で、rustypot がインストールされます: pip install 'lerobot[serial-motors]'、またはソースチェックアウトから pip install -e ".[serial-motors]" を実行してください。 このインターフェースの使用方法の良い例として、弊社の SO101 フォロー実装 をご覧ください。

互換性がある場合はこれらを使用してください。互換性がない場合は、Python インターフェースを見つけるか、自分で作成する必要があります(このチュートリアルでは扱いません):

  • Pythonで既存のSDKを見つける(またはC/C++へのバインディングを使用する)
  • または、基本的な通信ラッパーを実装します(例:pyserial、socket、または CANopen を使用)

あなただけではありません。コミュニティの貢献の多くが、カスタムボードやファームウェアを使用しています!

FeetechとDynamixelについては、現在これらのサーボに対応しています:

  • Feetech STS & SMS シリーズ: sts3215、sts3250、sm8512bl
  • Feetech SCSシリーズ: scs0009
  • Dynamixel X シリーズ(プロトコル 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 はインストール済みの rustypot Python パッケージからモデルを読み取るため、新しいモデルを含む必要があります。具体的には、そのモデルを含む rustypot のリリース版、またはソースからビルドした rustypot です。

次のセクションでは、例としてFeetechモーターを使用します。必要に応じて、モデルを変更し、お使いのモーターに合わせて調整してください。

ステップ1: Robotインターフェースをサブクラス化

まず、ロボット用の設定クラスと文字列識別子(name)を指定する必要があります。ロボットに特別な要件があり、それを簡単に変更できるようにしたい場合は、ここで指定してください(例:ポート/アドレス、ボーレート)。

ここでは、ロボット用にポート名と1つのカメラをデフォルトで追加します。

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の他のツールでロボットを使用するために従う必要がある契約を定義しています。

ここでは、1つのカメラを備えたシンプルな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: 観測とアクションの特徴を定義する

これら2つのプロパティは、ロボットとそれを消費するツール(データ収集や学習パイプラインなど)の間のインターフェース契約を定義します。

これらのプロパティは、ロボットがまだ接続されていない場合でも呼び出せる必要があるため、それらの定義にランタイムのハードウェア状態に依存しないようにしてください。

observation_features

このプロパティは、ロボットからのセンサー出力の構造を記述する辞書を返す必要があります。キーはget_observation()が返すものと一致し、値は配列/画像の場合は形状、単純な値の場合は型を記述します。

カメラ1台を備えた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}

この場合、観測値は各モーターの位置とカメラ画像を格納するシンプルな辞書(dict)で構成されています。

action_features

このプロパティは、send_action()を介してロボットが期待するコマンドを記述します。同様に、キーは期待される入力形式と一致する必要があり、値は各コマンドの形状/型を定義します。

ここでは、observation_features と同様に、同じ関節の固有受容性特徴量 (self._motors_ft) を使用します。送信されるアクションは、各モーターの目標位置そのものになります。

@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:
    pass

is_calibrated

これは、ロボットに必要なキャリブレーションが読み込まれているかどうかを反映するはずです。

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

calibrate()

キャリブレーションの目的は2つあります:

  • 各モーターの物理的な可動範囲を把握し、この範囲内でのみコマンドを送信するようにしてください。
  • 使用しているモーターに依存する、他の環境では再現できない任意の離散値ではなく、パーセンテージや角度などの妥当な連続値に、生のモーター位置を正規化します。

キャリブレーション(関連する場合)のロジックを実装し、self.calibration辞書を更新する必要があります。FeetechまたはDynamixelモーターを使用している場合、SerialMotorsBusはすでに手動でキャリブレーションを実行しています:

  1. _keep_calibration_file() は、この ID のキャリブレーションファイルを再利用するかどうかを尋ねます。保持を選択した場合、モーターに書き込まれ、キャリブレーションはそこで終了します。
  2. record_calibration() はモーターを解放し、指定した動作モードに設定します。
  3. 各関節を可動域の中央に保持するよう求めます。その姿勢が、各モーターの半回転になります。
  4. 各関節を動かす範囲を記録し、ENTERキーを押すまで続行します。無限に回転する関節はfull_turn_motorsに格納され、エンコーダーの全範囲が割り当てられます。なお、5自由度アームには該当する関節はありません。
  5. 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: センサーの読み取りとアクションの送信を実装する

これらは最も重要なランタイム関数であり、コアの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

予測間の待機

通常のローカル RTC は、新しいロボット機能なしでも引き続き使用できます。リモートロールアウト、計画された非同期の言語一時停止、および回復可能なアクション飢餓には、保持ターゲットの待機契約が必要です。すべてのアクションフィールドをsend_action()を通じて更新することがコントローラーにとって安全な場合のみ、supports_position_hold = Trueでオプトインしてください。通常、これらのフィールドは実際の位置ターゲットです。オンボードコントローラーのバランス維持が保たれる場合、SONIC の潜在運動トークンなどの、別途検証された保持コマンドでコントローラーがオプトインすることもできます。トルクイネーブルや.posサフィックスだけではこれを確立できません。速度/トルクおよび歩行コマンドは引き続き不適切です。

リセットコマンドなしで待機できるロボット(send_action() の呼び出し時のみ進行するシミュレータなど)は、supports_command_hold = True を設定し、hold() を実装します。これは位置ホールドの代わりに使用されます。

共有のロールアウト実装は、send_action() が返した最後の完全な有限アクション(グリップおよびドライバによるクリッピングを含む)を更新します。ドライバがアクションを変更する場合は、元の要求ではなく、実際に適用されたターゲットを返してください。その後の観測値の更新では、そのターゲットは置き換えられません。コマンドが適用される前に、検証済みの測定位置のスナップショットによって初期化されます。ロボットは保持されたターゲットに向かって安定する可能性があります。この契約は、即座の物理的な停止を保証するのではなく、軌道の進行を停止します。各デプロイメントにおいて、ハードウェア上でロードサポート、グリップ、およびコマンド更新の動作を検証してください。

holding が有効な場合、カスタムアクションプロセッサおよびテレオペレーション補正は、グリップを含む完全な位置コマンドを保持する必要があります。ドライバーは、すべてのアクションコンポーネントについて、有限の適用値を返す必要があります。コマンド適用後の不完全または無効な返却は契約違反とみなされ、ロールアウトは停止し、古いターゲットの再利用の代わりにホーミングを抑制します。同期ロールアウトでは、コマンドの失敗もホーミングを抑制します。

Remote rollout はこのドライバーの機能を自動的に確認します。追加のホールドモード CLI フラグは不要です。一般的な観測の失敗では、通常のロールアウトのクリーンアップパスが使用されます。初期位置への復帰には完全な観測が必要であり、カメラが利用できない状態が続いている場合は失敗する可能性があります。

ロボットの実装ポジションターゲットの待機サポート
SO フォロワー (SO-100/SO-101)、OMX、Kochサポートされています。
HopeJrのアームとハンドサポートされています。
Rebot B601既存の絶対位置アーム/グリップモード全体でサポートされています。
YAMサポートされています。command_timeout_s 以内にコマンドが届かない場合、アームは idle_mode で設定されたように、その位置を保持するか、浮遊します。
OpenArmuse_velocity_and_torque=false でのサポートに対応しています。混合アクションスキーマは対象外です。
BiSO、BiOpenArm、BiRebot B601、BiYAM両方の下層アームがサポートしている場合にサポートされます。適用されたターゲットには、左/右のプレフィックスが保持されます。
Reachy2with_mobile_base=false、use_external_commands=false、max_relative_target=null の場合のみサポートされます。Mobile-base のコマンドは速度であり、外部モードではターゲットが送信されません。また、クリッピングパスは現在、適用後の値ではなく元の値を返します。
LeKiwi(ネットワーククライアントを含む)、EarthRover Mini Plusサポートされていません: アクションにはモバイルベースの速度が含まれており、位置ターゲットとして保持することはできません。
Unitree G1SonicWholeBodyControllerでのみサポートされています: waitingは、オンボードのデコーダーがバランス調整を継続している間、最後の完全な64次元モーショントークンを繰り返します。GR00T/Holosomaの移動および生の.q制御は引き続きサポートされていません。

これらの宣言は、ドライバのコマンド契約を反映したものであり、すべての実装やコントローラ設定に対する物理的な検証ではありません。汎用的なRobotのデフォルト値は引き続きfalseです。機能チェックは、チェックポイントの互換性、コントローラのチューニング、負荷のサポート、ハードウェアの健全性を確立するものではありません。

テレオペレーターの追加

テレオペレーションデバイスの実装のために、Teleoperatorベースクラスも提供しています。このクラスはRobotベースクラスと非常に似ており、フォームファクターについても何も仮定していません。

主な違いは I/O 関数にあります。テレオペレーターでは、get_action を介してアクションを生成し、send_feedback を介してフィードバックアクションを受信できます。フィードバックとは、テレオペレーション装置上で制御可能であり、操作者が送信したアクションの結果を理解するのに役立つあらゆるものです。例えば、リーダーアームのモーション/フォースフィードバックや、ゲームパッドコントローラーの振動などが挙げられます。テレオペレーターを実装するには、このチュートリアルを参考にし、これら 2 つのメソッドに合わせて適応させることができます。

ご自身のLeRobotデバイスを使用する🔌

独自のカメラ、ロボット、またはテレオペレーションデバイスなど、カスタムハードウェアを個別のインストール可能な Python パッケージとして作成することで、lerobot を簡単に拡張できます。いくつかのシンプルな規約に従うことで、lerobot のコマンドラインツール(lerobot-teleoperate や lerobot-record など)は、lerobot のソースコードを変更することなく、作成したものを自動的に検出し、統合します。

このガイドでは、プラグインが従う必要がある規約を説明します。

4つのコア規約

カスタムデバイスが検出可能であることを保証するには、以下の4つのルールに従う必要があります。

1 . 特定のプレフィックスを指定してインストール可能なパッケージを作成する

プロジェクトは、標準的なインストール可能なPythonパッケージである必要があります。特に重要なのは、パッケージ名(pyproject.tomlまたはsetup.pyで定義されるもの)が、以下のプレフィックスのいずれかで始まることです。

  • ロボットにはlerobot_robot_を使用します。
  • カメラにはlerobot_camera_を指定します。
  • テレオペレーションデバイス用のlerobot_teleoperator_
  • lerobot-rollout戦略用のlerobot_strategy_(独自の戦略の持ち込みを参照)。

このプレフィックスシステムにより、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

インストールと使用方法

  1. 新しいプラグインをPython環境にインストールします。 ローカルのプラグインパッケージは、pipのeditableモードまたは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. コマンドラインから直接使用します。 これにより、デバイスの型を参照してカスタムデバイスを使用できます。

    # Add the other arguments you need (robot type, ports, ids, ...)
    lerobot-teleoperate --teleop.type=my_awesome_teleop ...

これで完了です!カスタムデバイスが完全に統合されました。

例をお探しですか?

コミュニティは、この方法で作られた数十のプラグインを維持管理しています。お使いのハードウェアに近いものを探すには、サードパーティ製ロボット & テレオペレーターおよびサードパーティ製カメラ & センサーをご覧ください。あるいは、以下の2つの小さなパッケージから始めることもできます:

まとめ

ロボットクラスが完成したら、LeRobotエコシステムを活用できます。

  • 利用可能なテレオペレーターでロボットを制御するか、テレオペレーションデバイスを直接統合してください
  • トレーニングデータを記録して可視化
  • RLまたは模倣学習のパイプラインに統合します

プラグインが公開されたら、Third-Party Robots & TeleoperatorsまたはThird-Party Cameras & Sensorsに追加するPRを開き、他のユーザーが探しやすくしてください。

Discord 🤗 のコミュニティに遠慮なく助けを求めてください。

Update on GitHub