inspect-robots-so101 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- inspect_robots_so101-0.3.0/.gitignore +28 -0
- inspect_robots_so101-0.3.0/LICENSE +21 -0
- inspect_robots_so101-0.3.0/PKG-INFO +209 -0
- inspect_robots_so101-0.3.0/README.md +178 -0
- inspect_robots_so101-0.3.0/pyproject.toml +115 -0
- inspect_robots_so101-0.3.0/src/inspect_robots_so101/CLAUDE.md +36 -0
- inspect_robots_so101-0.3.0/src/inspect_robots_so101/__init__.py +38 -0
- inspect_robots_so101-0.3.0/src/inspect_robots_so101/config.py +180 -0
- inspect_robots_so101-0.3.0/src/inspect_robots_so101/embodiment.py +236 -0
- inspect_robots_so101-0.3.0/src/inspect_robots_so101/operator.py +51 -0
- inspect_robots_so101-0.3.0/src/inspect_robots_so101/packing.py +86 -0
- inspect_robots_so101-0.3.0/src/inspect_robots_so101/policy.py +224 -0
- inspect_robots_so101-0.3.0/src/inspect_robots_so101/preflight.py +95 -0
- inspect_robots_so101-0.3.0/src/inspect_robots_so101/py.typed +0 -0
- inspect_robots_so101-0.3.0/tests/test_api_snapshot.py +43 -0
- inspect_robots_so101-0.3.0/tests/test_compat.py +69 -0
- inspect_robots_so101-0.3.0/tests/test_config.py +103 -0
- inspect_robots_so101-0.3.0/tests/test_embodiment.py +232 -0
- inspect_robots_so101-0.3.0/tests/test_eval_end_to_end.py +63 -0
- inspect_robots_so101-0.3.0/tests/test_operator.py +41 -0
- inspect_robots_so101-0.3.0/tests/test_packing.py +84 -0
- inspect_robots_so101-0.3.0/tests/test_policy.py +378 -0
- inspect_robots_so101-0.3.0/tests/test_preflight.py +115 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
|
|
9
|
+
# Virtualenvs
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
|
|
13
|
+
# Test / coverage
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.coverage
|
|
16
|
+
coverage.xml
|
|
17
|
+
htmlcov/
|
|
18
|
+
.mypy_cache/
|
|
19
|
+
.ruff_cache/
|
|
20
|
+
|
|
21
|
+
# Secrets / env
|
|
22
|
+
.env
|
|
23
|
+
.env.*
|
|
24
|
+
|
|
25
|
+
# OS / editor
|
|
26
|
+
.DS_Store
|
|
27
|
+
.idea/
|
|
28
|
+
.vscode/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RoboCurve
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: inspect-robots-so101
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Inspect Robots adapters for LeRobot SO-ARM followers (SO-100/SO-101) driven by LeRobot policies.
|
|
5
|
+
Project-URL: Homepage, https://github.com/robocurve/inspect-robots-so101
|
|
6
|
+
Project-URL: Framework, https://github.com/robocurve/inspect-robots
|
|
7
|
+
Project-URL: LeRobot, https://github.com/huggingface/lerobot
|
|
8
|
+
Author: RoboCurve
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: inspect-robots,lerobot,robotics,so-arm,so100,so101,vla
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: inspect-robots>=0.3
|
|
20
|
+
Requires-Dist: numpy>=1.24
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
23
|
+
Requires-Dist: numpy<2.5; extra == 'dev'
|
|
24
|
+
Requires-Dist: pre-commit>=3.5; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
28
|
+
Provides-Extra: lerobot
|
|
29
|
+
Requires-Dist: lerobot[feetech]<0.6,>=0.5; (python_version >= '3.12') and extra == 'lerobot'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
<div align="center">
|
|
33
|
+
|
|
34
|
+
# π¦Ύ inspect-robots-so101
|
|
35
|
+
|
|
36
|
+
**Run [Inspect Robots](https://github.com/robocurve/inspect-robots) evals on real
|
|
37
|
+
[SO-ARM](https://github.com/TheRobotStudio/SO-ARM100) followers (SO-100 / SO-101)
|
|
38
|
+
driven by [LeRobot](https://github.com/huggingface/lerobot) policies.**
|
|
39
|
+
|
|
40
|
+
[](https://github.com/robocurve/inspect-robots-so101/actions/workflows/ci.yml)
|
|
41
|
+
[](LICENSE)
|
|
42
|
+
[](https://github.com/robocurve/inspect-robots-so101/actions/workflows/ci.yml)
|
|
43
|
+
[](https://github.com/robocurve/inspect-robots)
|
|
44
|
+
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
Inspect Robots has **two** swappable inputs: a `Policy` (the VLA brain) and an
|
|
48
|
+
`Embodiment` (the robot body + world). This package provides both for the
|
|
49
|
+
SO-ARM + LeRobot stack, so any embodiment-agnostic Inspect Robots task runs on a real
|
|
50
|
+
arm:
|
|
51
|
+
|
|
52
|
+
- **`lerobot` policy** β wraps a LeRobot checkpoint (ACT, SmolVLA, Ο0, diffusionβ¦)
|
|
53
|
+
and runs it **in process** on the GPU, returning an action chunk per inference.
|
|
54
|
+
- **`so_arm` embodiment** β the LeRobot SO follower driver (Feetech bus), with a
|
|
55
|
+
hard safety clamp, operator-in-the-loop success, and self-paced control.
|
|
56
|
+
|
|
57
|
+
Both declare the **same 6-D joint-position contract** (`shoulder_pan`,
|
|
58
|
+
`shoulder_lift`, `elbow_flex`, `wrist_flex`, `wrist_roll`, `gripper`; the cameras
|
|
59
|
+
you configure; packed `joint_pos` state), so Inspect Robots's compatibility check passes
|
|
60
|
+
with **zero errors and zero warnings** β verifiable before any motion.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
inspect-robots run --task cubepick-reach --policy lerobot --embodiment so_arm
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
> This is the SO-ARM/LeRobot sibling of
|
|
67
|
+
> [inspect-robots-yam](https://github.com/robocurve/inspect-robots-yam) (bimanual I2RT YAM +
|
|
68
|
+
> MolmoAct2). Same Inspect Robots contract, different body and brain.
|
|
69
|
+
|
|
70
|
+
## Install (on the robot/GPU machine)
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Inspect Robots isn't on PyPI yet; uv resolves it from git. The `lerobot` extra pulls
|
|
74
|
+
# torch + lerobot + the Feetech motor bus the SO follower uses.
|
|
75
|
+
uv pip install "inspect-robots-so101[lerobot] @ git+https://github.com/robocurve/inspect-robots-so101"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- `lerobot` β `lerobot[feetech]` (torch, the policy, and the SO-ARM driver).
|
|
79
|
+
- **The `lerobot` extra needs Python β₯ 3.12** (lerobot β₯ 0.5's floor). On
|
|
80
|
+
3.10/3.11 the extra silently resolves to *nothing*: the core package still
|
|
81
|
+
imports, but no torch/lerobot is installed and hardware runs will fail.
|
|
82
|
+
|
|
83
|
+
Then pick a checkpoint. Any LeRobot policy trained on your SO-ARM works β e.g. the
|
|
84
|
+
public `lerobot/smolvla_base`, or your own ACT/Ο0 checkpoint on the Hub or a path.
|
|
85
|
+
|
|
86
|
+
## Preflight β *prove compatibility before any motion*
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
inspect-robots-so101-preflight # dims/semantics/cameras/state
|
|
90
|
+
inspect-robots-so101-preflight --task cubepick-reach # + scene realizability
|
|
91
|
+
inspect-robots-so101-preflight --dry-run # affirm no motion
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
A green preflight means action dim (6), control mode (`joint_pos`), cameras, and
|
|
95
|
+
state keys all line up. **It does not prove the joint values are interpreted the
|
|
96
|
+
same way** β see *Safety* below.
|
|
97
|
+
|
|
98
|
+
## Calibrate first (once, with lerobot)
|
|
99
|
+
|
|
100
|
+
The embodiment **never** runs lerobot's interactive calibration β connecting with
|
|
101
|
+
an uncalibrated arm would otherwise drop into a *blocking* prompt that moves the
|
|
102
|
+
arm mid-eval. Calibrate once with lerobot's own tool, then tell the config which
|
|
103
|
+
identity you used:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
lerobot-calibrate --robot.type=so101_follower --robot.port=/dev/ttyACM0 --robot.id=my_follower
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`SOArmConfig(robot_id="my_follower")` selects that calibration file
|
|
110
|
+
(`<calibration_dir>/<robot_id>.json`; leave `calibration_dir=None` for lerobot's
|
|
111
|
+
default location). If the arm isn't calibrated β or the file no longer matches
|
|
112
|
+
the motors β `reset()` fails fast with an actionable error instead of prompting.
|
|
113
|
+
|
|
114
|
+
## Run on hardware
|
|
115
|
+
|
|
116
|
+
You must point the embodiment at your serial port, calibration id, and camera
|
|
117
|
+
config, and the policy at a checkpoint:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from inspect_robots import eval
|
|
121
|
+
from inspect_robots.approver import ClampApprover
|
|
122
|
+
from inspect_robots_so101 import LeRobotPolicy, SOArmEmbodiment, SOArmConfig, LeRobotPolicyConfig
|
|
123
|
+
from lerobot.cameras.opencv import OpenCVCameraConfig # your camera backend
|
|
124
|
+
|
|
125
|
+
emb = SOArmEmbodiment(SOArmConfig(
|
|
126
|
+
port="/dev/ttyACM0",
|
|
127
|
+
robot_type="so101_follower",
|
|
128
|
+
robot_id="my_follower", # the id you ran `lerobot-calibrate` with
|
|
129
|
+
max_relative_target=10.0, # lerobot's slew limit (deg/step); required for home_pose
|
|
130
|
+
cameras=("front",),
|
|
131
|
+
camera_configs={"front": OpenCVCameraConfig(index_or_path=0, width=640, height=480, fps=30)},
|
|
132
|
+
))
|
|
133
|
+
pol = LeRobotPolicy(LeRobotPolicyConfig(
|
|
134
|
+
pretrained_path="lerobot/smolvla_base", policy_type="smolvla", device="cuda",
|
|
135
|
+
))
|
|
136
|
+
|
|
137
|
+
with emb: # guarantees disconnect (and torque-off) even if the eval raises
|
|
138
|
+
(log,) = eval("cubepick-reach", pol, emb,
|
|
139
|
+
approver=ClampApprover(emb.info.action_space)) # defense in depth
|
|
140
|
+
print(log.status, log.results.metrics)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
(Equivalently, wrap the `eval(...)` in `try: ... finally: emb.close()`.)
|
|
144
|
+
|
|
145
|
+
At each episode end the embodiment asks the operator (y/N); a `yes` records
|
|
146
|
+
`termination_reason="success"`, which the task's `success_at_end` scorer reads.
|
|
147
|
+
Unattended runs simply run to `max_steps` and score as failures.
|
|
148
|
+
|
|
149
|
+
## Safety
|
|
150
|
+
|
|
151
|
+
- **Hard clamp backstop.** Every command is clipped to `SOArmConfig.joint_low/high`
|
|
152
|
+
*inside* `step()`, independent of any Inspect Robots `Approver` and on top of LeRobot's
|
|
153
|
+
own `max_relative_target` slew limit β unclamped model outputs can never reach
|
|
154
|
+
the motors. **Set these to your real, calibrated SO-ARM joint limits** (the
|
|
155
|
+
defaults are conservative placeholders: joints Β±180Β°, gripper 0β100).
|
|
156
|
+
- **Use `ClampApprover`** on hardware for a second layer.
|
|
157
|
+
- **Native units, no renormalization.** LeRobot's postprocessor unnormalizes the
|
|
158
|
+
policy output to the robot's native motor units (degrees for joints, 0β100 for
|
|
159
|
+
the gripper), so the embodiment commands them verbatim after the clamp. Train
|
|
160
|
+
and run the policy in the *same* units. `use_degrees=False` (lerobot's
|
|
161
|
+
normalized Β±100 mode) is **rejected** β the state spec and default limits here
|
|
162
|
+
assume degrees. Also note the units are *degrees* while Inspect Robots's canonical
|
|
163
|
+
`joint_pos` convention is radians: the compat check compares state **keys**
|
|
164
|
+
only, so pairing either component with a third-party counterpart will *not*
|
|
165
|
+
flag a unit mismatch β verify units yourself when mixing stacks.
|
|
166
|
+
- **Homing is slew-limited or refused.** `home_pose` sends a single absolute
|
|
167
|
+
command, so the config requires `max_relative_target` (LeRobot's per-step slew
|
|
168
|
+
limit) whenever `home_pose` is set β otherwise the arm would slam to home at
|
|
169
|
+
full speed from wherever it happens to be.
|
|
170
|
+
- **Absolute vs. delta joints β verify first.** Actions are treated as **absolute**
|
|
171
|
+
joint targets by default. If your checkpoint emits deltas, set
|
|
172
|
+
`SOArmConfig(joints_are_delta=True)` (the embodiment converts to absolute
|
|
173
|
+
internally so the declared `joint_pos` stays honest). The compat check *cannot*
|
|
174
|
+
tell these apart β confirm with `--dry-run` and a single slow jog before a task.
|
|
175
|
+
|
|
176
|
+
## Configuration
|
|
177
|
+
|
|
178
|
+
`SOArmConfig`: `port`, `robot_type`, `robot_id`, `calibration_dir`, `cameras`,
|
|
179
|
+
`camera_configs`, `control_hz`, `cam_height/width`, `joint_low/high`,
|
|
180
|
+
`home_pose` (requires `max_relative_target`), `joints_are_delta`, `use_degrees`
|
|
181
|
+
(must stay `True`), `max_relative_target`, `disable_torque_on_disconnect`.
|
|
182
|
+
`robot_type` is validated (`so101_follower` / `so100_follower`) but is a label:
|
|
183
|
+
at lerobot v0.5.x both names alias the same driver class, so it changes no
|
|
184
|
+
runtime behavior.
|
|
185
|
+
`LeRobotPolicyConfig`: `pretrained_path`, `policy_type`, `device`, `cameras`,
|
|
186
|
+
`state_key`, `chunk_size`, `cam_height/width`.
|
|
187
|
+
|
|
188
|
+
Scalar knobs are settable from the CLI:
|
|
189
|
+
`inspect-robots run -P pretrained_path=lerobot/smolvla_base -E port=/dev/ttyACM0 ...`.
|
|
190
|
+
|
|
191
|
+
## Development
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
uv venv && uv pip install -e ".[dev]" # inspect-robots from a git tag
|
|
195
|
+
uv run pre-commit install
|
|
196
|
+
uv run pytest --cov # 100% coverage required
|
|
197
|
+
uv run ruff check . && uv run mypy
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The whole suite runs with **no hardware, no GPU, no torch, no lerobot, and no
|
|
201
|
+
stdin** β the SO-ARM driver, the policy inference, the clock, and operator I/O are
|
|
202
|
+
all injected. The real model seam (`_default_predict`) is covered via
|
|
203
|
+
`sys.modules` fakes, and a dedicated `lerobot-seam` CI job (py3.12) imports the
|
|
204
|
+
real lerobot symbols it uses; only direct hardware/TTY I/O keeps
|
|
205
|
+
`# pragma: no cover`.
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# π¦Ύ inspect-robots-so101
|
|
4
|
+
|
|
5
|
+
**Run [Inspect Robots](https://github.com/robocurve/inspect-robots) evals on real
|
|
6
|
+
[SO-ARM](https://github.com/TheRobotStudio/SO-ARM100) followers (SO-100 / SO-101)
|
|
7
|
+
driven by [LeRobot](https://github.com/huggingface/lerobot) policies.**
|
|
8
|
+
|
|
9
|
+
[](https://github.com/robocurve/inspect-robots-so101/actions/workflows/ci.yml)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
[](https://github.com/robocurve/inspect-robots-so101/actions/workflows/ci.yml)
|
|
12
|
+
[](https://github.com/robocurve/inspect-robots)
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
Inspect Robots has **two** swappable inputs: a `Policy` (the VLA brain) and an
|
|
17
|
+
`Embodiment` (the robot body + world). This package provides both for the
|
|
18
|
+
SO-ARM + LeRobot stack, so any embodiment-agnostic Inspect Robots task runs on a real
|
|
19
|
+
arm:
|
|
20
|
+
|
|
21
|
+
- **`lerobot` policy** β wraps a LeRobot checkpoint (ACT, SmolVLA, Ο0, diffusionβ¦)
|
|
22
|
+
and runs it **in process** on the GPU, returning an action chunk per inference.
|
|
23
|
+
- **`so_arm` embodiment** β the LeRobot SO follower driver (Feetech bus), with a
|
|
24
|
+
hard safety clamp, operator-in-the-loop success, and self-paced control.
|
|
25
|
+
|
|
26
|
+
Both declare the **same 6-D joint-position contract** (`shoulder_pan`,
|
|
27
|
+
`shoulder_lift`, `elbow_flex`, `wrist_flex`, `wrist_roll`, `gripper`; the cameras
|
|
28
|
+
you configure; packed `joint_pos` state), so Inspect Robots's compatibility check passes
|
|
29
|
+
with **zero errors and zero warnings** β verifiable before any motion.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
inspect-robots run --task cubepick-reach --policy lerobot --embodiment so_arm
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
> This is the SO-ARM/LeRobot sibling of
|
|
36
|
+
> [inspect-robots-yam](https://github.com/robocurve/inspect-robots-yam) (bimanual I2RT YAM +
|
|
37
|
+
> MolmoAct2). Same Inspect Robots contract, different body and brain.
|
|
38
|
+
|
|
39
|
+
## Install (on the robot/GPU machine)
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Inspect Robots isn't on PyPI yet; uv resolves it from git. The `lerobot` extra pulls
|
|
43
|
+
# torch + lerobot + the Feetech motor bus the SO follower uses.
|
|
44
|
+
uv pip install "inspect-robots-so101[lerobot] @ git+https://github.com/robocurve/inspect-robots-so101"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- `lerobot` β `lerobot[feetech]` (torch, the policy, and the SO-ARM driver).
|
|
48
|
+
- **The `lerobot` extra needs Python β₯ 3.12** (lerobot β₯ 0.5's floor). On
|
|
49
|
+
3.10/3.11 the extra silently resolves to *nothing*: the core package still
|
|
50
|
+
imports, but no torch/lerobot is installed and hardware runs will fail.
|
|
51
|
+
|
|
52
|
+
Then pick a checkpoint. Any LeRobot policy trained on your SO-ARM works β e.g. the
|
|
53
|
+
public `lerobot/smolvla_base`, or your own ACT/Ο0 checkpoint on the Hub or a path.
|
|
54
|
+
|
|
55
|
+
## Preflight β *prove compatibility before any motion*
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
inspect-robots-so101-preflight # dims/semantics/cameras/state
|
|
59
|
+
inspect-robots-so101-preflight --task cubepick-reach # + scene realizability
|
|
60
|
+
inspect-robots-so101-preflight --dry-run # affirm no motion
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
A green preflight means action dim (6), control mode (`joint_pos`), cameras, and
|
|
64
|
+
state keys all line up. **It does not prove the joint values are interpreted the
|
|
65
|
+
same way** β see *Safety* below.
|
|
66
|
+
|
|
67
|
+
## Calibrate first (once, with lerobot)
|
|
68
|
+
|
|
69
|
+
The embodiment **never** runs lerobot's interactive calibration β connecting with
|
|
70
|
+
an uncalibrated arm would otherwise drop into a *blocking* prompt that moves the
|
|
71
|
+
arm mid-eval. Calibrate once with lerobot's own tool, then tell the config which
|
|
72
|
+
identity you used:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
lerobot-calibrate --robot.type=so101_follower --robot.port=/dev/ttyACM0 --robot.id=my_follower
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`SOArmConfig(robot_id="my_follower")` selects that calibration file
|
|
79
|
+
(`<calibration_dir>/<robot_id>.json`; leave `calibration_dir=None` for lerobot's
|
|
80
|
+
default location). If the arm isn't calibrated β or the file no longer matches
|
|
81
|
+
the motors β `reset()` fails fast with an actionable error instead of prompting.
|
|
82
|
+
|
|
83
|
+
## Run on hardware
|
|
84
|
+
|
|
85
|
+
You must point the embodiment at your serial port, calibration id, and camera
|
|
86
|
+
config, and the policy at a checkpoint:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from inspect_robots import eval
|
|
90
|
+
from inspect_robots.approver import ClampApprover
|
|
91
|
+
from inspect_robots_so101 import LeRobotPolicy, SOArmEmbodiment, SOArmConfig, LeRobotPolicyConfig
|
|
92
|
+
from lerobot.cameras.opencv import OpenCVCameraConfig # your camera backend
|
|
93
|
+
|
|
94
|
+
emb = SOArmEmbodiment(SOArmConfig(
|
|
95
|
+
port="/dev/ttyACM0",
|
|
96
|
+
robot_type="so101_follower",
|
|
97
|
+
robot_id="my_follower", # the id you ran `lerobot-calibrate` with
|
|
98
|
+
max_relative_target=10.0, # lerobot's slew limit (deg/step); required for home_pose
|
|
99
|
+
cameras=("front",),
|
|
100
|
+
camera_configs={"front": OpenCVCameraConfig(index_or_path=0, width=640, height=480, fps=30)},
|
|
101
|
+
))
|
|
102
|
+
pol = LeRobotPolicy(LeRobotPolicyConfig(
|
|
103
|
+
pretrained_path="lerobot/smolvla_base", policy_type="smolvla", device="cuda",
|
|
104
|
+
))
|
|
105
|
+
|
|
106
|
+
with emb: # guarantees disconnect (and torque-off) even if the eval raises
|
|
107
|
+
(log,) = eval("cubepick-reach", pol, emb,
|
|
108
|
+
approver=ClampApprover(emb.info.action_space)) # defense in depth
|
|
109
|
+
print(log.status, log.results.metrics)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
(Equivalently, wrap the `eval(...)` in `try: ... finally: emb.close()`.)
|
|
113
|
+
|
|
114
|
+
At each episode end the embodiment asks the operator (y/N); a `yes` records
|
|
115
|
+
`termination_reason="success"`, which the task's `success_at_end` scorer reads.
|
|
116
|
+
Unattended runs simply run to `max_steps` and score as failures.
|
|
117
|
+
|
|
118
|
+
## Safety
|
|
119
|
+
|
|
120
|
+
- **Hard clamp backstop.** Every command is clipped to `SOArmConfig.joint_low/high`
|
|
121
|
+
*inside* `step()`, independent of any Inspect Robots `Approver` and on top of LeRobot's
|
|
122
|
+
own `max_relative_target` slew limit β unclamped model outputs can never reach
|
|
123
|
+
the motors. **Set these to your real, calibrated SO-ARM joint limits** (the
|
|
124
|
+
defaults are conservative placeholders: joints Β±180Β°, gripper 0β100).
|
|
125
|
+
- **Use `ClampApprover`** on hardware for a second layer.
|
|
126
|
+
- **Native units, no renormalization.** LeRobot's postprocessor unnormalizes the
|
|
127
|
+
policy output to the robot's native motor units (degrees for joints, 0β100 for
|
|
128
|
+
the gripper), so the embodiment commands them verbatim after the clamp. Train
|
|
129
|
+
and run the policy in the *same* units. `use_degrees=False` (lerobot's
|
|
130
|
+
normalized Β±100 mode) is **rejected** β the state spec and default limits here
|
|
131
|
+
assume degrees. Also note the units are *degrees* while Inspect Robots's canonical
|
|
132
|
+
`joint_pos` convention is radians: the compat check compares state **keys**
|
|
133
|
+
only, so pairing either component with a third-party counterpart will *not*
|
|
134
|
+
flag a unit mismatch β verify units yourself when mixing stacks.
|
|
135
|
+
- **Homing is slew-limited or refused.** `home_pose` sends a single absolute
|
|
136
|
+
command, so the config requires `max_relative_target` (LeRobot's per-step slew
|
|
137
|
+
limit) whenever `home_pose` is set β otherwise the arm would slam to home at
|
|
138
|
+
full speed from wherever it happens to be.
|
|
139
|
+
- **Absolute vs. delta joints β verify first.** Actions are treated as **absolute**
|
|
140
|
+
joint targets by default. If your checkpoint emits deltas, set
|
|
141
|
+
`SOArmConfig(joints_are_delta=True)` (the embodiment converts to absolute
|
|
142
|
+
internally so the declared `joint_pos` stays honest). The compat check *cannot*
|
|
143
|
+
tell these apart β confirm with `--dry-run` and a single slow jog before a task.
|
|
144
|
+
|
|
145
|
+
## Configuration
|
|
146
|
+
|
|
147
|
+
`SOArmConfig`: `port`, `robot_type`, `robot_id`, `calibration_dir`, `cameras`,
|
|
148
|
+
`camera_configs`, `control_hz`, `cam_height/width`, `joint_low/high`,
|
|
149
|
+
`home_pose` (requires `max_relative_target`), `joints_are_delta`, `use_degrees`
|
|
150
|
+
(must stay `True`), `max_relative_target`, `disable_torque_on_disconnect`.
|
|
151
|
+
`robot_type` is validated (`so101_follower` / `so100_follower`) but is a label:
|
|
152
|
+
at lerobot v0.5.x both names alias the same driver class, so it changes no
|
|
153
|
+
runtime behavior.
|
|
154
|
+
`LeRobotPolicyConfig`: `pretrained_path`, `policy_type`, `device`, `cameras`,
|
|
155
|
+
`state_key`, `chunk_size`, `cam_height/width`.
|
|
156
|
+
|
|
157
|
+
Scalar knobs are settable from the CLI:
|
|
158
|
+
`inspect-robots run -P pretrained_path=lerobot/smolvla_base -E port=/dev/ttyACM0 ...`.
|
|
159
|
+
|
|
160
|
+
## Development
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
uv venv && uv pip install -e ".[dev]" # inspect-robots from a git tag
|
|
164
|
+
uv run pre-commit install
|
|
165
|
+
uv run pytest --cov # 100% coverage required
|
|
166
|
+
uv run ruff check . && uv run mypy
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The whole suite runs with **no hardware, no GPU, no torch, no lerobot, and no
|
|
170
|
+
stdin** β the SO-ARM driver, the policy inference, the clock, and operator I/O are
|
|
171
|
+
all injected. The real model seam (`_default_predict`) is covered via
|
|
172
|
+
`sys.modules` fakes, and a dedicated `lerobot-seam` CI job (py3.12) imports the
|
|
173
|
+
real lerobot symbols it uses; only direct hardware/TTY I/O keeps
|
|
174
|
+
`# pragma: no cover`.
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.18"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "inspect-robots-so101"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Inspect Robots adapters for LeRobot SO-ARM followers (SO-100/SO-101) driven by LeRobot policies."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "RoboCurve" }]
|
|
14
|
+
keywords = ["robotics", "vla", "so-arm", "so100", "so101", "lerobot", "inspect-robots"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
dependencies = ["inspect-robots>=0.3", "numpy>=1.24"]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
# Real SO-ARM driver + in-process LeRobot policy inference (torch + the checkpoint).
|
|
27
|
+
# `lerobot[feetech]` pulls the Feetech motor bus the SO follower uses. lerobot>=0.5
|
|
28
|
+
# requires Python>=3.12, so the extra is gated to 3.12+ (the numpy-only core still
|
|
29
|
+
# imports on 3.10/3.11 β the hardware extra just isn't installable there). Without
|
|
30
|
+
# this marker `uv`'s universal resolution fails on the 3.10/3.11 splits.
|
|
31
|
+
# Capped <0.6: the inference seam relies on internal-ish lerobot modules
|
|
32
|
+
# (async_inference.helpers, datasets.feature_utils) whose paths already moved once
|
|
33
|
+
# within 0.5.x; the `lerobot-seam` CI job validates the pin before it is raised.
|
|
34
|
+
lerobot = ["lerobot[feetech]>=0.5,<0.6; python_version >= '3.12'"]
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest>=8.0",
|
|
37
|
+
"pytest-cov>=5.0",
|
|
38
|
+
"ruff>=0.6",
|
|
39
|
+
"mypy>=1.11",
|
|
40
|
+
"pre-commit>=3.5",
|
|
41
|
+
# numpy 2.5.0's type stubs use 3.12-only syntax that mypy (python_version 3.10)
|
|
42
|
+
# rejects; pin the 2.4.x line for a deterministic dev/CI gate (runtime is free).
|
|
43
|
+
"numpy<2.5",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://github.com/robocurve/inspect-robots-so101"
|
|
48
|
+
Framework = "https://github.com/robocurve/inspect-robots"
|
|
49
|
+
LeRobot = "https://github.com/huggingface/lerobot"
|
|
50
|
+
|
|
51
|
+
# inspect-robots isn't on PyPI yet β resolve from a tagged GitHub release. lerobot IS on
|
|
52
|
+
# PyPI (the `lerobot` extra above). This is uv-only metadata (stripped from wheels).
|
|
53
|
+
# For local dev against a sibling inspect-robots checkout: uv pip install -e ../inspect-robots
|
|
54
|
+
[tool.uv.sources]
|
|
55
|
+
inspect-robots = { git = "https://github.com/robocurve/inspect-robots", tag = "v0.3.0" }
|
|
56
|
+
|
|
57
|
+
# Inspect Robots discovers these by entry point β no import needed for `inspect-robots list`.
|
|
58
|
+
[project.entry-points."inspect_robots.embodiments"]
|
|
59
|
+
so_arm = "inspect_robots_so101.embodiment:SOArmEmbodiment"
|
|
60
|
+
|
|
61
|
+
[project.entry-points."inspect_robots.policies"]
|
|
62
|
+
lerobot = "inspect_robots_so101.policy:LeRobotPolicy"
|
|
63
|
+
|
|
64
|
+
[project.scripts]
|
|
65
|
+
inspect-robots-so101-preflight = "inspect_robots_so101.preflight:main"
|
|
66
|
+
|
|
67
|
+
[tool.hatch.build.targets.wheel]
|
|
68
|
+
packages = ["src/inspect_robots_so101"]
|
|
69
|
+
|
|
70
|
+
[tool.hatch.build.targets.sdist]
|
|
71
|
+
include = ["src/inspect_robots_so101", "tests", "README.md"]
|
|
72
|
+
|
|
73
|
+
[tool.ruff]
|
|
74
|
+
line-length = 100
|
|
75
|
+
target-version = "py310"
|
|
76
|
+
src = ["src", "tests"]
|
|
77
|
+
|
|
78
|
+
[tool.ruff.lint]
|
|
79
|
+
select = ["E", "F", "W", "I", "UP", "B", "C4", "SIM", "RUF"]
|
|
80
|
+
|
|
81
|
+
[tool.ruff.lint.isort]
|
|
82
|
+
known-first-party = ["inspect_robots_so101"]
|
|
83
|
+
|
|
84
|
+
[tool.mypy]
|
|
85
|
+
python_version = "3.10"
|
|
86
|
+
strict = true
|
|
87
|
+
files = ["src/inspect_robots_so101"]
|
|
88
|
+
|
|
89
|
+
# torch / lerobot are lazily imported behind pragma'd seams and are not test deps;
|
|
90
|
+
# silence mypy's missing-import errors for them (mirrors inspect-robots-isaacsim).
|
|
91
|
+
[[tool.mypy.overrides]]
|
|
92
|
+
module = ["torch.*", "lerobot.*"]
|
|
93
|
+
ignore_missing_imports = true
|
|
94
|
+
|
|
95
|
+
[tool.pytest.ini_options]
|
|
96
|
+
minversion = "8.0"
|
|
97
|
+
testpaths = ["tests"]
|
|
98
|
+
addopts = "-ra --strict-markers"
|
|
99
|
+
|
|
100
|
+
[tool.coverage.run]
|
|
101
|
+
branch = true
|
|
102
|
+
source = ["inspect_robots_so101"]
|
|
103
|
+
|
|
104
|
+
[tool.coverage.report]
|
|
105
|
+
fail_under = 100
|
|
106
|
+
show_missing = true
|
|
107
|
+
exclude_also = [
|
|
108
|
+
"if TYPE_CHECKING:",
|
|
109
|
+
"@(abc\\.)?abstractmethod",
|
|
110
|
+
"raise NotImplementedError",
|
|
111
|
+
"if __name__ == .__main__.:",
|
|
112
|
+
"^\\s*\\.\\.\\.$",
|
|
113
|
+
": \\.\\.\\.$",
|
|
114
|
+
"pragma: no cover",
|
|
115
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# `inspect_robots_so101` package β module map
|
|
2
|
+
|
|
3
|
+
Two Inspect Robots components + the glue to make them an honest, testable, safe pair.
|
|
4
|
+
The package is `mypy --strict` clean, ships `py.typed`, and is 100%-covered.
|
|
5
|
+
|
|
6
|
+
## Modules
|
|
7
|
+
|
|
8
|
+
| Module | Responsibility |
|
|
9
|
+
|--------|----------------|
|
|
10
|
+
| `packing.py` | **Pure** 6-D SO-ARM packing β the single source of truth for the motor order (`shoulder_pan, shoulder_lift, elbow_flex, wrist_flex, wrist_roll, gripper`) and how the flat vector maps to LeRobot's `"<motor>.pos"` dicts. `to_action_dict`/`from_obs_dict`/`validate_dim`, `STATE_KEY`, `STATE_SPEC`, `MOTORS`. No optional deps. |
|
|
11
|
+
| `config.py` | `SOArmConfig` / `LeRobotPolicyConfig` (frozen, `from_kwargs` for CLI scalars) + shared `action_box()` / `observation_space()` / `ACTION_SEMANTICS` so both components declare an **identical** contract. |
|
|
12
|
+
| `operator.py` | `OperatorIO` (injectable stdin/stdout) for readiness + success prompts; `default_poll_end` (real TTY poll, `# pragma: no cover`). |
|
|
13
|
+
| `policy.py` | `LeRobotPolicy` β wraps a LeRobot checkpoint. `act()` builds the RAW robot payload (`"<motor>.pos"` floats, frames keyed by camera name, `task`), runs the injectable `predict_fn`, truncates to `chunk_size`, returns an `ActionChunk`. Real in-process inference is `_default_predict` (lazy torch + lerobot; obs prep via lerobot's own `raw_observation_to_observation`) β NOT pragma'd: it is covered by `sys.modules`-fake tests, and its real import paths are validated by the `lerobot-seam` CI job. |
|
|
14
|
+
| `embodiment.py` | `SOArmEmbodiment` β LeRobot SO follower driver. Clamp backstop, optional deltaβabs, `SELF_PACED` pacing, operator-keypress success, context manager (`with emb:` guarantees `close()`; `close()` clears the handle even if disconnect raises). The driver's `get_observation` yields motor positions **and** cameras. Hardware seam (`_default_driver_factory`) is injected/pragma'd; it connects with `calibrate=False` and fails fast via the tested `_check_calibrated` (never lerobot's blocking interactive calibration). |
|
|
15
|
+
| `preflight.py` | `build` / `run_preflight` + the `inspect-robots-so101-preflight` CLI: run the compat check, print, exit non-zero on errors. |
|
|
16
|
+
| `__init__.py` | Public API fenced by `__all__` (guarded by `tests/test_api_snapshot.py`). |
|
|
17
|
+
|
|
18
|
+
## Key invariants
|
|
19
|
+
|
|
20
|
+
- **Contract symmetry:** policy and embodiment build their `action_space` /
|
|
21
|
+
`observation_space` from the *same* `config.py` helpers. If you change the dim,
|
|
22
|
+
semantics, camera names, or state key, change them there once β not in two
|
|
23
|
+
places β or compat breaks.
|
|
24
|
+
- **Construction is inert:** `__init__` touches no hardware/model/stdin (only
|
|
25
|
+
`.info`). The driver connects, and the model loads, lazily on first use. This is
|
|
26
|
+
what lets the registry (`factories[name]()`) and preflight construct components
|
|
27
|
+
freely β and keeps `import inspect_robots_so101` free of torch.
|
|
28
|
+
- **Coverage discipline:** the only uncoverable code is hardware/TTY I/O,
|
|
29
|
+
isolated in `# pragma: no cover` seams (`_default_driver_factory`,
|
|
30
|
+
`default_poll_end`, the `_require_driver` pre-reset guard, `__main__`).
|
|
31
|
+
`_default_predict` is NOT pragma'd β it is exercised with `sys.modules` fakes
|
|
32
|
+
(see `tests/test_policy.py`), and the `lerobot-seam` CI job imports the real
|
|
33
|
+
lerobot symbols it relies on. Prefer that pattern (fake the modules, assert the
|
|
34
|
+
wiring, guard the imports in CI) over new pragmas.
|
|
35
|
+
- **Safety lives in `step()`**, not in an optional Approver β see the root
|
|
36
|
+
`CLAUDE.md`.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""inspect-robots-so101 β Inspect Robots adapters for LeRobot SO-ARM followers + LeRobot policies.
|
|
2
|
+
|
|
3
|
+
Registers two Inspect Robots components via entry points:
|
|
4
|
+
|
|
5
|
+
* embodiment ``so_arm`` β :class:`~inspect_robots_so101.embodiment.SOArmEmbodiment`
|
|
6
|
+
* policy ``lerobot`` β :class:`~inspect_robots_so101.policy.LeRobotPolicy`
|
|
7
|
+
|
|
8
|
+
so ``inspect-robots run --task cubepick-reach --policy lerobot --embodiment so_arm``
|
|
9
|
+
works once both packages are installed. Use
|
|
10
|
+
:func:`~inspect_robots_so101.preflight.run_preflight` (or the ``inspect-robots-so101-preflight``
|
|
11
|
+
CLI) to verify compatibility before any motion.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from inspect_robots_so101.config import LeRobotPolicyConfig, SOArmConfig
|
|
17
|
+
from inspect_robots_so101.embodiment import SOArmEmbodiment
|
|
18
|
+
from inspect_robots_so101.operator import OperatorIO
|
|
19
|
+
from inspect_robots_so101.packing import MOTORS, STATE_KEY, TOTAL_DIM, from_obs_dict, to_action_dict
|
|
20
|
+
from inspect_robots_so101.policy import LeRobotPolicy
|
|
21
|
+
from inspect_robots_so101.preflight import build, run_preflight
|
|
22
|
+
|
|
23
|
+
__version__ = "0.3.0"
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"MOTORS",
|
|
27
|
+
"STATE_KEY",
|
|
28
|
+
"TOTAL_DIM",
|
|
29
|
+
"LeRobotPolicy",
|
|
30
|
+
"LeRobotPolicyConfig",
|
|
31
|
+
"OperatorIO",
|
|
32
|
+
"SOArmConfig",
|
|
33
|
+
"SOArmEmbodiment",
|
|
34
|
+
"build",
|
|
35
|
+
"from_obs_dict",
|
|
36
|
+
"run_preflight",
|
|
37
|
+
"to_action_dict",
|
|
38
|
+
]
|