HuggingFace の LeRobot × SO-101 双腕アームを、macOS 上で組み立て&セットアップしてテレオペが動くところまでやりました。
手順そのものは ABEJA さんの記事(SO-101 組み立てレポート v2)が非常に丁寧なので、そちらと公式チュートリアルを併読してください。この記事は実際にやってみて踏んだ罠と、その診断・復旧の記録に絞ります。
補足:作業は Claude Code(Anthropic の CLI コーディングエージェント)に伴走してもらいながら進めました。ハマるたびにエラーログを貼って一緒に切り分けた記録なので、内容は実機で再現したものです。
リポジトリ:yutoAb/so101-lab
完成したもの
リーダー(手前、人間の手で握って動かす)→ フォロワー(奥)が追従する状態まで持っていきました。
リーダー(手前)を手で動かすとフォロワー(奥)が追従する
全体像
LeRobot × SO-101 のセットアップは以下の順序依存パイプラインで進みます。前段が生成する値(USB ポート、キャリブレーションファイル等)を後段が消費するので、順番を飛ばせません。
この記事は install 〜 teleoperate(黄色+緑のところ)までの実機ログです。
tl;dr:踏んだ罠 5 連発
| # | 罠 | 症状 | 根本原因 | 直し方 |
|---|---|---|---|---|
| 1 | USB ポート探し |
lerobot-find-port が EOFError で落ちる |
非対話シェルで input() が読めない |
ls /dev/tty.usbmodem* の差分法で代用 |
| 2 | サーボ取り違え | リーダー用(混在ギア比)のサーボをフォロワーフレームに組み込んでしまった | 組立順の勘違い | SO-101 はフレーム共通なので、組んだ方を「リーダー」に再定義 + グリッパー → ハンドル換装 |
| 3 | キャリブ失敗 | Missing motor IDs: 3, 4, 5, 6 |
デイジーチェーン断線(m2 と m3 の間のケーブル) | 該当箇所のケーブルを差し直し |
| 4 | キャリブ表示で誤解 |
POS が MIN や MAX に張り付いていて「校正失敗?」と焦った |
POS はホーム位置ではなく「可動域スキャン終了時の位置」 |
仕様。ホーム位置は別途保存されている |
| 5 | テレオペ初回失敗 | [TxRxResult] Incorrect status packet! |
一過性の通信エラー | リトライで通る |
環境
- macOS 15.x(Apple Silicon)
- Python 3.12(uv 管理)
-
lerobot==0.5.1(uv add 'lerobot[feetech]') - SO-101 双腕キット(リーダー × 1 + フォロワー × 1)
ABEJA さんの記事だと「Windows 限定でファームウェア更新が必須」と書かれていますが、Mac では工場出荷ファームのままキャリブレーション・テレオペまで通りました(少なくとも今回購入したロットでは)。Mac ユーザーは、まず更新せずにキャリブまで走らせてみて、エラーが出てから対処すれば良さそうです。
罠 1: lerobot-find-port が対話 CLI で詰む
USB ポートの判定(リーダー基板はどっち?フォロワー基板はどっち?)は、公式チュートリアルだと lerobot-find-port という対話 CLI を使います。「片方の USB を抜いて Enter」のような流れで、消えたポートをそのアーム用と判定する設計。
これが非対話的なシェル(Claude Code の bash、CI、リモートシェル等)で動かすと EOFError で落ちます。input() を呼ぶので当然なんですが、結構詰まりました。
回避策:ls の差分法
両方つないだ状態で:
$ ls /dev/tty.usbmodem*
/dev/tty.usbmodem5B420772061
/dev/tty.usbmodem5B420772561
片方(リーダー側)の USB だけ抜く:
$ ls /dev/tty.usbmodem*
/dev/tty.usbmodem5B420772061 # 残った方 = フォロワー
# 消えた方 = リーダー
lerobot-find-port がやっているのは結局これなので、対話 CLI に頼らなくても十分です。
罠 2: サーボ取り違え事件
SO-101 はリーダー腕 6 軸でギア比が混在しています:
| 関節 | リーダーのギア比 |
|---|---|
| shoulder_pan | 1:191 |
| shoulder_lift | 1:345 |
| elbow_flex | 1:191 |
| wrist_flex | 1:147 |
| wrist_roll | 1:147 |
| gripper | 1:147 |
一方でフォロワー腕は全 6 軸が 1:345で揃っています。
なぜリーダーだけ混在なのか
簡単に言うと「人間が手で動かす(=バックドライバビリティが必要)」からです。
- 高いギア比(1:345)= 高トルク・バックドライブが重い
- 低いギア比(1:147)= 低トルク・バックドライブが軽い
リーダーは人間が手で動かして教示します。全部 1:345 にすると重くてギクシャクしか動かず、教示データの品質が落ちます。逆に全部 1:147 にすると、手を離した瞬間に自重で崩れ落ちる。なので自重を支える必要がある shoulder_lift だけ 1:345、shoulder_pan と elbow_flex は中庸の 1:191、手首・グリッパーは軽い 1:147、というバランス設計になっています。
何が起きたか
Phase 3(モーター ID 書き込み)でリーダー用サーボに ID 1〜6 を書いた後、そのままフォロワー用のフレームに組み付けてしまった。半分組み立てた段階で「あれ、ギア比が混在してる側はリーダーだったよな…?」と気付きました。
SO-101 のサーボはアーム内部にケーブルが通されているので、組み立て後にやり直すには分解が必要。終わった、と思いました。(サーボは一度取り付けたら、硬すぎてもう取れません😭)
救済策:組んだ方を「リーダー」と再定義する
ここで救いだったのが、SO-101 のリーダーとフォロワーはフレームの 3D プリント部品が共通で、違うのは末端だけ:
- フォロワー: 末端にグリッパー(物を掴む爪)
- リーダー: 末端にハンドル(人間が握る取手)
つまり、「フォロワーとして組んだ腕」を「リーダー」と再定義して、グリッパーをハンドルに換装すれば成立する。あとは残りのフレーム + 残ったフォロワー用サーボ(全 1:345)でフォロワーを組み直す。
幸い、キットにはもう 1 セット分の 3D プリント部品とハンドルパーツが残っていたので、これで完了。
教訓
- 組立前にサーボを 1 個ずつトレイに並べて「これはリーダーの shoulder_pan(1:191)、これは…」とラベリングする
- ギア比が混在するパーツは紙テープに関節名を書いて貼る
- 公式手順の「組立前に ID を書き込め」は、組立後に分解不可能だからこそ厳守
罠 3: キャリブで Missing motor IDs(デイジーチェーン断線)
組み直したリーダーでキャリブレーションを実行:
$ bash scripts/calibrate.sh
==> Calibrating leader arm...
RuntimeError: FeetechMotorsBus motor check failed on port '/dev/tty.usbmodem5B420772561':
Missing motor IDs:
- 3 (expected model: 777)
- 4 (expected model: 777)
- 5 (expected model: 777)
- 6 (expected model: 777)
Full found motor list (id: model_number):
{1: 777, 2: 777}
ID 1, 2(shoulder_pan, shoulder_lift)は見えるのに、3〜6(elbow_flex 以降)が全滅。
これは典型的なデイジーチェーン切断です。Feetech STS3215 は 3 線バス通信で、サーボが数珠つなぎになっています:
m2 と m3 の間のサーボバスケーブルを物理確認したら、コネクタが半挿しで抜けかけていました。差し直して再実行 → 全 ID 認識 OK。
教訓
- エラーメッセージの「どの ID が見えて、どの ID が消えているか」で断線箇所が一意に特定できる
- 組立中、関節をぐりぐり動かすとコネクタが緩む可能性がある(アーム内部で挟まれることもある)
- 全 ID 消えていれば「電源 or 基板からの 1 本目」、途中から消えていれば「その境目のケーブル」
罠 4: キャリブの POS 値で誤解しかけた話
フォロワーのキャリブ結果が以下のように表示されました:
NAME | MIN | POS | MAX
shoulder_pan | 679 | 1953 | 3285
shoulder_lift | 885 | 904 | 3270 ← POS が MIN にほぼ張り付き
elbow_flex | 893 | 3120 | 3134 ← POS が MAX にほぼ張り付き
wrist_flex | 804 | 2838 | 3162
gripper | 1988 | 1997 | 3481 ← POS が MIN にほぼ張り付き
最初「POS がホーム位置なら、いくつかの関節が可動域の端に張り付いている = ホーム位置がおかしい」と思って焦りました。
実際には、POS は最初に Enter を押した時のホーム位置ではなく、「可動域スキャンを Enter で止めた瞬間の現在位置」。可動域スキャンの最後にどこかの関節を端っこまで動かして Enter を押すと、その関節の POS は MIN や MAX に張り付きます。ホーム位置は別途保存されているので、これで全く問題ありません。
LeRobot 側で表示文言を変えてくれると嬉しいですが、現状はそういう仕様だと知っておくと精神衛生上良いです。
罠 5: テレオペ初回の Incorrect status packet!
キャリブが終わって、いよいよテレオペ。
$ bash scripts/teleoperate.sh
INFO so_leader.py:78 my_leader SOLeader connected.
INFO follower.py:105 my_follower SOFollower connected.
ConnectionError: Failed to sync read 'Present_Position' on ids=[1, 2, 3, 4, 5, 6]
after 1 tries. [TxRxResult] Incorrect status packet!
接続のハンドシェイクは通る(=サーボは見えている)のに、関節位置の読み取りでパケットが化けたエラー。「またデイジーチェーンか…」と思って物理確認しようとしたんですが、ダメ元でもう一度実行したら何事もなく動きました。
$ bash scripts/teleoperate.sh
[警告は出るが、その後は正常動作]
たぶん起動直後の初期化タイミングで、まれにバスがノイズを拾ったとかその辺。再現性が無いタイプのエラーは、原因究明より「もう一回やる」の方が早いことがあります(ただし常時再現するなら配線を疑う)。
副産物:uv run ベースの bash スクリプト構成
毎回 lerobot-* の長いコマンドをコピペするのが嫌だったので、薄いラッパー bash スクリプト集にしました。
scripts/
_env.sh # USB ポート、HF トークン、データセット名等を集約(.gitignore)
_env.sh.example # テンプレ
install.sh # uv sync + HF auth チェック
find-port.sh # lerobot-find-port のラッパー(対話 CLI)
setup-motors-follower.sh
setup-motors-leader.sh
calibrate.sh # 両アームのキャリブを連続実行
teleoperate.sh
record.sh # データ収集(リーダー操縦 + カメラ + Hub push)
train.sh # ポリシー学習
eval.sh # 学習済みポリシーで自律推論
ポイント:
-
環境変数は
scripts/_env.shに集約(.gitignore対象。HF トークン・USB パス含む) -
全スクリプトが
uv run lerobot-*を呼ぶ(pip/venv 不要) - タスク切り替えは
_env.shのTASK_NAMEを書き換えるだけ -
record.shとeval.shは同じlerobot-recordを呼ぶ:違いは--teleop.*(人間操縦)か--policy.path(学習済みポリシー)か
例えば calibrate.sh は:
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/_env.sh"
cd "$SCRIPT_DIR/.."
echo "==> Calibrating follower arm..."
uv run lerobot-calibrate \
--robot.type=so101_follower \
--robot.port="$FOLLOWER_PORT" \
--robot.id="$FOLLOWER_ID"
echo "==> Calibrating leader arm..."
uv run lerobot-calibrate \
--teleop.type=so101_leader \
--teleop.port="$LEADER_PORT" \
--teleop.id="$LEADER_ID"
echo "==> Done. Next: bash scripts/teleoperate.sh"
lerobot-calibrate 単体の CLI を毎回叩くより、bash scripts/calibrate.sh の方が頭を使わなくて済みます。リポジトリ全体は yutoAb/so101-lab にあります。
次にやること
ここまでで Phase 1〜6(インストール → ポート判定 → モーター設定 → 組立 → キャリブ → テレオペ動作確認)完了。次は:
-
Phase 7: カメラ(Logitech C920n を発注済み)で
record.shを実行 → HF Hub にデータセット push -
Phase 8: Colab か手元 GPU で
train.sh→ ポリシー学習 -
Phase 9:
eval.shで学習済みポリシーを自律実行
カメラが届いて Phase 7 以降を回せたら、続編を書く予定です。
まとめ
- 公式手順の通り進めれば理屈上は詰まらないが、実機では泥臭いトラブルが連発する
- 一番怖いのは組立後にやり直しが効かない作業(サーボ ID 書き込み、ケーブリング)。組立前にラベリングを徹底
-
エラーメッセージは正直。
Missing motor IDs: 3, 4, 5, 6は「2 と 3 の間で切れている」と明示的に教えてくれている - 再現性のないエラーはリトライしてから原因究明(ただし常時再現は配線を疑う)
- macOS では現状ファームウェア更新なしでも完走可能
参考
- LeRobot 公式ドキュメント
- SO-101 組み立てレポート v2(ABEJA Tech Blog) — 手順の詳細はこちら
- yutoAb/so101-lab — 今回の作業ノート&スクリプト集
