Inspect Robots adapters for LeRobot SO-ARM followers (SO-100/SO-101) driven by LeRobot policies.
Project description
🦾 inspect-robots-so101
Run Inspect Robots evals on real SO-ARM followers (SO-100 / SO-101) driven by LeRobot policies.
Inspect Robots has two swappable inputs: a Policy (the VLA brain) and an
Embodiment (the robot body + world). This package provides both for the
SO-ARM + LeRobot stack, so any embodiment-agnostic Inspect Robots task runs on a real
arm:
lerobotpolicy — wraps a LeRobot checkpoint (ACT, SmolVLA, π0, diffusion…) and runs it in process on the GPU, returning an action chunk per inference.so_armembodiment — the LeRobot SO follower driver (Feetech bus), with a hard safety clamp, operator-in-the-loop success, and self-paced control.
Both declare the same 6-D joint-position contract (shoulder_pan,
shoulder_lift, elbow_flex, wrist_flex, wrist_roll, gripper; the cameras
you configure; packed joint_pos state), so Inspect Robots's compatibility check passes
with zero errors and zero warnings — verifiable before any motion.
inspect-robots run --task cubepick-reach --policy lerobot --embodiment so_arm
This is the SO-ARM/LeRobot sibling of inspect-robots-yam (bimanual I2RT YAM + MolmoAct2). Same Inspect Robots contract, different body and brain.
Install (on the robot/GPU machine)
# Inspect Robots isn't on PyPI yet; uv resolves it from git. The `lerobot` extra pulls
# torch + lerobot + the Feetech motor bus the SO follower uses.
uv pip install "inspect-robots-so101[lerobot] @ git+https://github.com/robocurve/inspect-robots-so101"
lerobot→lerobot[feetech](torch, the policy, and the SO-ARM driver).- The
lerobotextra needs Python ≥ 3.12 (lerobot ≥ 0.5's floor). On 3.10/3.11 the extra silently resolves to nothing: the core package still imports, but no torch/lerobot is installed and hardware runs will fail.
Then pick a checkpoint. Any LeRobot policy trained on your SO-ARM works — e.g. the
public lerobot/smolvla_base, or your own ACT/π0 checkpoint on the Hub or a path.
Preflight — prove compatibility before any motion
inspect-robots-so101-preflight # dims/semantics/cameras/state
inspect-robots-so101-preflight --task cubepick-reach # + scene realizability
inspect-robots-so101-preflight --dry-run # affirm no motion
A green preflight means action dim (6), control mode (joint_pos), cameras, and
state keys all line up. It does not prove the joint values are interpreted the
same way — see Safety below.
Calibrate first (once, with lerobot)
The embodiment never runs lerobot's interactive calibration — connecting with an uncalibrated arm would otherwise drop into a blocking prompt that moves the arm mid-eval. Calibrate once with lerobot's own tool, then tell the config which identity you used:
lerobot-calibrate --robot.type=so101_follower --robot.port=/dev/ttyACM0 --robot.id=my_follower
SOArmConfig(robot_id="my_follower") selects that calibration file
(<calibration_dir>/<robot_id>.json; leave calibration_dir=None for lerobot's
default location). If the arm isn't calibrated — or the file no longer matches
the motors — reset() fails fast with an actionable error instead of prompting.
Run on hardware
You must point the embodiment at your serial port, calibration id, and camera config, and the policy at a checkpoint:
from inspect_robots import eval
from inspect_robots.approver import ClampApprover
from inspect_robots_so101 import LeRobotPolicy, SOArmEmbodiment, SOArmConfig, LeRobotPolicyConfig
from lerobot.cameras.opencv import OpenCVCameraConfig # your camera backend
emb = SOArmEmbodiment(SOArmConfig(
port="/dev/ttyACM0",
robot_type="so101_follower",
robot_id="my_follower", # the id you ran `lerobot-calibrate` with
max_relative_target=10.0, # lerobot's slew limit (deg/step); required for home_pose
cameras=("front",),
camera_configs={"front": OpenCVCameraConfig(index_or_path=0, width=640, height=480, fps=30)},
))
pol = LeRobotPolicy(LeRobotPolicyConfig(
pretrained_path="lerobot/smolvla_base", policy_type="smolvla", device="cuda",
))
with emb: # guarantees disconnect (and torque-off) even if the eval raises
(log,) = eval("cubepick-reach", pol, emb,
approver=ClampApprover(emb.info.action_space)) # defense in depth
print(log.status, log.results.metrics)
(Equivalently, wrap the eval(...) in try: ... finally: emb.close().)
At each episode end the embodiment asks the operator (y/N); a yes records
termination_reason="success", which the task's success_at_end scorer reads.
Unattended runs simply run to max_steps and score as failures.
Safety
- Hard clamp backstop. Every command is clipped to
SOArmConfig.joint_low/highinsidestep(), independent of any Inspect RobotsApproverand on top of LeRobot's ownmax_relative_targetslew limit — unclamped model outputs can never reach the motors. Set these to your real, calibrated SO-ARM joint limits (the defaults are conservative placeholders: joints ±180°, gripper 0–100). - Use
ClampApproveron hardware for a second layer. - Native units, no renormalization. LeRobot's postprocessor unnormalizes the
policy output to the robot's native motor units (degrees for joints, 0–100 for
the gripper), so the embodiment commands them verbatim after the clamp. Train
and run the policy in the same units.
use_degrees=False(lerobot's normalized ±100 mode) is rejected — the state spec and default limits here assume degrees. Also note the units are degrees while Inspect Robots's canonicaljoint_posconvention is radians: the compat check compares state keys only, so pairing either component with a third-party counterpart will not flag a unit mismatch — verify units yourself when mixing stacks. - Homing is slew-limited or refused.
home_posesends a single absolute command, so the config requiresmax_relative_target(LeRobot's per-step slew limit) wheneverhome_poseis set — otherwise the arm would slam to home at full speed from wherever it happens to be. - Absolute vs. delta joints — verify first. Actions are treated as absolute
joint targets by default. If your checkpoint emits deltas, set
SOArmConfig(joints_are_delta=True)(the embodiment converts to absolute internally so the declaredjoint_posstays honest). The compat check cannot tell these apart — confirm with--dry-runand a single slow jog before a task.
Configuration
SOArmConfig: port, robot_type, robot_id, calibration_dir, cameras,
camera_configs, control_hz, cam_height/width, joint_low/high,
home_pose (requires max_relative_target), joints_are_delta, use_degrees
(must stay True), max_relative_target, disable_torque_on_disconnect.
robot_type is validated (so101_follower / so100_follower) but is a label:
at lerobot v0.5.x both names alias the same driver class, so it changes no
runtime behavior.
LeRobotPolicyConfig: pretrained_path, policy_type, device, cameras,
state_key, chunk_size, cam_height/width.
Scalar knobs are settable from the CLI:
inspect-robots run -P pretrained_path=lerobot/smolvla_base -E port=/dev/ttyACM0 ....
Development
uv venv && uv pip install -e ".[dev]" # inspect-robots from a git tag
uv run pre-commit install
uv run pytest --cov # 100% coverage required
uv run ruff check . && uv run mypy
The whole suite runs with no hardware, no GPU, no torch, no lerobot, and no
stdin — the SO-ARM driver, the policy inference, the clock, and operator I/O are
all injected. The real model seam (_default_predict) is covered via
sys.modules fakes, and a dedicated lerobot-seam CI job (py3.12) imports the
real lerobot symbols it uses; only direct hardware/TTY I/O keeps
# pragma: no cover.
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file inspect_robots_so101-0.3.0.tar.gz.
File metadata
- Download URL: inspect_robots_so101-0.3.0.tar.gz
- Upload date:
- Size: 30.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7dc8c38f43f1556f66aaa87815c18dfe911e4b7498b8a16facf185738fa4a0d3
|
|
| MD5 |
55c02436813b0fa375c397c8a39fbc83
|
|
| BLAKE2b-256 |
7f354c9f3345dd47f3b3bc15b10b203065be3b8830c7f64ba35e5a5f43094857
|
Provenance
The following attestation bundles were made for inspect_robots_so101-0.3.0.tar.gz:
Publisher:
release.yml on robocurve/inspect-robots-so101
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
inspect_robots_so101-0.3.0.tar.gz -
Subject digest:
7dc8c38f43f1556f66aaa87815c18dfe911e4b7498b8a16facf185738fa4a0d3 - Sigstore transparency entry: 2094654052
- Sigstore integration time:
-
Permalink:
robocurve/inspect-robots-so101@eb1fd6f2ba367ff07cf8507dfd12575f06ca97c3 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/robocurve
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@eb1fd6f2ba367ff07cf8507dfd12575f06ca97c3 -
Trigger Event:
release
-
Statement type:
File details
Details for the file inspect_robots_so101-0.3.0-py3-none-any.whl.
File metadata
- Download URL: inspect_robots_so101-0.3.0-py3-none-any.whl
- Upload date:
- Size: 24.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
967bc309e11f1f5f4c15a1f295f8d55cf76a46f7e20a7543edfb78fa7568c6cb
|
|
| MD5 |
0a07f32331e21c53c77093844b4b7476
|
|
| BLAKE2b-256 |
1cd46b10394868033ef88b5a21b3f18a06a21e2470a541d8a6b3010e5f251de3
|
Provenance
The following attestation bundles were made for inspect_robots_so101-0.3.0-py3-none-any.whl:
Publisher:
release.yml on robocurve/inspect-robots-so101
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
inspect_robots_so101-0.3.0-py3-none-any.whl -
Subject digest:
967bc309e11f1f5f4c15a1f295f8d55cf76a46f7e20a7543edfb78fa7568c6cb - Sigstore transparency entry: 2094654354
- Sigstore integration time:
-
Permalink:
robocurve/inspect-robots-so101@eb1fd6f2ba367ff07cf8507dfd12575f06ca97c3 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/robocurve
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@eb1fd6f2ba367ff07cf8507dfd12575f06ca97c3 -
Trigger Event:
release
-
Statement type: