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.
Files changed (72) hide show
  1. so101_workshop-0.1.0/PKG-INFO +175 -0
  2. so101_workshop-0.1.0/README.md +148 -0
  3. so101_workshop-0.1.0/pyproject.toml +45 -0
  4. so101_workshop-0.1.0/setup.cfg +4 -0
  5. so101_workshop-0.1.0/src/so101_control/__init__.py +5 -0
  6. so101_workshop-0.1.0/src/so101_control/arm.py +369 -0
  7. so101_workshop-0.1.0/src/so101_control/backend.py +106 -0
  8. so101_workshop-0.1.0/src/so101_control/estop.py +15 -0
  9. so101_workshop-0.1.0/src/so101_control/feetech.py +632 -0
  10. so101_workshop-0.1.0/src/so101_control/kinematics.py +838 -0
  11. so101_workshop-0.1.0/src/so101_control/motion.py +120 -0
  12. so101_workshop-0.1.0/src/so101_control/teleop.py +308 -0
  13. so101_workshop-0.1.0/src/so101_utils/__init__.py +1 -0
  14. so101_workshop-0.1.0/src/so101_utils/assets.py +200 -0
  15. so101_workshop-0.1.0/src/so101_utils/auto_hand_eye.py +26 -0
  16. so101_workshop-0.1.0/src/so101_utils/camera.py +643 -0
  17. so101_workshop-0.1.0/src/so101_utils/click_app.py +112 -0
  18. so101_workshop-0.1.0/src/so101_utils/click_view.py +172 -0
  19. so101_workshop-0.1.0/src/so101_utils/config.py +183 -0
  20. so101_workshop-0.1.0/src/so101_utils/hand_eye_flow.py +36 -0
  21. so101_workshop-0.1.0/src/so101_utils/intrinsics.py +133 -0
  22. so101_workshop-0.1.0/src/so101_utils/keys.py +54 -0
  23. so101_workshop-0.1.0/src/so101_utils/model.py +69 -0
  24. so101_workshop-0.1.0/src/so101_utils/moves.py +103 -0
  25. so101_workshop-0.1.0/src/so101_utils/ports.py +59 -0
  26. so101_workshop-0.1.0/src/so101_utils/tags.py +98 -0
  27. so101_workshop-0.1.0/src/so101_utils/transforms.py +160 -0
  28. so101_workshop-0.1.0/src/so101_utils/views.py +477 -0
  29. so101_workshop-0.1.0/src/so101_workshop/__init__.py +1 -0
  30. so101_workshop-0.1.0/src/so101_workshop/agent.py +1373 -0
  31. so101_workshop-0.1.0/src/so101_workshop/cli.py +74 -0
  32. so101_workshop-0.1.0/src/so101_workshop/detections.py +191 -0
  33. so101_workshop-0.1.0/src/so101_workshop/hooks.py +22 -0
  34. so101_workshop-0.1.0/src/so101_workshop/server.py +328 -0
  35. so101_workshop-0.1.0/src/so101_workshop/static/index.html +2139 -0
  36. so101_workshop-0.1.0/src/so101_workshop/station.py +46 -0
  37. so101_workshop-0.1.0/src/so101_workshop/templates/camera.yaml +21 -0
  38. so101_workshop-0.1.0/src/so101_workshop/templates/camera_demo.yaml +21 -0
  39. so101_workshop-0.1.0/src/so101_workshop/templates/camera_logi.yaml +21 -0
  40. so101_workshop-0.1.0/src/so101_workshop/templates/camera_realsense.yaml +15 -0
  41. so101_workshop-0.1.0/src/so101_workshop/templates/camera_recamera.yaml +21 -0
  42. so101_workshop-0.1.0/src/so101_workshop/templates/camera_usbcam.yaml +21 -0
  43. so101_workshop-0.1.0/src/so101_workshop/templates/robot.yaml +26 -0
  44. so101_workshop-0.1.0/src/so101_workshop/templates/workshop.yaml +16 -0
  45. so101_workshop-0.1.0/src/so101_workshop/viewer.py +204 -0
  46. so101_workshop-0.1.0/src/so101_workshop.egg-info/PKG-INFO +175 -0
  47. so101_workshop-0.1.0/src/so101_workshop.egg-info/SOURCES.txt +70 -0
  48. so101_workshop-0.1.0/src/so101_workshop.egg-info/dependency_links.txt +1 -0
  49. so101_workshop-0.1.0/src/so101_workshop.egg-info/entry_points.txt +2 -0
  50. so101_workshop-0.1.0/src/so101_workshop.egg-info/requires.txt +22 -0
  51. so101_workshop-0.1.0/src/so101_workshop.egg-info/top_level.txt +3 -0
  52. so101_workshop-0.1.0/tests/test_assets.py +159 -0
  53. so101_workshop-0.1.0/tests/test_camera.py +197 -0
  54. so101_workshop-0.1.0/tests/test_click_and_pick.py +203 -0
  55. so101_workshop-0.1.0/tests/test_click_view.py +114 -0
  56. so101_workshop-0.1.0/tests/test_detect_and_pick.py +42 -0
  57. so101_workshop-0.1.0/tests/test_detections.py +73 -0
  58. so101_workshop-0.1.0/tests/test_feetech.py +146 -0
  59. so101_workshop-0.1.0/tests/test_gripper.py +106 -0
  60. so101_workshop-0.1.0/tests/test_hand_eye.py +82 -0
  61. so101_workshop-0.1.0/tests/test_intrinsics.py +64 -0
  62. so101_workshop-0.1.0/tests/test_keyboard_control.py +164 -0
  63. so101_workshop-0.1.0/tests/test_kinematics.py +442 -0
  64. so101_workshop-0.1.0/tests/test_kinematics_urdf.py +244 -0
  65. so101_workshop-0.1.0/tests/test_mock_arm.py +183 -0
  66. so101_workshop-0.1.0/tests/test_motion.py +104 -0
  67. so101_workshop-0.1.0/tests/test_station.py +24 -0
  68. so101_workshop-0.1.0/tests/test_transforms.py +45 -0
  69. so101_workshop-0.1.0/tests/test_workshop_api.py +202 -0
  70. so101_workshop-0.1.0/tests/test_workshop_features.py +647 -0
  71. so101_workshop-0.1.0/tests/test_workshop_ui.py +144 -0
  72. 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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """SO-101 arm control. Poses are metres; joint angles are degrees."""
2
+
3
+ from .arm import Pose, SO101
4
+
5
+ __all__ = ["Pose", "SO101"]