so101-workshop 0.1.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.
- so101_workshop-0.1.0/PKG-INFO +175 -0
- so101_workshop-0.1.0/README.md +148 -0
- so101_workshop-0.1.0/pyproject.toml +45 -0
- so101_workshop-0.1.0/setup.cfg +4 -0
- so101_workshop-0.1.0/src/so101_control/__init__.py +5 -0
- so101_workshop-0.1.0/src/so101_control/arm.py +369 -0
- so101_workshop-0.1.0/src/so101_control/backend.py +106 -0
- so101_workshop-0.1.0/src/so101_control/estop.py +15 -0
- so101_workshop-0.1.0/src/so101_control/feetech.py +632 -0
- so101_workshop-0.1.0/src/so101_control/kinematics.py +838 -0
- so101_workshop-0.1.0/src/so101_control/motion.py +120 -0
- so101_workshop-0.1.0/src/so101_control/teleop.py +308 -0
- so101_workshop-0.1.0/src/so101_utils/__init__.py +1 -0
- so101_workshop-0.1.0/src/so101_utils/assets.py +200 -0
- so101_workshop-0.1.0/src/so101_utils/auto_hand_eye.py +26 -0
- so101_workshop-0.1.0/src/so101_utils/camera.py +643 -0
- so101_workshop-0.1.0/src/so101_utils/click_app.py +112 -0
- so101_workshop-0.1.0/src/so101_utils/click_view.py +172 -0
- so101_workshop-0.1.0/src/so101_utils/config.py +183 -0
- so101_workshop-0.1.0/src/so101_utils/hand_eye_flow.py +36 -0
- so101_workshop-0.1.0/src/so101_utils/intrinsics.py +133 -0
- so101_workshop-0.1.0/src/so101_utils/keys.py +54 -0
- so101_workshop-0.1.0/src/so101_utils/model.py +69 -0
- so101_workshop-0.1.0/src/so101_utils/moves.py +103 -0
- so101_workshop-0.1.0/src/so101_utils/ports.py +59 -0
- so101_workshop-0.1.0/src/so101_utils/tags.py +98 -0
- so101_workshop-0.1.0/src/so101_utils/transforms.py +160 -0
- so101_workshop-0.1.0/src/so101_utils/views.py +477 -0
- so101_workshop-0.1.0/src/so101_workshop/__init__.py +1 -0
- so101_workshop-0.1.0/src/so101_workshop/agent.py +1373 -0
- so101_workshop-0.1.0/src/so101_workshop/cli.py +74 -0
- so101_workshop-0.1.0/src/so101_workshop/detections.py +191 -0
- so101_workshop-0.1.0/src/so101_workshop/hooks.py +22 -0
- so101_workshop-0.1.0/src/so101_workshop/server.py +328 -0
- so101_workshop-0.1.0/src/so101_workshop/static/index.html +2139 -0
- so101_workshop-0.1.0/src/so101_workshop/station.py +46 -0
- so101_workshop-0.1.0/src/so101_workshop/templates/camera.yaml +21 -0
- so101_workshop-0.1.0/src/so101_workshop/templates/camera_demo.yaml +21 -0
- so101_workshop-0.1.0/src/so101_workshop/templates/camera_logi.yaml +21 -0
- so101_workshop-0.1.0/src/so101_workshop/templates/camera_realsense.yaml +15 -0
- so101_workshop-0.1.0/src/so101_workshop/templates/camera_recamera.yaml +21 -0
- so101_workshop-0.1.0/src/so101_workshop/templates/camera_usbcam.yaml +21 -0
- so101_workshop-0.1.0/src/so101_workshop/templates/robot.yaml +26 -0
- so101_workshop-0.1.0/src/so101_workshop/templates/workshop.yaml +16 -0
- so101_workshop-0.1.0/src/so101_workshop/viewer.py +204 -0
- so101_workshop-0.1.0/src/so101_workshop.egg-info/PKG-INFO +175 -0
- so101_workshop-0.1.0/src/so101_workshop.egg-info/SOURCES.txt +70 -0
- so101_workshop-0.1.0/src/so101_workshop.egg-info/dependency_links.txt +1 -0
- so101_workshop-0.1.0/src/so101_workshop.egg-info/entry_points.txt +2 -0
- so101_workshop-0.1.0/src/so101_workshop.egg-info/requires.txt +22 -0
- so101_workshop-0.1.0/src/so101_workshop.egg-info/top_level.txt +3 -0
- so101_workshop-0.1.0/tests/test_assets.py +159 -0
- so101_workshop-0.1.0/tests/test_camera.py +197 -0
- so101_workshop-0.1.0/tests/test_click_and_pick.py +203 -0
- so101_workshop-0.1.0/tests/test_click_view.py +114 -0
- so101_workshop-0.1.0/tests/test_detect_and_pick.py +42 -0
- so101_workshop-0.1.0/tests/test_detections.py +73 -0
- so101_workshop-0.1.0/tests/test_feetech.py +146 -0
- so101_workshop-0.1.0/tests/test_gripper.py +106 -0
- so101_workshop-0.1.0/tests/test_hand_eye.py +82 -0
- so101_workshop-0.1.0/tests/test_intrinsics.py +64 -0
- so101_workshop-0.1.0/tests/test_keyboard_control.py +164 -0
- so101_workshop-0.1.0/tests/test_kinematics.py +442 -0
- so101_workshop-0.1.0/tests/test_kinematics_urdf.py +244 -0
- so101_workshop-0.1.0/tests/test_mock_arm.py +183 -0
- so101_workshop-0.1.0/tests/test_motion.py +104 -0
- so101_workshop-0.1.0/tests/test_station.py +24 -0
- so101_workshop-0.1.0/tests/test_transforms.py +45 -0
- so101_workshop-0.1.0/tests/test_workshop_api.py +202 -0
- so101_workshop-0.1.0/tests/test_workshop_features.py +647 -0
- so101_workshop-0.1.0/tests/test_workshop_ui.py +144 -0
- so101_workshop-0.1.0/tests/test_workshop_viewer.py +170 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: so101-workshop
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SO-101 workshop: local arm control and a browser UI
|
|
5
|
+
Author-email: iltlo <pypi@ianlo.net>
|
|
6
|
+
Requires-Python: <3.14,>=3.12
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: numpy<2.3,>=2
|
|
9
|
+
Requires-Dist: scipy>=1.14
|
|
10
|
+
Requires-Dist: PyYAML>=6
|
|
11
|
+
Requires-Dist: opencv-python-headless<4.14,>=4.11
|
|
12
|
+
Requires-Dist: fastapi>=0.115
|
|
13
|
+
Requires-Dist: uvicorn>=0.32
|
|
14
|
+
Requires-Dist: pyserial<4,>=3.5
|
|
15
|
+
Requires-Dist: feetech-servo-sdk>=1.0.0
|
|
16
|
+
Requires-Dist: paho-mqtt>=2.1
|
|
17
|
+
Requires-Dist: pupil-apriltags>=1.0.4
|
|
18
|
+
Requires-Dist: websockets>=13
|
|
19
|
+
Requires-Dist: rerun-sdk<0.34,>=0.33
|
|
20
|
+
Provides-Extra: vision
|
|
21
|
+
Requires-Dist: pyrealsense2; extra == "vision"
|
|
22
|
+
Provides-Extra: twin
|
|
23
|
+
Requires-Dist: pynput<1.9,>=1.7.8; extra == "twin"
|
|
24
|
+
Provides-Extra: test
|
|
25
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
26
|
+
Requires-Dist: httpx>=0.27; extra == "test"
|
|
27
|
+
|
|
28
|
+
# SO-101 workshop
|
|
29
|
+
|
|
30
|
+
Local control for an SO-101 follower: a browser UI for connection, calibration, teleop, and picking, plus a small Python API. Distances are metres. Joint angles are degrees.
|
|
31
|
+
|
|
32
|
+
## Run
|
|
33
|
+
|
|
34
|
+
Install [uv](https://docs.astral.sh/uv/), then start one local process. It prints `http://127.0.0.1:8020` and listens only on this machine. An installed run keeps its config and downloaded URDF meshes in `~/.so101-workshop/`. Override that directory with `--config-dir` or `SO101_WORKSHOP_HOME`.
|
|
35
|
+
|
|
36
|
+
macOS and Linux:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
40
|
+
uvx so101-workshop@latest run
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Windows PowerShell. Use native Windows so the reCamera USB gadget at `192.168.42.1` is visible:
|
|
44
|
+
|
|
45
|
+
```powershell
|
|
46
|
+
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
|
47
|
+
uvx so101-workshop@latest run
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
From a checkout of this repository:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
uvx --from . so101-workshop run
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`--mock` runs the page without an arm. `--no-viewer` skips the Rerun panel.
|
|
57
|
+
|
|
58
|
+
The page is split into **Hardware** (arm, camera, USB port, motor calibration), **Hand-eye** and **Intrinsics**, and **Tasks** (teleop, click and go, click and pick, detect and pick). Motor calibration stays disabled until you press **Start motor calibration**. The E-stop turns torque off.
|
|
59
|
+
|
|
60
|
+
- Linux: add your user to the `dialout` group. The arm is usually `/dev/ttyACM0`.
|
|
61
|
+
- Windows: serial ports are `COM*`.
|
|
62
|
+
- macOS: the arm is `/dev/cu.usbmodem*`.
|
|
63
|
+
|
|
64
|
+
Requires Python 3.12 or 3.13. On macOS, AprilTags provides a wheel for Apple silicon only.
|
|
65
|
+
|
|
66
|
+
## Safety
|
|
67
|
+
|
|
68
|
+
Clear the table before any live motion. Joint speed defaults to 90 deg/s, which is the maximum. Change it with `arm.set_speed(...)`. Torque off lets the arm fall: support it, and keep a power cutoff within reach. Ctrl-C and the keyboard are not emergency stops. There is no collision checker.
|
|
69
|
+
|
|
70
|
+
## First use
|
|
71
|
+
|
|
72
|
+
Plug in the USB adapter. Motors can stay off. Under **Arm connection**, find the port, then calibrate the motors once before any live move. Torque turns off, so support the arm. Pose every joint at the middle of its travel and confirm. Then move every joint except wrist roll through its whole travel, including the gripper, and confirm again. Wrist roll keeps a full turn. That writes `motors.json`. Later connects load it and update a servo whose midpoint or range differs. 0° is the calibrated midpoint.
|
|
73
|
+
|
|
74
|
+
Connect the arm and the camera. The default camera is the reCamera at `ws://192.168.42.1:8090/ws/boxes`.
|
|
75
|
+
|
|
76
|
+
## Calibration
|
|
77
|
+
|
|
78
|
+
**Intrinsics.** The webcam and the reCamera do not report fx, fy, or distortion. The board has 8 × 11 inner corners (9 × 12 squares). Hold it in view, move it between captures (tilt it, and use different parts of the image), and take at least ten views. **Capture chessboard** is green only while the board is in view.
|
|
79
|
+
|
|
80
|
+
**Hand-eye.** The camera stays fixed. The tag is tag36h11, id 0, 40 mm, mounted on the gripper. That mount is the `tag:` section of `robot.yaml`. With the arm and camera connected, torque on, and the tag in view, **Run auto calibration** moves through the poses, keeps the samples that see the tag, and saves `hand_eye.yaml`. **Stop** and the E-stop abort the tour. Clear the table first, because the arm moves by itself. Redo this if you move the camera. The hand-eye view shows the tag's XYZ in millimetres and RPY in degrees while the tag is detected.
|
|
81
|
+
|
|
82
|
+
## Tasks
|
|
83
|
+
|
|
84
|
+
**Teleop** uses the keyboard or the on-screen keypad. WASD or the arrows move x and y. Q/E move z and keep the current orientation. Z/C change base roll, F/V change base pitch, R rests, H homes, and space toggles the gripper. Keys follow the Teleop tab. **Key teleop** in the top bar keeps them on across tabs.
|
|
85
|
+
|
|
86
|
+
**Click and go** treats a click as a ray onto the base plane z = 0, then moves the gripper there, 20 mm above the plane.
|
|
87
|
+
|
|
88
|
+
**Click and pick** starts from rest with the gripper open, picks at the click, closes, returns to rest, and opens.
|
|
89
|
+
|
|
90
|
+
**Detect and pick** uses the on-device YOLO boxes. Click inside a box and the arm picks that box's centre. Overlapping boxes: the smallest one wins. A click outside every box does not move the arm.
|
|
91
|
+
|
|
92
|
+
Depth picking is not included.
|
|
93
|
+
|
|
94
|
+
## Config
|
|
95
|
+
|
|
96
|
+
Files live in the station directory (`config/` in a checkout, `~/.so101-workshop/config/` when installed).
|
|
97
|
+
|
|
98
|
+
| File | What you edit |
|
|
99
|
+
| --- | --- |
|
|
100
|
+
| `robot.yaml` | port, arm id, joint limits, joint offsets, tag family/id/size and mount |
|
|
101
|
+
| `camera*.yaml` | backend, device, url, resolution, fps, intrinsics |
|
|
102
|
+
| `workshop.yaml` | feature flags, active camera profile, detection websocket and MQTT |
|
|
103
|
+
| `hand_eye.yaml` | camera pose in the base frame, written by calibration |
|
|
104
|
+
| `motors.json` | servo midpoint and range, written by motor calibration |
|
|
105
|
+
|
|
106
|
+
Switch camera under **Hardware → Camera connection**. Intrinsics belong to that profile, so calibrate the camera you will use. For a webcam, `device` is the camera index: `/dev/videoN` on Linux, DirectShow on Windows, and AVFoundation on macOS.
|
|
107
|
+
|
|
108
|
+
| Profile | Backend | Notes |
|
|
109
|
+
| --- | --- | --- |
|
|
110
|
+
| `camera_recamera.yaml` | `recamera` | Default. `ws://192.168.42.1:8090/ws/boxes`, 1280×720 |
|
|
111
|
+
| `camera_logi.yaml` | `webcamera` | USB webcam, index 0 |
|
|
112
|
+
| `camera_usbcam.yaml` | `webcamera` | Second USB webcam. Set `device` if that index is not this machine's camera |
|
|
113
|
+
|
|
114
|
+
RealSense needs the vision extra: `uvx --with pyrealsense2 so101-workshop run`.
|
|
115
|
+
|
|
116
|
+
`joint_limits_deg` and `joint_offsets_deg` in `robot.yaml` are this arm's limits and small zero corrections. Edit the offsets only if you re-measure them.
|
|
117
|
+
|
|
118
|
+
## Scripts
|
|
119
|
+
|
|
120
|
+
From a checkout, the same work is available as scripts. `--mock` draws the arm in Rerun and does not open the serial port. `--live` uses the arm. Clear the table first.
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
uv sync --locked --all-extras
|
|
124
|
+
uv run python scripts/fetch_so101_assets.py
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The asset script downloads the SO-101 URDF and its meshes once.
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
uv run python scripts/check_port.py
|
|
131
|
+
uv run python scripts/calibrate_motors.py
|
|
132
|
+
uv run python scripts/twin.py --mock
|
|
133
|
+
uv run python scripts/twin.py --live
|
|
134
|
+
uv run python scripts/get_pose.py --live
|
|
135
|
+
uv run python scripts/arm_move.py --live
|
|
136
|
+
uv run python scripts/gripper.py --live
|
|
137
|
+
uv run python scripts/joint_jog.py --mock
|
|
138
|
+
uv run python scripts/keyboard_control.py --live
|
|
139
|
+
uv run python scripts/camera_view.py
|
|
140
|
+
uv run python calibration/apriltag.py
|
|
141
|
+
uv run python calibration/intrinsics.py
|
|
142
|
+
uv run python calibration/hand_eye.py
|
|
143
|
+
uv run python calibration/auto_hand_eye.py --live
|
|
144
|
+
uv run python application/click_and_go.py --live
|
|
145
|
+
uv run python application/click_and_pick.py --live
|
|
146
|
+
uv run python application/detect_and_pick.py --live
|
|
147
|
+
uv run python application/pick_and_place.py --live
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`check_port.py` asks you to unplug the adapter and writes `port` in `config/robot.yaml`. `twin.py --live` only reads joint feedback; `--relax` lets `T` turn torque off while you pose the arm. `arm_move.py` homes (every arm joint to 0°) and visits three points; change `POINTS` in that file for different ones. Gripper percent is 0 closed and 100 fully open. `joint_jog.py` steps one joint by at most 2°: type `shoulder_pan 1`, then `q`.
|
|
151
|
+
|
|
152
|
+
Keyboard teleop adds P to print the pose, +/- to change the step, and Esc as E-stop. Translation keys keep the current orientation.
|
|
153
|
+
|
|
154
|
+
`camera_view.py` shows the live picture in Rerun. Q quits. `calibration/intrinsics.py` searches every frame for the chessboard, so that view updates more slowly. **Space** captures the current view, **Enter** solves and writes `config/camera.yaml`, **Q** quits.
|
|
155
|
+
|
|
156
|
+
`calibration/apriltag.py` is the camera only. Ctrl-C stops. `calibration/hand_eye.py` records settled poses: **Enter** samples, **C** solves and writes `config/hand_eye.yaml` after four or more samples, **T** turns torque off (support the arm first), **Q** quits without saving. `calibration/auto_hand_eye.py` drives the poses in `calibration/poses.md`.
|
|
157
|
+
|
|
158
|
+
`click_and_go.py --mock` still needs the camera, so the image and the ray are real. Only the arm is simulated. The click is on the printed browser page. Edit `PICK` and `PLACE` in `application/pick_and_place.py` before a live run. They are metres in the base frame.
|
|
159
|
+
|
|
160
|
+
## Using the arm from Python
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from so101_control import SO101
|
|
164
|
+
|
|
165
|
+
with SO101.connect(mock=True) as arm:
|
|
166
|
+
arm.home()
|
|
167
|
+
arm.rest()
|
|
168
|
+
arm.set_speed(60)
|
|
169
|
+
arm.move_to(0.20, 0.00, 0.10)
|
|
170
|
+
print(arm.get_pose())
|
|
171
|
+
arm.grip()
|
|
172
|
+
arm.release()
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`SO101.connect(mock=False)` opens the follower on the port in `robot.yaml`.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# SO-101 workshop
|
|
2
|
+
|
|
3
|
+
Local control for an SO-101 follower: a browser UI for connection, calibration, teleop, and picking, plus a small Python API. Distances are metres. Joint angles are degrees.
|
|
4
|
+
|
|
5
|
+
## Run
|
|
6
|
+
|
|
7
|
+
Install [uv](https://docs.astral.sh/uv/), then start one local process. It prints `http://127.0.0.1:8020` and listens only on this machine. An installed run keeps its config and downloaded URDF meshes in `~/.so101-workshop/`. Override that directory with `--config-dir` or `SO101_WORKSHOP_HOME`.
|
|
8
|
+
|
|
9
|
+
macOS and Linux:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
13
|
+
uvx so101-workshop@latest run
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Windows PowerShell. Use native Windows so the reCamera USB gadget at `192.168.42.1` is visible:
|
|
17
|
+
|
|
18
|
+
```powershell
|
|
19
|
+
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
|
20
|
+
uvx so101-workshop@latest run
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
From a checkout of this repository:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
uvx --from . so101-workshop run
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`--mock` runs the page without an arm. `--no-viewer` skips the Rerun panel.
|
|
30
|
+
|
|
31
|
+
The page is split into **Hardware** (arm, camera, USB port, motor calibration), **Hand-eye** and **Intrinsics**, and **Tasks** (teleop, click and go, click and pick, detect and pick). Motor calibration stays disabled until you press **Start motor calibration**. The E-stop turns torque off.
|
|
32
|
+
|
|
33
|
+
- Linux: add your user to the `dialout` group. The arm is usually `/dev/ttyACM0`.
|
|
34
|
+
- Windows: serial ports are `COM*`.
|
|
35
|
+
- macOS: the arm is `/dev/cu.usbmodem*`.
|
|
36
|
+
|
|
37
|
+
Requires Python 3.12 or 3.13. On macOS, AprilTags provides a wheel for Apple silicon only.
|
|
38
|
+
|
|
39
|
+
## Safety
|
|
40
|
+
|
|
41
|
+
Clear the table before any live motion. Joint speed defaults to 90 deg/s, which is the maximum. Change it with `arm.set_speed(...)`. Torque off lets the arm fall: support it, and keep a power cutoff within reach. Ctrl-C and the keyboard are not emergency stops. There is no collision checker.
|
|
42
|
+
|
|
43
|
+
## First use
|
|
44
|
+
|
|
45
|
+
Plug in the USB adapter. Motors can stay off. Under **Arm connection**, find the port, then calibrate the motors once before any live move. Torque turns off, so support the arm. Pose every joint at the middle of its travel and confirm. Then move every joint except wrist roll through its whole travel, including the gripper, and confirm again. Wrist roll keeps a full turn. That writes `motors.json`. Later connects load it and update a servo whose midpoint or range differs. 0° is the calibrated midpoint.
|
|
46
|
+
|
|
47
|
+
Connect the arm and the camera. The default camera is the reCamera at `ws://192.168.42.1:8090/ws/boxes`.
|
|
48
|
+
|
|
49
|
+
## Calibration
|
|
50
|
+
|
|
51
|
+
**Intrinsics.** The webcam and the reCamera do not report fx, fy, or distortion. The board has 8 × 11 inner corners (9 × 12 squares). Hold it in view, move it between captures (tilt it, and use different parts of the image), and take at least ten views. **Capture chessboard** is green only while the board is in view.
|
|
52
|
+
|
|
53
|
+
**Hand-eye.** The camera stays fixed. The tag is tag36h11, id 0, 40 mm, mounted on the gripper. That mount is the `tag:` section of `robot.yaml`. With the arm and camera connected, torque on, and the tag in view, **Run auto calibration** moves through the poses, keeps the samples that see the tag, and saves `hand_eye.yaml`. **Stop** and the E-stop abort the tour. Clear the table first, because the arm moves by itself. Redo this if you move the camera. The hand-eye view shows the tag's XYZ in millimetres and RPY in degrees while the tag is detected.
|
|
54
|
+
|
|
55
|
+
## Tasks
|
|
56
|
+
|
|
57
|
+
**Teleop** uses the keyboard or the on-screen keypad. WASD or the arrows move x and y. Q/E move z and keep the current orientation. Z/C change base roll, F/V change base pitch, R rests, H homes, and space toggles the gripper. Keys follow the Teleop tab. **Key teleop** in the top bar keeps them on across tabs.
|
|
58
|
+
|
|
59
|
+
**Click and go** treats a click as a ray onto the base plane z = 0, then moves the gripper there, 20 mm above the plane.
|
|
60
|
+
|
|
61
|
+
**Click and pick** starts from rest with the gripper open, picks at the click, closes, returns to rest, and opens.
|
|
62
|
+
|
|
63
|
+
**Detect and pick** uses the on-device YOLO boxes. Click inside a box and the arm picks that box's centre. Overlapping boxes: the smallest one wins. A click outside every box does not move the arm.
|
|
64
|
+
|
|
65
|
+
Depth picking is not included.
|
|
66
|
+
|
|
67
|
+
## Config
|
|
68
|
+
|
|
69
|
+
Files live in the station directory (`config/` in a checkout, `~/.so101-workshop/config/` when installed).
|
|
70
|
+
|
|
71
|
+
| File | What you edit |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| `robot.yaml` | port, arm id, joint limits, joint offsets, tag family/id/size and mount |
|
|
74
|
+
| `camera*.yaml` | backend, device, url, resolution, fps, intrinsics |
|
|
75
|
+
| `workshop.yaml` | feature flags, active camera profile, detection websocket and MQTT |
|
|
76
|
+
| `hand_eye.yaml` | camera pose in the base frame, written by calibration |
|
|
77
|
+
| `motors.json` | servo midpoint and range, written by motor calibration |
|
|
78
|
+
|
|
79
|
+
Switch camera under **Hardware → Camera connection**. Intrinsics belong to that profile, so calibrate the camera you will use. For a webcam, `device` is the camera index: `/dev/videoN` on Linux, DirectShow on Windows, and AVFoundation on macOS.
|
|
80
|
+
|
|
81
|
+
| Profile | Backend | Notes |
|
|
82
|
+
| --- | --- | --- |
|
|
83
|
+
| `camera_recamera.yaml` | `recamera` | Default. `ws://192.168.42.1:8090/ws/boxes`, 1280×720 |
|
|
84
|
+
| `camera_logi.yaml` | `webcamera` | USB webcam, index 0 |
|
|
85
|
+
| `camera_usbcam.yaml` | `webcamera` | Second USB webcam. Set `device` if that index is not this machine's camera |
|
|
86
|
+
|
|
87
|
+
RealSense needs the vision extra: `uvx --with pyrealsense2 so101-workshop run`.
|
|
88
|
+
|
|
89
|
+
`joint_limits_deg` and `joint_offsets_deg` in `robot.yaml` are this arm's limits and small zero corrections. Edit the offsets only if you re-measure them.
|
|
90
|
+
|
|
91
|
+
## Scripts
|
|
92
|
+
|
|
93
|
+
From a checkout, the same work is available as scripts. `--mock` draws the arm in Rerun and does not open the serial port. `--live` uses the arm. Clear the table first.
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
uv sync --locked --all-extras
|
|
97
|
+
uv run python scripts/fetch_so101_assets.py
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The asset script downloads the SO-101 URDF and its meshes once.
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
uv run python scripts/check_port.py
|
|
104
|
+
uv run python scripts/calibrate_motors.py
|
|
105
|
+
uv run python scripts/twin.py --mock
|
|
106
|
+
uv run python scripts/twin.py --live
|
|
107
|
+
uv run python scripts/get_pose.py --live
|
|
108
|
+
uv run python scripts/arm_move.py --live
|
|
109
|
+
uv run python scripts/gripper.py --live
|
|
110
|
+
uv run python scripts/joint_jog.py --mock
|
|
111
|
+
uv run python scripts/keyboard_control.py --live
|
|
112
|
+
uv run python scripts/camera_view.py
|
|
113
|
+
uv run python calibration/apriltag.py
|
|
114
|
+
uv run python calibration/intrinsics.py
|
|
115
|
+
uv run python calibration/hand_eye.py
|
|
116
|
+
uv run python calibration/auto_hand_eye.py --live
|
|
117
|
+
uv run python application/click_and_go.py --live
|
|
118
|
+
uv run python application/click_and_pick.py --live
|
|
119
|
+
uv run python application/detect_and_pick.py --live
|
|
120
|
+
uv run python application/pick_and_place.py --live
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`check_port.py` asks you to unplug the adapter and writes `port` in `config/robot.yaml`. `twin.py --live` only reads joint feedback; `--relax` lets `T` turn torque off while you pose the arm. `arm_move.py` homes (every arm joint to 0°) and visits three points; change `POINTS` in that file for different ones. Gripper percent is 0 closed and 100 fully open. `joint_jog.py` steps one joint by at most 2°: type `shoulder_pan 1`, then `q`.
|
|
124
|
+
|
|
125
|
+
Keyboard teleop adds P to print the pose, +/- to change the step, and Esc as E-stop. Translation keys keep the current orientation.
|
|
126
|
+
|
|
127
|
+
`camera_view.py` shows the live picture in Rerun. Q quits. `calibration/intrinsics.py` searches every frame for the chessboard, so that view updates more slowly. **Space** captures the current view, **Enter** solves and writes `config/camera.yaml`, **Q** quits.
|
|
128
|
+
|
|
129
|
+
`calibration/apriltag.py` is the camera only. Ctrl-C stops. `calibration/hand_eye.py` records settled poses: **Enter** samples, **C** solves and writes `config/hand_eye.yaml` after four or more samples, **T** turns torque off (support the arm first), **Q** quits without saving. `calibration/auto_hand_eye.py` drives the poses in `calibration/poses.md`.
|
|
130
|
+
|
|
131
|
+
`click_and_go.py --mock` still needs the camera, so the image and the ray are real. Only the arm is simulated. The click is on the printed browser page. Edit `PICK` and `PLACE` in `application/pick_and_place.py` before a live run. They are metres in the base frame.
|
|
132
|
+
|
|
133
|
+
## Using the arm from Python
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from so101_control import SO101
|
|
137
|
+
|
|
138
|
+
with SO101.connect(mock=True) as arm:
|
|
139
|
+
arm.home()
|
|
140
|
+
arm.rest()
|
|
141
|
+
arm.set_speed(60)
|
|
142
|
+
arm.move_to(0.20, 0.00, 0.10)
|
|
143
|
+
print(arm.get_pose())
|
|
144
|
+
arm.grip()
|
|
145
|
+
arm.release()
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`SO101.connect(mock=False)` opens the follower on the port in `robot.yaml`.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "so101-workshop"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "SO-101 workshop: local arm control and a browser UI"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{ name = "iltlo", email = "pypi@ianlo.net" }]
|
|
11
|
+
requires-python = ">=3.12,<3.14"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"numpy>=2,<2.3",
|
|
14
|
+
"scipy>=1.14",
|
|
15
|
+
"PyYAML>=6",
|
|
16
|
+
"opencv-python-headless>=4.11,<4.14",
|
|
17
|
+
"fastapi>=0.115",
|
|
18
|
+
"uvicorn>=0.32",
|
|
19
|
+
"pyserial>=3.5,<4",
|
|
20
|
+
"feetech-servo-sdk>=1.0.0",
|
|
21
|
+
"paho-mqtt>=2.1",
|
|
22
|
+
"pupil-apriltags>=1.0.4",
|
|
23
|
+
"websockets>=13",
|
|
24
|
+
"rerun-sdk>=0.33,<0.34",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
so101-workshop = "so101_workshop.cli:main"
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
vision = ["pyrealsense2"]
|
|
32
|
+
twin = ["pynput>=1.7.8,<1.9"]
|
|
33
|
+
test = ["pytest>=8", "httpx>=0.27"]
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
where = ["src"]
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.package-data]
|
|
39
|
+
so101_workshop = ["static/*", "templates/*"]
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
testpaths = ["tests"]
|
|
43
|
+
|
|
44
|
+
[tool.uv]
|
|
45
|
+
package = true
|