jolly-cli 0.3.0__tar.gz → 0.5.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 (76) hide show
  1. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/PKG-INFO +88 -24
  2. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/README.md +82 -21
  3. jolly_cli-0.5.0/docs/assets/jolly-so101-viewer.png +0 -0
  4. jolly_cli-0.5.0/docs/assets/jolly-terminal-pick-place-v050.gif +0 -0
  5. jolly_cli-0.5.0/docs/assets/jolly-terminal-pick-place-v050.mp4 +0 -0
  6. jolly_cli-0.5.0/docs/assets/jolly-web-demo.gif +0 -0
  7. jolly_cli-0.5.0/docs/assets/jolly-web-demo.webm +0 -0
  8. jolly_cli-0.5.0/docs/hardware/so101-calibration.example.json +10 -0
  9. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/docs/wiki/CLI-Reference.md +20 -2
  10. jolly_cli-0.5.0/docs/wiki/Challenges-and-Benchmarks.md +36 -0
  11. jolly_cli-0.5.0/docs/wiki/Physical-SO-101-Hardware.md +35 -0
  12. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/docs/wiki/Web-Viewer.md +9 -1
  13. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/docs/wiki/_Sidebar.md +1 -0
  14. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/__init__.py +1 -1
  15. jolly_cli-0.5.0/jolly/benchmark.py +85 -0
  16. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/challenges.py +23 -1
  17. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/cli.py +218 -11
  18. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/core/physics.py +84 -33
  19. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/core/scenes.py +11 -0
  20. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/driver.py +14 -0
  21. jolly_cli-0.5.0/jolly/hardware/__init__.py +3 -0
  22. jolly_cli-0.5.0/jolly/hardware/so101.py +297 -0
  23. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/web/app.py +36 -1
  24. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/pyproject.toml +5 -4
  25. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/skills/jolly/SKILL.md +8 -4
  26. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/tests/test_cli.py +63 -4
  27. jolly_cli-0.5.0/tests/test_hardware.py +111 -0
  28. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/tests/test_physics.py +14 -2
  29. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/tests/test_randomization.py +6 -14
  30. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/tests/test_web.py +42 -0
  31. jolly_cli-0.3.0/docs/assets/jolly-so101-viewer.png +0 -0
  32. jolly_cli-0.3.0/docs/assets/jolly-terminal-demo.gif +0 -0
  33. jolly_cli-0.3.0/docs/assets/jolly-terminal-demo.mp4 +0 -0
  34. jolly_cli-0.3.0/docs/assets/jolly-web-demo.gif +0 -0
  35. jolly_cli-0.3.0/docs/assets/jolly-web-demo.webm +0 -0
  36. jolly_cli-0.3.0/docs/wiki/Challenges-and-Benchmarks.md +0 -29
  37. jolly_cli-0.3.0/jolly/benchmark.py +0 -101
  38. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/.github/workflows/publish-pypi.yml +0 -0
  39. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/.gitignore +0 -0
  40. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/LICENSE-APACHE +0 -0
  41. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/LICENSE-MIT +0 -0
  42. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/SKILL.md +0 -0
  43. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/THIRD_PARTY_LICENSES.md +0 -0
  44. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/docs/wiki/Home.md +0 -0
  45. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/docs/wiki/Installation.md +0 -0
  46. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/docs/wiki/LLM-Agent-Safety.md +0 -0
  47. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/docs/wiki/Robot-Models-and-Licensing.md +0 -0
  48. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/__main__.py +0 -0
  49. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/assets/__init__.py +0 -0
  50. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/assets/jolly6.urdf +0 -0
  51. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/assets/robots/so101/CITATION.cff +0 -0
  52. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/assets/robots/so101/LICENSE-APACHE +0 -0
  53. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/assets/robots/so101/SOURCE.md +0 -0
  54. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/assets/robots/so101/UPSTREAM-README.md +0 -0
  55. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/assets/robots/so101/assets/base_motor_holder_so101_v1.stl +0 -0
  56. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/assets/robots/so101/assets/base_so101_v2.stl +0 -0
  57. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/assets/robots/so101/assets/motor_holder_so101_base_v1.stl +0 -0
  58. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/assets/robots/so101/assets/motor_holder_so101_wrist_v1.stl +0 -0
  59. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/assets/robots/so101/assets/moving_jaw_so101_v1.stl +0 -0
  60. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/assets/robots/so101/assets/rotation_pitch_so101_v1.stl +0 -0
  61. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/assets/robots/so101/assets/sts3215_03a_no_horn_v1.stl +0 -0
  62. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/assets/robots/so101/assets/sts3215_03a_v1.stl +0 -0
  63. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/assets/robots/so101/assets/under_arm_so101_v1.stl +0 -0
  64. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/assets/robots/so101/assets/upper_arm_so101_v1.stl +0 -0
  65. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/assets/robots/so101/assets/waveshare_mounting_plate_so101_v2.stl +0 -0
  66. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/assets/robots/so101/assets/wrist_roll_follower_so101_v1.stl +0 -0
  67. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/assets/robots/so101/assets/wrist_roll_pitch_so101_v2.stl +0 -0
  68. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/assets/robots/so101/so101_new_calib.urdf +0 -0
  69. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/core/__init__.py +0 -0
  70. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/core/errors.py +0 -0
  71. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/core/models.py +0 -0
  72. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/core/store.py +0 -0
  73. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/engine.py +0 -0
  74. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/web/__init__.py +0 -0
  75. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/jolly/web/index.html +0 -0
  76. {jolly_cli-0.3.0 → jolly_cli-0.5.0}/tests/test_models.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.5.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 explicit operator controls with the official SO-101
46
+ model in PyBullet, then measures the final physical state.
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 results.
41
51
 
42
52
  Jolly includes two offline robot profiles:
43
53
 
@@ -48,20 +58,30 @@ The package also accepts scenes, challenges, an engine benchmark, a native
48
58
  PyBullet viewer, and an optional local web viewer. No cloud service, MCP server,
49
59
  API key, or network connection is required at runtime.
50
60
 
51
- ## See Jolly in action
61
+ ## See Jolly 0.5.0 in action
62
+
63
+ ### Interactive SO-101 simulator
64
+
65
+ ![Jolly web viewer executing a Cartesian reach in the insertion scene](docs/assets/jolly-web-demo.gif)
52
66
 
53
- ### Local web simulator
67
+ [Watch the full WebM recording](docs/assets/jolly-web-demo.webm) ·
68
+ [Open the full-resolution still](docs/assets/jolly-so101-viewer.png)
54
69
 
55
- ![Jolly web viewer rendering the official SO-101 CAD model](docs/assets/jolly-so101-viewer.png)
70
+ The recording uses the official SO-101 CAD meshes in a real PyBullet world. It
71
+ executes a Cartesian reach in the insertion scene. The viewport is not a
72
+ diagram or a hand-drawn robot substitute.
56
73
 
57
- The viewport above is a real PyBullet render of the official SO-101 CAD meshes.
58
- It is not a diagram or a hand-drawn robot substitute.
74
+ ### Operator-controlled randomized pick-and-place
59
75
 
60
- ### Terminal-native control
76
+ ![Jolly 0.5.0 terminal demo showing explicit pick-and-place controls](docs/assets/jolly-terminal-pick-place-v050.gif)
61
77
 
62
- ![Jolly terminal CLI demo](docs/assets/jolly-terminal-demo.gif)
78
+ [Watch the MP4 recording](docs/assets/jolly-terminal-pick-place-v050.mp4)
63
79
 
64
- [Watch the MP4 recording](docs/assets/jolly-terminal-demo.mp4)
80
+ The terminal recording shows every control: move above the peg, move down,
81
+ close the gripper, lift, move sideways, lower, and release. `benchmark score`
82
+ does not move the robot and does not generate a numeric score. It reports PASS
83
+ only from the final measured peg pose, release state, collision state, and
84
+ command budget.
65
85
 
66
86
  ## Install
67
87
 
@@ -99,7 +119,9 @@ jolly move --joints "0,-20,40,-20,0" --gripper 0.0 --json
99
119
  jolly reach --x 0.30 --y 0.10 --z 0.18 --gripper 0.0 --json
100
120
  jolly render
101
121
  jolly challenge list --json
102
- jolly benchmark --json
122
+ jolly benchmark start --seed 42017 --model so101 --json
123
+ # Issue explicit jolly reach controls using the generated peg and hole coordinates.
124
+ jolly benchmark score --json
103
125
  ```
104
126
 
105
127
  Start the optional local web viewer:
@@ -140,7 +162,8 @@ Important commands:
140
162
  | `jolly reset` | Select a model and scene, then home the robot. |
141
163
  | `jolly scene list/load` | Discover and load deterministic scenes. |
142
164
  | `jolly challenge list/start/status` | Run seeded randomized manipulation challenges. |
143
- | `jolly benchmark [--seed N] [--cases N]` | Run randomized engine checks with reproducible inputs. |
165
+ | `jolly benchmark start/score` | Generate a randomized pick-and-place task, accept explicit controls, then measure PASS or FAIL. |
166
+ | `jolly hardware state/move/benchmark/stop` | Control and measure a physical SO-101 over its serial bus. |
144
167
 
145
168
  Jolly rejects joint-limit violations and unsafe workspace targets. By default,
146
169
  Jolly rolls back a motion that creates a collision. Use `--allow-collision`
@@ -184,15 +207,15 @@ jolly-cli/
184
207
 
185
208
  ## Physics scope
186
209
 
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.
210
+ `JollyEngine` owns state, safety, motion rollback, constrained grasping,
211
+ rendering, and the physics-task contract. `JollyDriver` generates seeded
212
+ challenge and benchmark cases. The project does not depend on Inspect, Inspect
213
+ AI, or an external robotics benchmark driver.
191
214
 
192
215
  PyBullet is the low-level open-source physics backend. It performs rigid-body
193
216
  simulation, FK, IK, contact generation, and scene settling. Jolly uses
194
217
  deterministic state restoration across short CLI processes.
195
- The grasp helper attaches a nearby graspable object while the gripper is closed.
218
+ The grasp helper attaches one contacted graspable object while the gripper is closed.
196
219
  This helper makes terminal pick-and-place repeatable. It is not a soft-contact or
197
220
  motor-current model.
198
221
 
@@ -204,12 +227,53 @@ The result includes the seed and full instance so results stay auditable. Omit
204
227
 
205
228
  ```bash
206
229
  jolly challenge start sort-red --seed 42017 --json
207
- jolly benchmark --seed 42017 --cases 5 --json
230
+ jolly benchmark start --seed 42017 --model so101 --json
231
+ # Read the generated instance, then issue each reach control yourself.
232
+ jolly benchmark score --json
233
+ ```
234
+
235
+ Benchmark start generates the randomized peg and hole but executes no motion.
236
+ Only explicit `jolly reach` or `jolly move` commands can change the attempt.
237
+ Benchmark score executes no motion and returns no numeric score. PASS requires
238
+ the released peg to settle inside the generated hole without collision and
239
+ within the command budget. The JSON result includes every requested control and
240
+ its measured tool, gripper, grasp, and collision state.
241
+
242
+ ## Physical SO-101 hardware
243
+
244
+ Install direct physical-hardware support:
245
+
246
+ ```bash
247
+ python -m pip install 'jolly-cli[hardware]'
248
+ ```
249
+
250
+ Jolly implements the Feetech STS3215 packet protocol directly. It does not use
251
+ Inspect Robots or an external robot SDK. Copy and calibrate the example file
252
+ before enabling torque:
253
+
254
+ ```bash
255
+ jolly hardware calibration-example --output so101-calibration.json
256
+ # Replace every placeholder with measurements from this physical arm.
257
+ jolly hardware scan --port /dev/ttyACM0 --calibration so101-calibration.json --json
258
+ jolly hardware state --port /dev/ttyACM0 --calibration so101-calibration.json --json
259
+ jolly hardware move --port /dev/ttyACM0 --calibration so101-calibration.json \
260
+ --joints '0,0,0,0,0' --gripper 0 --confirm-hardware --json
261
+ jolly hardware benchmark --port /dev/ttyACM0 --calibration so101-calibration.json \
262
+ --seed 42017 --cases 3 --confirm-hardware --json
263
+ jolly hardware stop --port /dev/ttyACM0 --calibration so101-calibration.json
208
264
  ```
209
265
 
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.
266
+ The example calibration values are placeholders and set `calibrated` to false.
267
+ Measure every zero point, direction, and gripper endpoint before setting it to
268
+ true. Physical commands require an explicit confirmation, enforce degree and
269
+ raw-count step limits, set bounded goal speed, issue simultaneous goals, and
270
+ read motor feedback. The driver disables torque after communication failures.
271
+ After successful movement it keeps torque enabled so the arm does not fall.
272
+ Support the arm before running `jolly hardware stop`.
273
+
274
+ The physical benchmark scores measured joint-position error only. The PyBullet
275
+ operator benchmark returns a measured PASS or FAIL without a numeric score.
276
+ Jolly never presents physics output as a physical-hardware result.
213
277
 
214
278
  The bundled SO-101 model is the official Apache-2.0 new-calibration URDF from
215
279
  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 explicit operator controls with the official SO-101
7
+ model in PyBullet, then measures the final physical state.
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 results.
5
12
 
6
13
  Jolly includes two offline robot profiles:
7
14
 
@@ -12,20 +19,30 @@ The package also accepts scenes, challenges, an engine benchmark, a native
12
19
  PyBullet viewer, and an optional local web viewer. No cloud service, MCP server,
13
20
  API key, or network connection is required at runtime.
14
21
 
15
- ## See Jolly in action
22
+ ## See Jolly 0.5.0 in action
23
+
24
+ ### Interactive SO-101 simulator
25
+
26
+ ![Jolly web viewer executing a Cartesian reach in the insertion scene](docs/assets/jolly-web-demo.gif)
16
27
 
17
- ### Local web simulator
28
+ [Watch the full WebM recording](docs/assets/jolly-web-demo.webm) ·
29
+ [Open the full-resolution still](docs/assets/jolly-so101-viewer.png)
18
30
 
19
- ![Jolly web viewer rendering the official SO-101 CAD model](docs/assets/jolly-so101-viewer.png)
31
+ The recording uses the official SO-101 CAD meshes in a real PyBullet world. It
32
+ executes a Cartesian reach in the insertion scene. The viewport is not a
33
+ diagram or a hand-drawn robot substitute.
20
34
 
21
- The viewport above is a real PyBullet render of the official SO-101 CAD meshes.
22
- It is not a diagram or a hand-drawn robot substitute.
35
+ ### Operator-controlled randomized pick-and-place
23
36
 
24
- ### Terminal-native control
37
+ ![Jolly 0.5.0 terminal demo showing explicit pick-and-place controls](docs/assets/jolly-terminal-pick-place-v050.gif)
25
38
 
26
- ![Jolly terminal CLI demo](docs/assets/jolly-terminal-demo.gif)
39
+ [Watch the MP4 recording](docs/assets/jolly-terminal-pick-place-v050.mp4)
27
40
 
28
- [Watch the MP4 recording](docs/assets/jolly-terminal-demo.mp4)
41
+ The terminal recording shows every control: move above the peg, move down,
42
+ close the gripper, lift, move sideways, lower, and release. `benchmark score`
43
+ does not move the robot and does not generate a numeric score. It reports PASS
44
+ only from the final measured peg pose, release state, collision state, and
45
+ command budget.
29
46
 
30
47
  ## Install
31
48
 
@@ -63,7 +80,9 @@ jolly move --joints "0,-20,40,-20,0" --gripper 0.0 --json
63
80
  jolly reach --x 0.30 --y 0.10 --z 0.18 --gripper 0.0 --json
64
81
  jolly render
65
82
  jolly challenge list --json
66
- jolly benchmark --json
83
+ jolly benchmark start --seed 42017 --model so101 --json
84
+ # Issue explicit jolly reach controls using the generated peg and hole coordinates.
85
+ jolly benchmark score --json
67
86
  ```
68
87
 
69
88
  Start the optional local web viewer:
@@ -104,7 +123,8 @@ Important commands:
104
123
  | `jolly reset` | Select a model and scene, then home the robot. |
105
124
  | `jolly scene list/load` | Discover and load deterministic scenes. |
106
125
  | `jolly challenge list/start/status` | Run seeded randomized manipulation challenges. |
107
- | `jolly benchmark [--seed N] [--cases N]` | Run randomized engine checks with reproducible inputs. |
126
+ | `jolly benchmark start/score` | Generate a randomized pick-and-place task, accept explicit controls, then measure PASS or FAIL. |
127
+ | `jolly hardware state/move/benchmark/stop` | Control and measure a physical SO-101 over its serial bus. |
108
128
 
109
129
  Jolly rejects joint-limit violations and unsafe workspace targets. By default,
110
130
  Jolly rolls back a motion that creates a collision. Use `--allow-collision`
@@ -148,15 +168,15 @@ jolly-cli/
148
168
 
149
169
  ## Physics scope
150
170
 
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.
171
+ `JollyEngine` owns state, safety, motion rollback, constrained grasping,
172
+ rendering, and the physics-task contract. `JollyDriver` generates seeded
173
+ challenge and benchmark cases. The project does not depend on Inspect, Inspect
174
+ AI, or an external robotics benchmark driver.
155
175
 
156
176
  PyBullet is the low-level open-source physics backend. It performs rigid-body
157
177
  simulation, FK, IK, contact generation, and scene settling. Jolly uses
158
178
  deterministic state restoration across short CLI processes.
159
- The grasp helper attaches a nearby graspable object while the gripper is closed.
179
+ The grasp helper attaches one contacted graspable object while the gripper is closed.
160
180
  This helper makes terminal pick-and-place repeatable. It is not a soft-contact or
161
181
  motor-current model.
162
182
 
@@ -168,12 +188,53 @@ The result includes the seed and full instance so results stay auditable. Omit
168
188
 
169
189
  ```bash
170
190
  jolly challenge start sort-red --seed 42017 --json
171
- jolly benchmark --seed 42017 --cases 5 --json
191
+ jolly benchmark start --seed 42017 --model so101 --json
192
+ # Read the generated instance, then issue each reach control yourself.
193
+ jolly benchmark score --json
194
+ ```
195
+
196
+ Benchmark start generates the randomized peg and hole but executes no motion.
197
+ Only explicit `jolly reach` or `jolly move` commands can change the attempt.
198
+ Benchmark score executes no motion and returns no numeric score. PASS requires
199
+ the released peg to settle inside the generated hole without collision and
200
+ within the command budget. The JSON result includes every requested control and
201
+ its measured tool, gripper, grasp, and collision state.
202
+
203
+ ## Physical SO-101 hardware
204
+
205
+ Install direct physical-hardware support:
206
+
207
+ ```bash
208
+ python -m pip install 'jolly-cli[hardware]'
209
+ ```
210
+
211
+ Jolly implements the Feetech STS3215 packet protocol directly. It does not use
212
+ Inspect Robots or an external robot SDK. Copy and calibrate the example file
213
+ before enabling torque:
214
+
215
+ ```bash
216
+ jolly hardware calibration-example --output so101-calibration.json
217
+ # Replace every placeholder with measurements from this physical arm.
218
+ jolly hardware scan --port /dev/ttyACM0 --calibration so101-calibration.json --json
219
+ jolly hardware state --port /dev/ttyACM0 --calibration so101-calibration.json --json
220
+ jolly hardware move --port /dev/ttyACM0 --calibration so101-calibration.json \
221
+ --joints '0,0,0,0,0' --gripper 0 --confirm-hardware --json
222
+ jolly hardware benchmark --port /dev/ttyACM0 --calibration so101-calibration.json \
223
+ --seed 42017 --cases 3 --confirm-hardware --json
224
+ jolly hardware stop --port /dev/ttyACM0 --calibration so101-calibration.json
172
225
  ```
173
226
 
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.
227
+ The example calibration values are placeholders and set `calibrated` to false.
228
+ Measure every zero point, direction, and gripper endpoint before setting it to
229
+ true. Physical commands require an explicit confirmation, enforce degree and
230
+ raw-count step limits, set bounded goal speed, issue simultaneous goals, and
231
+ read motor feedback. The driver disables torque after communication failures.
232
+ After successful movement it keeps torque enabled so the arm does not fall.
233
+ Support the arm before running `jolly hardware stop`.
234
+
235
+ The physical benchmark scores measured joint-position error only. The PyBullet
236
+ operator benchmark returns a measured PASS or FAIL without a numeric score.
237
+ Jolly never presents physics output as a physical-hardware result.
177
238
 
178
239
  The bundled SO-101 model is the official Apache-2.0 new-calibration URDF from
179
240
  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,30 @@ 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 start [--seed N] [--model so101] --json
27
+ # Issue explicit jolly reach or jolly move controls.
28
+ jolly benchmark score --json
27
29
  ```
28
30
 
29
31
  Challenge starts use a fresh random seed by default. The JSON response contains
30
32
  the seed and generated instance. Supply `--seed N` to replay the exact case.
31
- The benchmark randomizes every robot and scene check through `JollyDriver`.
33
+ Benchmark start generates a randomized drop-in-hole task through `JollyDriver`
34
+ without moving the robot. Benchmark score measures only the state produced by
35
+ the intervening explicit controls and returns PASS or FAIL without a numeric
36
+ score.
37
+
38
+ ## Physical SO-101
39
+
40
+ ```bash
41
+ jolly hardware scan --port /dev/ttyACM0 --calibration calibration.json --json
42
+ jolly hardware state --port /dev/ttyACM0 --calibration calibration.json --json
43
+ jolly hardware move --port /dev/ttyACM0 --calibration calibration.json --joints "0,0,0,0,0" --gripper 0 --confirm-hardware --json
44
+ jolly hardware benchmark --port /dev/ttyACM0 --calibration calibration.json --seed 42 --cases 3 --confirm-hardware --json
45
+ jolly hardware stop --port /dev/ttyACM0 --calibration calibration.json
46
+ ```
47
+
48
+ PyBullet and physical-hardware results use separate benchmark names. The
49
+ PyBullet operator benchmark has no numeric score.
32
50
 
33
51
  ## Viewers
34
52
 
@@ -0,0 +1,36 @@
1
+ # Challenges and Benchmarks
2
+
3
+ Jolly includes local scene templates:
4
+
5
+ - `empty`: clear workspace for kinematics.
6
+ - `blocks`: three blocks and two target pads.
7
+ - `shelf`: floor-to-shelf manipulation.
8
+ - `obstacles`: collision-aware reaching.
9
+
10
+ Agent challenges include `reach-center`, `sort-red`, `shelf-load`, and
11
+ `obstacle-reach`. `JollyDriver` generates a new target and scene layout for each
12
+ start. Each challenge has a command budget and explicit success metrics.
13
+
14
+ ```bash
15
+ jolly challenge start sort-red --model jolly6 --json
16
+ # Run move and reach commands.
17
+ jolly challenge status --json
18
+ ```
19
+
20
+ The start result contains a generated seed and instance. Use `--seed` to replay
21
+ the exact case.
22
+
23
+ `jolly benchmark start --json` generates a randomized peg and hole but does not
24
+ move the robot. The operator must issue each `jolly reach` or `jolly move`
25
+ control to approach, descend, grasp, lift, move sideways, lower, and release.
26
+
27
+ `jolly benchmark score --json` does not move the robot and returns no numeric
28
+ score. It returns PASS only when the released peg settles inside the generated
29
+ hole without collision and within the command budget. The result includes the
30
+ full explicit control history and measured final state.
31
+
32
+ Jolly uses its own `JollyEngine`, `JollyDriver`, and direct
33
+ `SO101HardwareDriver`. It does not use Inspect, Inspect AI, or an external robot
34
+ benchmark harness. PyBullet remains the local open-source rigid-body backend
35
+ for physics tasks. Physical SO-101 results come only from STS3215 motor feedback
36
+ and remain separate from the PyBullet PASS or FAIL result.
@@ -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 operator PASS or FAIL results 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
@@ -1,5 +1,10 @@
1
1
  # Web Viewer
2
2
 
3
+ ![Jolly web viewer executing a Cartesian reach in the insertion scene](../assets/jolly-web-demo.gif)
4
+
5
+ [Watch the full WebM recording](../assets/jolly-web-demo.webm) or
6
+ [open the full-resolution still](../assets/jolly-so101-viewer.png).
7
+
3
8
  Start the local viewer:
4
9
 
5
10
  ```bash
@@ -11,7 +16,7 @@ Open `http://127.0.0.1:8765`. API documentation is available at
11
16
 
12
17
  The viewer provides:
13
18
 
14
- - An interactive projected 3D arm scene.
19
+ - An interactive PyBullet render of the official SO-101 CAD meshes.
15
20
  - Robot and scene selectors.
16
21
  - Joint positions and limits.
17
22
  - End-effector pose.
@@ -19,5 +24,8 @@ The viewer provides:
19
24
  - Scene objects.
20
25
  - A safe local command console for state, reset, move, and reach.
21
26
 
27
+ The recording executes a Cartesian reach in the insertion scene through the
28
+ local command console.
29
+
22
30
  The API writes the same persistent state as the CLI. The default bind is
23
31
  loopback-only. Do not use `--unsafe-public` on an untrusted network.
@@ -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.5.0"
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from jolly.challenges import evaluate
6
+ from jolly.core.errors import ConfigurationError
7
+
8
+ BENCHMARK_ID = "jolly-operator-pick-place-v5"
9
+ BENCHMARK_CHALLENGE = "drop-in-hole"
10
+ BENCHMARK_METADATA_KEYS = (
11
+ "active_challenge",
12
+ "challenge_seed",
13
+ "challenge_instance",
14
+ "active_benchmark",
15
+ "benchmark_controls",
16
+ )
17
+
18
+ CONTROL_WORKFLOW = [
19
+ "Inspect the generated peg and hole coordinates in the start result or with 'jolly state --json'.",
20
+ "Move above the peg with 'jolly reach' and an open gripper.",
21
+ "Move down until the gripper contacts the peg, then close the gripper.",
22
+ "Lift the grasped peg.",
23
+ "Move the peg horizontally above the generated hole.",
24
+ "Lower the peg into the opening and release it.",
25
+ "Run 'jolly benchmark score' to measure the final physical state.",
26
+ ]
27
+
28
+
29
+ def benchmark_control_history(
30
+ previous: dict[str, object],
31
+ *,
32
+ source: str,
33
+ command: str,
34
+ requested: dict[str, object],
35
+ result: dict[str, object],
36
+ ) -> list[object] | None:
37
+ """Return the control history with one measured explicit motion appended."""
38
+ if previous.get("active_benchmark") != BENCHMARK_ID:
39
+ return None
40
+ existing = previous.get("benchmark_controls", [])
41
+ controls = list(existing) if isinstance(existing, list) else []
42
+ controls.append(
43
+ {
44
+ "index": len(controls) + 1,
45
+ "source": source,
46
+ "command": command,
47
+ "requested": requested,
48
+ "measured": {
49
+ "tool_position": result["end_effector"]["position"],
50
+ "gripper": result["gripper"],
51
+ "held_object": result["held_object"],
52
+ "collision": result["collisions"]["collision"],
53
+ },
54
+ }
55
+ )
56
+ return controls
57
+
58
+
59
+ def score_operator_benchmark(state: dict[str, Any]) -> dict[str, object]:
60
+ """Measure an operator-controlled pick-and-place attempt without moving the robot."""
61
+ if state.get("active_benchmark") != BENCHMARK_ID:
62
+ raise ConfigurationError("No operator benchmark is active. Run 'jolly benchmark start'.")
63
+ if state.get("active_challenge") != BENCHMARK_CHALLENGE:
64
+ raise ConfigurationError("The active benchmark is not the drop-in-hole pick-and-place task.")
65
+ controls = state.get("benchmark_controls")
66
+ if not isinstance(controls, list):
67
+ raise ConfigurationError("The active benchmark has no control history. Start it again.")
68
+ evaluation = evaluate(BENCHMARK_CHALLENGE, state)
69
+ success = bool(evaluation["success"])
70
+ return {
71
+ "ok": True,
72
+ "benchmark": BENCHMARK_ID,
73
+ "engine": "JollyEngine",
74
+ "physics_backend": "PyBullet",
75
+ "mode": "operator-controlled",
76
+ "seed": evaluation["seed"],
77
+ "outcome": "PASS" if success else "FAIL",
78
+ "success": success,
79
+ "control_count": len(controls),
80
+ "controls": controls,
81
+ "instance": evaluation["instance"],
82
+ "measurements": evaluation["metrics"],
83
+ "score": None,
84
+ "score_basis": "No synthetic numeric score. PASS requires the released peg to settle inside the generated hole without collision and within the command budget.",
85
+ }
@@ -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