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.
Files changed (70) hide show
  1. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/PKG-INFO +62 -14
  2. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/README.md +56 -11
  3. jolly_cli-0.4.0/docs/hardware/so101-calibration.example.json +10 -0
  4. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/wiki/CLI-Reference.md +14 -2
  5. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/wiki/Challenges-and-Benchmarks.md +9 -6
  6. jolly_cli-0.4.0/docs/wiki/Physical-SO-101-Hardware.md +35 -0
  7. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/wiki/_Sidebar.md +1 -0
  8. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/__init__.py +1 -1
  9. jolly_cli-0.4.0/jolly/benchmark.py +192 -0
  10. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/challenges.py +23 -1
  11. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/cli.py +90 -4
  12. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/core/physics.py +84 -33
  13. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/core/scenes.py +11 -0
  14. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/driver.py +14 -0
  15. jolly_cli-0.4.0/jolly/hardware/__init__.py +3 -0
  16. jolly_cli-0.4.0/jolly/hardware/so101.py +297 -0
  17. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/pyproject.toml +5 -4
  18. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/skills/jolly/SKILL.md +4 -3
  19. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/tests/test_cli.py +5 -3
  20. jolly_cli-0.4.0/tests/test_hardware.py +111 -0
  21. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/tests/test_physics.py +14 -2
  22. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/tests/test_randomization.py +5 -4
  23. jolly_cli-0.3.0/jolly/benchmark.py +0 -101
  24. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/.github/workflows/publish-pypi.yml +0 -0
  25. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/.gitignore +0 -0
  26. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/LICENSE-APACHE +0 -0
  27. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/LICENSE-MIT +0 -0
  28. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/SKILL.md +0 -0
  29. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/THIRD_PARTY_LICENSES.md +0 -0
  30. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/assets/jolly-so101-viewer.png +0 -0
  31. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/assets/jolly-terminal-demo.gif +0 -0
  32. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/assets/jolly-terminal-demo.mp4 +0 -0
  33. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/assets/jolly-web-demo.gif +0 -0
  34. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/assets/jolly-web-demo.webm +0 -0
  35. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/wiki/Home.md +0 -0
  36. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/wiki/Installation.md +0 -0
  37. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/wiki/LLM-Agent-Safety.md +0 -0
  38. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/wiki/Robot-Models-and-Licensing.md +0 -0
  39. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/docs/wiki/Web-Viewer.md +0 -0
  40. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/__main__.py +0 -0
  41. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/__init__.py +0 -0
  42. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/jolly6.urdf +0 -0
  43. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/CITATION.cff +0 -0
  44. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/LICENSE-APACHE +0 -0
  45. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/SOURCE.md +0 -0
  46. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/UPSTREAM-README.md +0 -0
  47. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/base_motor_holder_so101_v1.stl +0 -0
  48. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/base_so101_v2.stl +0 -0
  49. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/motor_holder_so101_base_v1.stl +0 -0
  50. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/motor_holder_so101_wrist_v1.stl +0 -0
  51. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/moving_jaw_so101_v1.stl +0 -0
  52. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/rotation_pitch_so101_v1.stl +0 -0
  53. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/sts3215_03a_no_horn_v1.stl +0 -0
  54. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/sts3215_03a_v1.stl +0 -0
  55. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/under_arm_so101_v1.stl +0 -0
  56. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/upper_arm_so101_v1.stl +0 -0
  57. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/waveshare_mounting_plate_so101_v2.stl +0 -0
  58. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/wrist_roll_follower_so101_v1.stl +0 -0
  59. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/assets/wrist_roll_pitch_so101_v2.stl +0 -0
  60. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/assets/robots/so101/so101_new_calib.urdf +0 -0
  61. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/core/__init__.py +0 -0
  62. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/core/errors.py +0 -0
  63. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/core/models.py +0 -0
  64. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/core/store.py +0 -0
  65. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/engine.py +0 -0
  66. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/web/__init__.py +0 -0
  67. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/web/app.py +0 -0
  68. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/jolly/web/index.html +0 -0
  69. {jolly_cli-0.3.0 → jolly_cli-0.4.0}/tests/test_models.py +0 -0
  70. {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.3.0
4
- Summary: Local, CLI-native robot-arm physics simulation for humans and LLM agents
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,simulation
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 is a local robot-arm physics simulator for terminal users and LLM agents.
40
- It uses PyBullet in deterministic direct mode and saves state between commands.
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]` | Run randomized engine checks with reproducible inputs. |
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, rendering, and the
188
- public simulator contract. `JollyDriver` generates seeded challenge and
189
- benchmark cases. The project does not depend on Inspect, Inspect AI, or an
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 generates new joint configurations for every robot case and new
211
- object layouts for every scene case. This prevents a policy from passing only by
212
- memorizing the original fixed coordinates.
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 is a local robot-arm physics simulator for terminal users and LLM agents.
4
- It uses PyBullet in deterministic direct mode and saves state between commands.
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]` | Run randomized engine checks with reproducible inputs. |
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, rendering, and the
152
- public simulator contract. `JollyDriver` generates seeded challenge and
153
- benchmark cases. The project does not depend on Inspect, Inspect AI, or an
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 generates new joint configurations for every robot case and new
175
- object layouts for every scene case. This prevents a policy from passing only by
176
- memorizing the original fixed coordinates.
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 randomizes every robot and scene check through `JollyDriver`.
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` checks engine health with randomized joint states and
24
- scene layouts. Use `--cases` to select cases per model and scene. Use `--seed`
25
- to replay the exact generated inputs.
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` and `JollyDriver`. It does not use Inspect,
28
- Inspect AI, or an external robot benchmark harness. PyBullet remains the local
29
- open-source rigid-body physics backend.
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)
@@ -1,4 +1,4 @@
1
1
  """Jolly robot-arm simulator."""
2
2
 
3
3
  __all__ = ["__version__"]
4
- __version__ = "0.3.0"
4
+ __version__ = "0.4.0"
@@ -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
- else:
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