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.
@@ -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
+ [![CI](https://github.com/robocurve/inspect-robots-so101/actions/workflows/ci.yml/badge.svg)](https://github.com/robocurve/inspect-robots-so101/actions/workflows/ci.yml)
41
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
42
+ [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/robocurve/inspect-robots-so101/actions/workflows/ci.yml)
43
+ [![Built on Inspect Robots](https://img.shields.io/badge/built%20on-Inspect%20Robots-indigo)](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
+ [![CI](https://github.com/robocurve/inspect-robots-so101/actions/workflows/ci.yml/badge.svg)](https://github.com/robocurve/inspect-robots-so101/actions/workflows/ci.yml)
10
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
11
+ [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/robocurve/inspect-robots-so101/actions/workflows/ci.yml)
12
+ [![Built on Inspect Robots](https://img.shields.io/badge/built%20on-Inspect%20Robots-indigo)](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
+ ]