jolly-cli 0.3.0__tar.gz → 0.4.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.
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/PKG-INFO +62 -14
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/README.md +56 -11
- jolly_cli-0.4.0/docs/hardware/so101-calibration.example.json +10 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/wiki/CLI-Reference.md +14 -2
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/wiki/Challenges-and-Benchmarks.md +9 -6
- jolly_cli-0.4.0/docs/wiki/Physical-SO-101-Hardware.md +35 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/wiki/_Sidebar.md +1 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/__init__.py +1 -1
- jolly_cli-0.4.0/jolly/benchmark.py +192 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/challenges.py +23 -1
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/cli.py +90 -4
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/core/physics.py +84 -33
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/core/scenes.py +11 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/driver.py +14 -0
- jolly_cli-0.4.0/jolly/hardware/__init__.py +3 -0
- jolly_cli-0.4.0/jolly/hardware/so101.py +297 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/pyproject.toml +5 -4
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/skills/jolly/SKILL.md +4 -3
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/tests/test_cli.py +5 -3
- jolly_cli-0.4.0/tests/test_hardware.py +111 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/tests/test_physics.py +14 -2
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/tests/test_randomization.py +5 -4
- jolly_cli-0.3.0/jolly/benchmark.py +0 -101
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/.github/workflows/publish-pypi.yml +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/.gitignore +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/LICENSE-APACHE +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/LICENSE-MIT +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/SKILL.md +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/THIRD_PARTY_LICENSES.md +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/assets/jolly-so101-viewer.png +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/assets/jolly-terminal-demo.gif +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/assets/jolly-terminal-demo.mp4 +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/assets/jolly-web-demo.gif +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/assets/jolly-web-demo.webm +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/wiki/Home.md +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/wiki/Installation.md +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/wiki/LLM-Agent-Safety.md +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/wiki/Robot-Models-and-Licensing.md +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/wiki/Web-Viewer.md +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/__main__.py +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/__init__.py +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/jolly6.urdf +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/CITATION.cff +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/LICENSE-APACHE +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/SOURCE.md +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/UPSTREAM-README.md +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/base_motor_holder_so101_v1.stl +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/base_so101_v2.stl +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/motor_holder_so101_base_v1.stl +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/motor_holder_so101_wrist_v1.stl +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/moving_jaw_so101_v1.stl +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/rotation_pitch_so101_v1.stl +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/sts3215_03a_no_horn_v1.stl +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/sts3215_03a_v1.stl +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/under_arm_so101_v1.stl +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/upper_arm_so101_v1.stl +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/waveshare_mounting_plate_so101_v2.stl +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/wrist_roll_follower_so101_v1.stl +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/wrist_roll_pitch_so101_v2.stl +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/so101_new_calib.urdf +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/core/__init__.py +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/core/errors.py +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/core/models.py +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/core/store.py +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/engine.py +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/web/__init__.py +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/web/app.py +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/web/index.html +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/tests/test_models.py +0 -0
- {jolly_cli-0.3.0 → jolly_cli-0.4.0}/tests/test_web.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jolly-cli
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: CLI-native SO-101 physics tasks and physical hardware control
|
|
5
5
|
Project-URL: Homepage, https://github.com/TeamAudiyo/Jolly
|
|
6
6
|
Project-URL: Repository, https://github.com/TeamAudiyo/Jolly
|
|
7
7
|
Project-URL: Issues, https://github.com/TeamAudiyo/Jolly/issues
|
|
@@ -9,7 +9,7 @@ Author: TeamAudiyo
|
|
|
9
9
|
License: MIT OR Apache-2.0
|
|
10
10
|
License-File: LICENSE-APACHE
|
|
11
11
|
License-File: LICENSE-MIT
|
|
12
|
-
Keywords: cli,kinematics,llm,pybullet,robotics,
|
|
12
|
+
Keywords: cli,hardware,kinematics,llm,pybullet,robotics,so101
|
|
13
13
|
Classifier: Development Status :: 3 - Alpha
|
|
14
14
|
Classifier: Environment :: Console
|
|
15
15
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
@@ -27,8 +27,11 @@ Requires-Dist: rich<15,>=13.7
|
|
|
27
27
|
Provides-Extra: dev
|
|
28
28
|
Requires-Dist: build<2,>=1.2; extra == 'dev'
|
|
29
29
|
Requires-Dist: httpx<1,>=0.27; extra == 'dev'
|
|
30
|
+
Requires-Dist: pyserial<4,>=3.5; extra == 'dev'
|
|
30
31
|
Requires-Dist: pytest-cov<7,>=5; extra == 'dev'
|
|
31
32
|
Requires-Dist: pytest<9,>=8.2; extra == 'dev'
|
|
33
|
+
Provides-Extra: hardware
|
|
34
|
+
Requires-Dist: pyserial<4,>=3.5; extra == 'hardware'
|
|
32
35
|
Provides-Extra: web
|
|
33
36
|
Requires-Dist: fastapi<1,>=0.115; extra == 'web'
|
|
34
37
|
Requires-Dist: uvicorn<1,>=0.30; extra == 'web'
|
|
@@ -36,8 +39,15 @@ Description-Content-Type: text/markdown
|
|
|
36
39
|
|
|
37
40
|
# Jolly CLI
|
|
38
41
|
|
|
39
|
-
Jolly
|
|
40
|
-
|
|
42
|
+
Jolly provides two separate robot-control backends for terminal users and LLM
|
|
43
|
+
agents:
|
|
44
|
+
|
|
45
|
+
- `JollyEngine` executes measured contact tasks with the official SO-101 model
|
|
46
|
+
in PyBullet.
|
|
47
|
+
- `SO101HardwareDriver` sends the real STS3215 serial protocol to a physical
|
|
48
|
+
SO-101 and reads measured motor positions.
|
|
49
|
+
|
|
50
|
+
The two backends never combine their scores.
|
|
41
51
|
|
|
42
52
|
Jolly includes two offline robot profiles:
|
|
43
53
|
|
|
@@ -99,7 +109,7 @@ jolly move --joints "0,-20,40,-20,0" --gripper 0.0 --json
|
|
|
99
109
|
jolly reach --x 0.30 --y 0.10 --z 0.18 --gripper 0.0 --json
|
|
100
110
|
jolly render
|
|
101
111
|
jolly challenge list --json
|
|
102
|
-
jolly benchmark --json
|
|
112
|
+
jolly benchmark --seed 42017 --cases 3 --model so101 --json
|
|
103
113
|
```
|
|
104
114
|
|
|
105
115
|
Start the optional local web viewer:
|
|
@@ -140,7 +150,8 @@ Important commands:
|
|
|
140
150
|
| `jolly reset` | Select a model and scene, then home the robot. |
|
|
141
151
|
| `jolly scene list/load` | Discover and load deterministic scenes. |
|
|
142
152
|
| `jolly challenge list/start/status` | Run seeded randomized manipulation challenges. |
|
|
143
|
-
| `jolly benchmark [--seed N] [--cases N]` |
|
|
153
|
+
| `jolly benchmark [--seed N] [--cases N]` | Execute randomized reach, obstacle, and drop-in-hole contact tasks. |
|
|
154
|
+
| `jolly hardware state/move/benchmark/stop` | Control and measure a physical SO-101 over its serial bus. |
|
|
144
155
|
|
|
145
156
|
Jolly rejects joint-limit violations and unsafe workspace targets. By default,
|
|
146
157
|
Jolly rolls back a motion that creates a collision. Use `--allow-collision`
|
|
@@ -184,10 +195,10 @@ jolly-cli/
|
|
|
184
195
|
|
|
185
196
|
## Physics scope
|
|
186
197
|
|
|
187
|
-
`JollyEngine` owns state, safety, motion rollback, grasping,
|
|
188
|
-
|
|
189
|
-
benchmark cases. The project does not depend on Inspect, Inspect
|
|
190
|
-
external robotics benchmark driver.
|
|
198
|
+
`JollyEngine` owns state, safety, motion rollback, constrained grasping,
|
|
199
|
+
rendering, and the physics-task contract. `JollyDriver` generates seeded
|
|
200
|
+
challenge and benchmark cases. The project does not depend on Inspect, Inspect
|
|
201
|
+
AI, or an external robotics benchmark driver.
|
|
191
202
|
|
|
192
203
|
PyBullet is the low-level open-source physics backend. It performs rigid-body
|
|
193
204
|
simulation, FK, IK, contact generation, and scene settling. Jolly uses
|
|
@@ -207,9 +218,46 @@ jolly challenge start sort-red --seed 42017 --json
|
|
|
207
218
|
jolly benchmark --seed 42017 --cases 5 --json
|
|
208
219
|
```
|
|
209
220
|
|
|
210
|
-
The benchmark
|
|
211
|
-
|
|
212
|
-
|
|
221
|
+
The benchmark physically executes randomized reach, obstacle avoidance, and
|
|
222
|
+
drop-in-hole tasks. Its score comes from measured reach error, collisions, grasp
|
|
223
|
+
state, physical release, and final peg pose. A generated health check does not
|
|
224
|
+
add points. Different seeds can produce different scores.
|
|
225
|
+
|
|
226
|
+
## Physical SO-101 hardware
|
|
227
|
+
|
|
228
|
+
Install direct physical-hardware support:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
python -m pip install 'jolly-cli[hardware]'
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Jolly implements the Feetech STS3215 packet protocol directly. It does not use
|
|
235
|
+
Inspect Robots or an external robot SDK. Copy and calibrate the example file
|
|
236
|
+
before enabling torque:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
jolly hardware calibration-example --output so101-calibration.json
|
|
240
|
+
# Replace every placeholder with measurements from this physical arm.
|
|
241
|
+
jolly hardware scan --port /dev/ttyACM0 --calibration so101-calibration.json --json
|
|
242
|
+
jolly hardware state --port /dev/ttyACM0 --calibration so101-calibration.json --json
|
|
243
|
+
jolly hardware move --port /dev/ttyACM0 --calibration so101-calibration.json \
|
|
244
|
+
--joints '0,0,0,0,0' --gripper 0 --confirm-hardware --json
|
|
245
|
+
jolly hardware benchmark --port /dev/ttyACM0 --calibration so101-calibration.json \
|
|
246
|
+
--seed 42017 --cases 3 --confirm-hardware --json
|
|
247
|
+
jolly hardware stop --port /dev/ttyACM0 --calibration so101-calibration.json
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The example calibration values are placeholders and set `calibrated` to false.
|
|
251
|
+
Measure every zero point, direction, and gripper endpoint before setting it to
|
|
252
|
+
true. Physical commands require an explicit confirmation, enforce degree and
|
|
253
|
+
raw-count step limits, set bounded goal speed, issue simultaneous goals, and
|
|
254
|
+
read motor feedback. The driver disables torque after communication failures.
|
|
255
|
+
After successful movement it keeps torque enabled so the arm does not fall.
|
|
256
|
+
Support the arm before running `jolly hardware stop`.
|
|
257
|
+
|
|
258
|
+
The physical benchmark scores measured joint-position error only. The physics
|
|
259
|
+
benchmark scores contact tasks only. Jolly never presents physics output as a
|
|
260
|
+
physical-hardware result.
|
|
213
261
|
|
|
214
262
|
The bundled SO-101 model is the official Apache-2.0 new-calibration URDF from
|
|
215
263
|
The Robot Studio. Jolly includes the 13 referenced STL meshes from pinned commit
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
# Jolly CLI
|
|
2
2
|
|
|
3
|
-
Jolly
|
|
4
|
-
|
|
3
|
+
Jolly provides two separate robot-control backends for terminal users and LLM
|
|
4
|
+
agents:
|
|
5
|
+
|
|
6
|
+
- `JollyEngine` executes measured contact tasks with the official SO-101 model
|
|
7
|
+
in PyBullet.
|
|
8
|
+
- `SO101HardwareDriver` sends the real STS3215 serial protocol to a physical
|
|
9
|
+
SO-101 and reads measured motor positions.
|
|
10
|
+
|
|
11
|
+
The two backends never combine their scores.
|
|
5
12
|
|
|
6
13
|
Jolly includes two offline robot profiles:
|
|
7
14
|
|
|
@@ -63,7 +70,7 @@ jolly move --joints "0,-20,40,-20,0" --gripper 0.0 --json
|
|
|
63
70
|
jolly reach --x 0.30 --y 0.10 --z 0.18 --gripper 0.0 --json
|
|
64
71
|
jolly render
|
|
65
72
|
jolly challenge list --json
|
|
66
|
-
jolly benchmark --json
|
|
73
|
+
jolly benchmark --seed 42017 --cases 3 --model so101 --json
|
|
67
74
|
```
|
|
68
75
|
|
|
69
76
|
Start the optional local web viewer:
|
|
@@ -104,7 +111,8 @@ Important commands:
|
|
|
104
111
|
| `jolly reset` | Select a model and scene, then home the robot. |
|
|
105
112
|
| `jolly scene list/load` | Discover and load deterministic scenes. |
|
|
106
113
|
| `jolly challenge list/start/status` | Run seeded randomized manipulation challenges. |
|
|
107
|
-
| `jolly benchmark [--seed N] [--cases N]` |
|
|
114
|
+
| `jolly benchmark [--seed N] [--cases N]` | Execute randomized reach, obstacle, and drop-in-hole contact tasks. |
|
|
115
|
+
| `jolly hardware state/move/benchmark/stop` | Control and measure a physical SO-101 over its serial bus. |
|
|
108
116
|
|
|
109
117
|
Jolly rejects joint-limit violations and unsafe workspace targets. By default,
|
|
110
118
|
Jolly rolls back a motion that creates a collision. Use `--allow-collision`
|
|
@@ -148,10 +156,10 @@ jolly-cli/
|
|
|
148
156
|
|
|
149
157
|
## Physics scope
|
|
150
158
|
|
|
151
|
-
`JollyEngine` owns state, safety, motion rollback, grasping,
|
|
152
|
-
|
|
153
|
-
benchmark cases. The project does not depend on Inspect, Inspect
|
|
154
|
-
external robotics benchmark driver.
|
|
159
|
+
`JollyEngine` owns state, safety, motion rollback, constrained grasping,
|
|
160
|
+
rendering, and the physics-task contract. `JollyDriver` generates seeded
|
|
161
|
+
challenge and benchmark cases. The project does not depend on Inspect, Inspect
|
|
162
|
+
AI, or an external robotics benchmark driver.
|
|
155
163
|
|
|
156
164
|
PyBullet is the low-level open-source physics backend. It performs rigid-body
|
|
157
165
|
simulation, FK, IK, contact generation, and scene settling. Jolly uses
|
|
@@ -171,9 +179,46 @@ jolly challenge start sort-red --seed 42017 --json
|
|
|
171
179
|
jolly benchmark --seed 42017 --cases 5 --json
|
|
172
180
|
```
|
|
173
181
|
|
|
174
|
-
The benchmark
|
|
175
|
-
|
|
176
|
-
|
|
182
|
+
The benchmark physically executes randomized reach, obstacle avoidance, and
|
|
183
|
+
drop-in-hole tasks. Its score comes from measured reach error, collisions, grasp
|
|
184
|
+
state, physical release, and final peg pose. A generated health check does not
|
|
185
|
+
add points. Different seeds can produce different scores.
|
|
186
|
+
|
|
187
|
+
## Physical SO-101 hardware
|
|
188
|
+
|
|
189
|
+
Install direct physical-hardware support:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
python -m pip install 'jolly-cli[hardware]'
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Jolly implements the Feetech STS3215 packet protocol directly. It does not use
|
|
196
|
+
Inspect Robots or an external robot SDK. Copy and calibrate the example file
|
|
197
|
+
before enabling torque:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
jolly hardware calibration-example --output so101-calibration.json
|
|
201
|
+
# Replace every placeholder with measurements from this physical arm.
|
|
202
|
+
jolly hardware scan --port /dev/ttyACM0 --calibration so101-calibration.json --json
|
|
203
|
+
jolly hardware state --port /dev/ttyACM0 --calibration so101-calibration.json --json
|
|
204
|
+
jolly hardware move --port /dev/ttyACM0 --calibration so101-calibration.json \
|
|
205
|
+
--joints '0,0,0,0,0' --gripper 0 --confirm-hardware --json
|
|
206
|
+
jolly hardware benchmark --port /dev/ttyACM0 --calibration so101-calibration.json \
|
|
207
|
+
--seed 42017 --cases 3 --confirm-hardware --json
|
|
208
|
+
jolly hardware stop --port /dev/ttyACM0 --calibration so101-calibration.json
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The example calibration values are placeholders and set `calibrated` to false.
|
|
212
|
+
Measure every zero point, direction, and gripper endpoint before setting it to
|
|
213
|
+
true. Physical commands require an explicit confirmation, enforce degree and
|
|
214
|
+
raw-count step limits, set bounded goal speed, issue simultaneous goals, and
|
|
215
|
+
read motor feedback. The driver disables torque after communication failures.
|
|
216
|
+
After successful movement it keeps torque enabled so the arm does not fall.
|
|
217
|
+
Support the arm before running `jolly hardware stop`.
|
|
218
|
+
|
|
219
|
+
The physical benchmark scores measured joint-position error only. The physics
|
|
220
|
+
benchmark scores contact tasks only. Jolly never presents physics output as a
|
|
221
|
+
physical-hardware result.
|
|
177
222
|
|
|
178
223
|
The bundled SO-101 model is the official Apache-2.0 new-calibration URDF from
|
|
179
224
|
The Robot Studio. Jolly includes the 13 referenced STL meshes from pinned commit
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"motors": {
|
|
3
|
+
"shoulder_pan": {"id": 1, "zero_raw": 2048, "direction": 1, "raw_per_degree": 11.3777777778},
|
|
4
|
+
"shoulder_lift": {"id": 2, "zero_raw": 2048, "direction": 1, "raw_per_degree": 11.3777777778},
|
|
5
|
+
"elbow_flex": {"id": 3, "zero_raw": 2048, "direction": 1, "raw_per_degree": 11.3777777778},
|
|
6
|
+
"wrist_flex": {"id": 4, "zero_raw": 2048, "direction": 1, "raw_per_degree": 11.3777777778},
|
|
7
|
+
"wrist_roll": {"id": 5, "zero_raw": 2048, "direction": 1, "raw_per_degree": 11.3777777778},
|
|
8
|
+
"gripper": {"id": 6, "open_raw": 2048, "closed_raw": 3072}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -23,12 +23,24 @@ jolly scene load blocks --json
|
|
|
23
23
|
jolly challenge list --json
|
|
24
24
|
jolly challenge start sort-red --model jolly6 [--seed N] --json
|
|
25
25
|
jolly challenge status --json
|
|
26
|
-
jolly benchmark [--seed N] [--cases 3] --json
|
|
26
|
+
jolly benchmark [--seed N] [--cases 3] [--model so101] --json
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
Challenge starts use a fresh random seed by default. The JSON response contains
|
|
30
30
|
the seed and generated instance. Supply `--seed N` to replay the exact case.
|
|
31
|
-
The benchmark
|
|
31
|
+
The benchmark executes randomized reach, obstacle, and drop-in-hole tasks through `JollyDriver`.
|
|
32
|
+
|
|
33
|
+
## Physical SO-101
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
jolly hardware scan --port /dev/ttyACM0 --calibration calibration.json --json
|
|
37
|
+
jolly hardware state --port /dev/ttyACM0 --calibration calibration.json --json
|
|
38
|
+
jolly hardware move --port /dev/ttyACM0 --calibration calibration.json --joints "0,0,0,0,0" --gripper 0 --confirm-hardware --json
|
|
39
|
+
jolly hardware benchmark --port /dev/ttyACM0 --calibration calibration.json --seed 42 --cases 3 --confirm-hardware --json
|
|
40
|
+
jolly hardware stop --port /dev/ttyACM0 --calibration calibration.json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Physics and physical-hardware results use separate benchmark names and scores.
|
|
32
44
|
|
|
33
45
|
## Viewers
|
|
34
46
|
|
|
@@ -20,10 +20,13 @@ jolly challenge status --json
|
|
|
20
20
|
The start result contains a generated seed and instance. Use `--seed` to replay
|
|
21
21
|
the exact case.
|
|
22
22
|
|
|
23
|
-
`jolly benchmark --json`
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
`jolly benchmark --json` executes randomized reaching, obstacle avoidance, and
|
|
24
|
+
drop-in-hole tasks. The score uses measured reach error, collision state, grasp
|
|
25
|
+
state, release state, and the peg's final physical pose. Scene construction and
|
|
26
|
+
forward-kinematics health checks contribute no points.
|
|
26
27
|
|
|
27
|
-
Jolly uses its own `JollyEngine
|
|
28
|
-
Inspect AI, or an external robot
|
|
29
|
-
open-source rigid-body
|
|
28
|
+
Jolly uses its own `JollyEngine`, `JollyDriver`, and direct
|
|
29
|
+
`SO101HardwareDriver`. It does not use Inspect, Inspect AI, or an external robot
|
|
30
|
+
benchmark harness. PyBullet remains the local open-source rigid-body backend
|
|
31
|
+
for physics tasks. Physical SO-101 results come only from STS3215 motor feedback
|
|
32
|
+
and remain separate from physics scores.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Physical SO-101 Hardware
|
|
2
|
+
|
|
3
|
+
Install the serial dependency with `pip install 'jolly-cli[hardware]'`.
|
|
4
|
+
|
|
5
|
+
Jolly's `SO101HardwareDriver` sends Feetech STS3215 packets directly at the
|
|
6
|
+
SO-101 default 1,000,000 baud. The configured motor IDs default to the common
|
|
7
|
+
SO-101 sequence of 1 through 6. Each physical arm requires measured calibration.
|
|
8
|
+
|
|
9
|
+
Generate a template, then replace all placeholder zero points, directions, and
|
|
10
|
+
gripper endpoints before movement.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
jolly hardware calibration-example --output calibration.json
|
|
14
|
+
jolly hardware scan --port /dev/ttyACM0 --calibration calibration.json --json
|
|
15
|
+
jolly hardware state --port /dev/ttyACM0 --calibration calibration.json --json
|
|
16
|
+
jolly hardware move --port /dev/ttyACM0 --calibration calibration.json \
|
|
17
|
+
--joints '0,0,0,0,0' --gripper 0 --confirm-hardware --json
|
|
18
|
+
jolly hardware benchmark --port /dev/ttyACM0 --calibration calibration.json \
|
|
19
|
+
--seed 42017 --cases 3 --confirm-hardware --json
|
|
20
|
+
jolly hardware stop --port /dev/ttyACM0 --calibration calibration.json
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The physical benchmark makes bounded movements of at most five degrees from the
|
|
24
|
+
measured starting pose. It scores measured motor-position error and returns to
|
|
25
|
+
the starting pose. Torque stays enabled to hold that pose. Support the arm
|
|
26
|
+
before `jolly hardware stop`. Communication failures trigger a best-effort
|
|
27
|
+
torque disable. Clear the workspace and keep an emergency power cutoff nearby.
|
|
28
|
+
|
|
29
|
+
Physical hardware scores and PyBullet contact-task scores are separate. Jolly
|
|
30
|
+
never labels a physics result as a physical-hardware result.
|
|
31
|
+
|
|
32
|
+
Protocol and hardware references:
|
|
33
|
+
|
|
34
|
+
- https://huggingface.co/docs/lerobot/en/so101
|
|
35
|
+
- https://pages.switch-science.com/comparison/files/feetech/serial-sts/STS3215_datasheet.pdf
|
|
@@ -5,5 +5,6 @@
|
|
|
5
5
|
- [CLI Reference](CLI-Reference)
|
|
6
6
|
- [Robot Models and Licensing](Robot-Models-and-Licensing)
|
|
7
7
|
- [Challenges and Benchmarks](Challenges-and-Benchmarks)
|
|
8
|
+
- [Physical SO-101 Hardware](Physical-SO-101-Hardware)
|
|
8
9
|
- [LLM Agent Safety](LLM-Agent-Safety)
|
|
9
10
|
- [Web Viewer](Web-Viewer)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
from jolly.challenges import list_challenges
|
|
8
|
+
from jolly.core.errors import ConfigurationError
|
|
9
|
+
from jolly.driver import JollyDriver
|
|
10
|
+
from jolly.engine import JollyEngine
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _bounded_accuracy(error: float, full_score_error: float, zero_score_error: float) -> float:
|
|
14
|
+
if error <= full_score_error:
|
|
15
|
+
return 1.0
|
|
16
|
+
if error >= zero_score_error:
|
|
17
|
+
return 0.0
|
|
18
|
+
return 1.0 - (error - full_score_error) / (zero_score_error - full_score_error)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _task_result(
|
|
22
|
+
*,
|
|
23
|
+
name: str,
|
|
24
|
+
weight: float,
|
|
25
|
+
seed: int,
|
|
26
|
+
started: float,
|
|
27
|
+
score: float,
|
|
28
|
+
metrics: dict[str, object],
|
|
29
|
+
error: str | None = None,
|
|
30
|
+
) -> dict[str, object]:
|
|
31
|
+
result: dict[str, object] = {
|
|
32
|
+
"name": name,
|
|
33
|
+
"seed": seed,
|
|
34
|
+
"weight": weight,
|
|
35
|
+
"score": round(max(0.0, min(weight, score)), 3),
|
|
36
|
+
"passed": score >= weight * 0.7,
|
|
37
|
+
"duration_ms": round((time.perf_counter() - started) * 1000, 3),
|
|
38
|
+
"metrics": metrics,
|
|
39
|
+
}
|
|
40
|
+
if error:
|
|
41
|
+
result["error"] = error
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _run_reach_task(model: str, instance: dict[str, Any]) -> dict[str, object]:
|
|
46
|
+
started = time.perf_counter()
|
|
47
|
+
weight = 25.0
|
|
48
|
+
target = [float(value) for value in instance["target"]]
|
|
49
|
+
try:
|
|
50
|
+
with JollyEngine(model=model, scene="empty") as engine:
|
|
51
|
+
state = engine.reach(*target, tolerance=0.06)
|
|
52
|
+
actual = state["end_effector"]["position"]
|
|
53
|
+
error = math.dist(actual, target)
|
|
54
|
+
collision_free = not state["collisions"]["collision"]
|
|
55
|
+
score = weight * _bounded_accuracy(error, 0.015, 0.08)
|
|
56
|
+
if not collision_free:
|
|
57
|
+
score *= 0.25
|
|
58
|
+
return _task_result(
|
|
59
|
+
name="randomized-reach",
|
|
60
|
+
weight=weight,
|
|
61
|
+
seed=int(instance["seed"]),
|
|
62
|
+
started=started,
|
|
63
|
+
score=score,
|
|
64
|
+
metrics={"target": target, "actual": actual, "error_meters": round(error, 6), "collision_free": collision_free},
|
|
65
|
+
)
|
|
66
|
+
except Exception as exc:
|
|
67
|
+
return _task_result(name="randomized-reach", weight=weight, seed=int(instance["seed"]), started=started, score=0.0, metrics={"target": target}, error=str(exc))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _run_obstacle_task(model: str, instance: dict[str, Any]) -> dict[str, object]:
|
|
71
|
+
started = time.perf_counter()
|
|
72
|
+
weight = 25.0
|
|
73
|
+
goal = [float(value) for value in instance["target"]]
|
|
74
|
+
target = [goal[0], goal[1], max(0.07, goal[2] + 0.045)]
|
|
75
|
+
try:
|
|
76
|
+
with JollyEngine(model=model, scene="obstacles") as engine:
|
|
77
|
+
engine.set_object_positions(instance["object_positions"])
|
|
78
|
+
state = engine.reach(*target, tolerance=0.07)
|
|
79
|
+
actual = state["end_effector"]["position"]
|
|
80
|
+
error = math.dist(actual, goal)
|
|
81
|
+
collision_free = not state["collisions"]["collision"]
|
|
82
|
+
score = weight * _bounded_accuracy(error, 0.05, 0.12)
|
|
83
|
+
if not collision_free:
|
|
84
|
+
score = 0.0
|
|
85
|
+
return _task_result(
|
|
86
|
+
name="randomized-obstacle-reach",
|
|
87
|
+
weight=weight,
|
|
88
|
+
seed=int(instance["seed"]),
|
|
89
|
+
started=started,
|
|
90
|
+
score=score,
|
|
91
|
+
metrics={"goal": goal, "commanded_target": target, "actual": actual, "goal_error_meters": round(error, 6), "collision_free": collision_free},
|
|
92
|
+
)
|
|
93
|
+
except Exception as exc:
|
|
94
|
+
return _task_result(name="randomized-obstacle-reach", weight=weight, seed=int(instance["seed"]), started=started, score=0.0, metrics={"target": target}, error=str(exc))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _run_drop_task(model: str, instance: dict[str, Any]) -> dict[str, object]:
|
|
98
|
+
started = time.perf_counter()
|
|
99
|
+
weight = 50.0
|
|
100
|
+
peg_start = [float(value) for value in instance["object_positions"]["peg"]]
|
|
101
|
+
target = [float(value) for value in instance["target"]]
|
|
102
|
+
metrics: dict[str, object] = {"peg_start": peg_start, "hole_center": target}
|
|
103
|
+
try:
|
|
104
|
+
with JollyEngine(model=model, scene="insertion") as engine:
|
|
105
|
+
engine.set_object_positions(instance["object_positions"])
|
|
106
|
+
engine.reach(peg_start[0], peg_start[1], peg_start[2] + 0.10, gripper=0.0, tolerance=0.06)
|
|
107
|
+
grasp_state = engine.reach(peg_start[0], peg_start[1], peg_start[2] + 0.055, gripper=1.0, tolerance=0.07)
|
|
108
|
+
grasped = grasp_state["held_object"] == "peg"
|
|
109
|
+
metrics["grasped"] = grasped
|
|
110
|
+
if grasped:
|
|
111
|
+
engine.reach(peg_start[0], peg_start[1], 0.34, gripper=1.0, tolerance=0.08)
|
|
112
|
+
state = engine.reach(target[0], target[1], 0.34, gripper=1.0, tolerance=0.08)
|
|
113
|
+
for height in (0.34, 0.27, 0.27):
|
|
114
|
+
peg = next(item for item in state["objects"] if item["name"] == "peg")
|
|
115
|
+
tool = state["end_effector"]["position"]
|
|
116
|
+
corrected_x = tool[0] + target[0] - peg["position"][0]
|
|
117
|
+
corrected_y = tool[1] + target[1] - peg["position"][1]
|
|
118
|
+
state = engine.reach(corrected_x, corrected_y, height, gripper=1.0, tolerance=0.08)
|
|
119
|
+
tool = state["end_effector"]["position"]
|
|
120
|
+
state = engine.reach(tool[0], tool[1], 0.27, gripper=0.0, tolerance=0.08)
|
|
121
|
+
else:
|
|
122
|
+
state = grasp_state
|
|
123
|
+
peg = next(item for item in state["objects"] if item["name"] == "peg")
|
|
124
|
+
xy_error = math.dist(peg["position"][:2], target[:2])
|
|
125
|
+
below_rim = peg["position"][2] < float(instance["rim_height"])
|
|
126
|
+
released = state["held_object"] is None
|
|
127
|
+
collision_free = not state["collisions"]["collision"]
|
|
128
|
+
inside = xy_error <= float(instance["hole_radius"]) and below_rim
|
|
129
|
+
score = 0.0
|
|
130
|
+
if grasped:
|
|
131
|
+
score += 10.0
|
|
132
|
+
score += 15.0 * _bounded_accuracy(xy_error, 0.008, 0.06)
|
|
133
|
+
score += 20.0 if inside else (8.0 if below_rim and released else 0.0)
|
|
134
|
+
score += 5.0 if released and collision_free else 0.0
|
|
135
|
+
metrics.update(
|
|
136
|
+
{
|
|
137
|
+
"final_position": peg["position"],
|
|
138
|
+
"xy_error_meters": round(xy_error, 6),
|
|
139
|
+
"inside_hole": inside,
|
|
140
|
+
"below_rim": below_rim,
|
|
141
|
+
"released": released,
|
|
142
|
+
"collision_free": collision_free,
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
return _task_result(name="randomized-drop-in-hole", weight=weight, seed=int(instance["seed"]), started=started, score=score, metrics=metrics)
|
|
146
|
+
except Exception as exc:
|
|
147
|
+
return _task_result(name="randomized-drop-in-hole", weight=weight, seed=int(instance["seed"]), started=started, score=0.0, metrics=metrics, error=str(exc))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def run_benchmark(*, seed: int | None = None, cases: int = 3, model: str = "so101") -> dict[str, object]:
|
|
151
|
+
"""Execute randomized contact tasks and score only measured physical outcomes."""
|
|
152
|
+
if not 1 <= cases <= 25:
|
|
153
|
+
raise ConfigurationError("Benchmark cases must be between 1 and 25.")
|
|
154
|
+
if model not in ("so101", "jolly6"):
|
|
155
|
+
raise ConfigurationError(f"Unsupported benchmark model '{model}'.")
|
|
156
|
+
driver = JollyDriver(seed)
|
|
157
|
+
tasks: list[dict[str, object]] = []
|
|
158
|
+
started = time.perf_counter()
|
|
159
|
+
generators: list[tuple[str, Callable[[str, dict[str, Any]], dict[str, object]]]] = [
|
|
160
|
+
("reach-center", _run_reach_task),
|
|
161
|
+
("obstacle-reach", _run_obstacle_task),
|
|
162
|
+
("drop-in-hole", _run_drop_task),
|
|
163
|
+
]
|
|
164
|
+
for case_index in range(cases):
|
|
165
|
+
for challenge_id, execute in generators:
|
|
166
|
+
instance = driver.challenge_instance(challenge_id)
|
|
167
|
+
result = execute(model, instance)
|
|
168
|
+
result["case"] = case_index + 1
|
|
169
|
+
result["challenge"] = challenge_id
|
|
170
|
+
result["instance"] = instance
|
|
171
|
+
tasks.append(result)
|
|
172
|
+
earned = sum(float(task["score"]) for task in tasks)
|
|
173
|
+
available = sum(float(task["weight"]) for task in tasks)
|
|
174
|
+
score = 100.0 * earned / available if available else 0.0
|
|
175
|
+
passed = sum(1 for task in tasks if task["passed"])
|
|
176
|
+
return {
|
|
177
|
+
"ok": passed == len(tasks),
|
|
178
|
+
"benchmark": "jolly-pybullet-contact-tasks-v4",
|
|
179
|
+
"engine": JollyEngine.name,
|
|
180
|
+
"driver": JollyDriver.name,
|
|
181
|
+
"physics_backend": JollyEngine.physics_backend,
|
|
182
|
+
"model": model,
|
|
183
|
+
"seed": driver.seed,
|
|
184
|
+
"cases": cases,
|
|
185
|
+
"score": round(score, 2),
|
|
186
|
+
"score_basis": "PyBullet reach error, contact-safe motion, contact-confirmed grasp, constrained carry, release, and final peg pose.",
|
|
187
|
+
"passed": passed,
|
|
188
|
+
"total": len(tasks),
|
|
189
|
+
"duration_ms": round((time.perf_counter() - started) * 1000, 3),
|
|
190
|
+
"tasks": tasks,
|
|
191
|
+
"agent_challenges": list_challenges(),
|
|
192
|
+
}
|
|
@@ -49,6 +49,14 @@ CHALLENGES: dict[str, Challenge] = {
|
|
|
49
49
|
success="Tool-to-goal error <= 7 cm and no collision.",
|
|
50
50
|
max_commands=10,
|
|
51
51
|
),
|
|
52
|
+
"drop-in-hole": Challenge(
|
|
53
|
+
id="drop-in-hole",
|
|
54
|
+
name="Drop in hole",
|
|
55
|
+
scene="insertion",
|
|
56
|
+
description="Grasp the generated peg and physically release it through the generated opening.",
|
|
57
|
+
success="The peg settles inside the opening below the rim without robot contact.",
|
|
58
|
+
max_commands=12,
|
|
59
|
+
),
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
|
|
@@ -97,11 +105,25 @@ def evaluate(challenge_id: str, state: dict[str, object]) -> dict[str, object]:
|
|
|
97
105
|
inside = bounds["x"][0] <= position[0] <= bounds["x"][1] and bounds["y"][0] <= position[1] <= bounds["y"][1]
|
|
98
106
|
success = inside and position[2] >= bounds["minimum_z"]
|
|
99
107
|
metrics = {"inside_shelf_xy": inside, "cargo_height_meters": position[2]}
|
|
100
|
-
|
|
108
|
+
elif challenge_id == "obstacle-reach":
|
|
101
109
|
goal = instance["target"]
|
|
102
110
|
error = math.dist(ee, goal)
|
|
103
111
|
success = error <= 0.07 and not collision
|
|
104
112
|
metrics = {"goal_error_meters": round(error, 6), "collision_free": not collision}
|
|
113
|
+
else:
|
|
114
|
+
peg = objects["peg"]["position"]
|
|
115
|
+
target = instance["target"]
|
|
116
|
+
error = math.dist(peg[:2], target[:2])
|
|
117
|
+
rim_height = float(instance["rim_height"])
|
|
118
|
+
inside = error <= float(instance["hole_radius"]) and peg[2] < rim_height
|
|
119
|
+
success = inside and state.get("held_object") is None and not collision
|
|
120
|
+
metrics = {
|
|
121
|
+
"target_xy_error_meters": round(error, 6),
|
|
122
|
+
"peg_height_meters": peg[2],
|
|
123
|
+
"below_rim": peg[2] < rim_height,
|
|
124
|
+
"released": state.get("held_object") is None,
|
|
125
|
+
"collision_free": not collision,
|
|
126
|
+
}
|
|
105
127
|
command_count = int(state.get("challenge_commands", 0))
|
|
106
128
|
within_budget = command_count <= challenge.max_commands
|
|
107
129
|
success = success and within_budget
|