Programmatic control of an SO-101 follower arm via lerobot (joint waypoints + IK).
Project description
so101-control
A small Python package for programmatic control of an SO-101 robot arm via lerobot.
Move the arm to specific joint-angle configurations, or drive the end-effector to
a Cartesian target using inverse kinematics — no teleop or trained policy
required. Install it as a library in your own project, or use the bundled
so101-control CLI.
What's included
| Path | Purpose |
|---|---|
src/so101_control/ |
The installable so101_control package (control wrappers + CLI). |
src/so101_control/control.py |
Reusable primitives: move_to_joints, go_to_ee, home, rest. |
src/so101_control/cli.py |
The so101-control command-line entry point. |
examples/ |
Example scripts showing library usage. |
Installation
Requires Python ≥3.12 and uv (or any PEP 621-aware
installer such as pip).
From source (development)
git clone https://github.com/vi-vi-3482/so101-control
cd so101-control
uv sync
This installs the so101_control package (importable as import so101_control)
together with lerobot[kinematics,feetech] (which pulls in placo for IK) and
pins cmeel-urdfdom==4.0.1 to work around a shared-library mismatch in the
placo wheels.
Platform compatibility
CI runs on macOS and Linux. Windows is not currently supported for direct
installation: the placo/pin/coal-library chain depends on cmeel-qhull,
which has no prebuilt Windows wheel and fails to build from source on recent
CMake (its cmake_minimum_required predates 3.5, which modern CMake no longer
accepts). This is an upstream packaging issue, not specific to so101-control.
On Windows, run so101-control inside WSL
using a Linux install of lerobot until a newer upstream release ships working
Windows wheels.
As a dependency in your own project
Add it to your pyproject.toml dependencies (or let uv do it for you):
# from a local checkout
uv add /path/to/so101-control
# …or from git
uv add "git+https://github.com/vi-vi-3482/so101-control.git"
Then import it from your own code:
from so101_control import (
HOME_JOINTS,
go_to_ee,
home,
move_to_joints,
print_status,
rest,
)
Fetch the SO-101 URDF + meshes (only needed for end-effector / IK mode)
The URDF references STL meshes in an assets/ folder. Download both:
curl -L -o so101_new_calib.urdf \
https://raw.githubusercontent.com/TheRobotStudio/SO-ARM100/main/Simulation/SO101/so101_new_calib.urdf
mkdir -p assets && cd assets
for f in base_motor_holder_so101_v1 base_so101_v2 motor_holder_so101_base_v1 \
motor_holder_so101_wrist_v1 moving_jaw_so101_v1 rotation_pitch_so101_v1 \
sts3215_03a_no_horn_v1 sts3215_03a_v1 under_arm_so101_v1 upper_arm_so101_v1 \
waveshare_mounting_plate_so101_v2 wrist_roll_follower_so101_v1 \
wrist_roll_pitch_so101_v2; do
curl -sL -o "$f.stl" \
"https://raw.githubusercontent.com/TheRobotStudio/SO-ARM100/main/Simulation/SO101/assets/$f.stl"
done
cd ..
Calibrate the arm (one-time)
If you haven't already calibrated your follower, follow the commands in
LeRobot. Calibration is stored under
~/.cache/huggingface/lerobot/calibration/robots/<robot-id>/ and is located
automatically by the --robot-id you pass to the CLI.
Quick start
Once installed, the so101-control CLI is available.
Dry-run (no hardware needed — prints the planned trajectory):
so101-control --mode joint \
--target-joints shoulder_pan=0,shoulder_lift=-20,elbow_flex=60,wrist_flex=30,wrist_roll=0,gripper=50 \
--dry-run
Live joint move:
so101-control \
--port /dev/tty.usbmodem585A0076841 \
--robot-id my_awesome_follower_arm \
--mode joint \
--target-joints shoulder_pan=0,shoulder_lift=-20,elbow_flex=60,wrist_flex=30,wrist_roll=0,gripper=50
End-effector move (position-only IK by default):
so101-control \
--port /dev/tty.usbmodem585A0076841 \
--robot-id my_awesome_follower_arm \
--urdf-path /absolute/path/to/so101_new_calib.urdf \
--mode ee --target-xyz 0.2,0.0,0.15
You can also invoke the module directly without installing the console script:
uv run python -m so101_control.cli --mode home --dry-run
See the Library usage section below for the full API, library usage, safety knobs, and the joint schema.
Library usage
import time
from lerobot.model.kinematics import RobotKinematics
from lerobot.robots.so_follower import SO101Follower, SO101FollowerConfig
from so101_control import (
HOME_JOINTS,
go_to_ee,
home,
move_to_joints,
print_status,
rest,
)
robot = SO101Follower(
SO101FollowerConfig(
port="/dev/tty.usbmodem...", id="my_awesome_follower_arm"
)
)
robot.connect(calibrate=False)
print("Moving to home joint angle")
move_to_joints(robot, HOME_JOINTS, duration_s=2.0)
kin = RobotKinematics(
urdf_path="/absolute/path/to/so101_new_calib.urdf", # Must be an absolute path
target_frame_name="gripper_frame_link",
joint_names=[
"shoulder_pan",
"shoulder_lift",
"elbow_flex",
"wrist_flex",
"wrist_roll",
],
)
print_status(kin, robot)
pos = [0.3, 0.0, 0.5]
print(f"Moving to EE position {pos}")
go_to_ee(kin, robot, pos, duration_s=3.0)
time.sleep(0.5)
rest(robot)
time.sleep(0.1)
robot.disconnect()
A ready-to-edit version of the above lives at
examples/example_move_ee.py.
Primitives
The package exposes four reusable primitives (all runnable without hardware
via dry_run=True):
| Primitive | Description |
|---|---|
move_to_joints |
Smoothly interpolate to a *.pos -> value joint dict. |
go_to_ee |
Solve IK and route the result through move_to_joints. |
home |
Return the arm to HOME_JOINTS (extended, neutral). |
rest |
Return the arm to REST_JOINTS — the curled-up pose. |
Joint schema
SO-101 has 5 arm DOF + 1 gripper. The package exposes the following constants:
| Constant | Value |
|---|---|
ARM_JOINTS |
shoulder_pan, shoulder_lift, elbow_flex, wrist_flex, wrist_roll |
ALL_JOINTS |
ARM_JOINTS + [gripper] |
ACTION_KEYS |
ARM_JOINTS/ALL_JOINTS suffixed with .pos (the lerobot action keys). |
EE_FRAME |
gripper_frame_link — the IK target frame in the SO-ARM100 URDF. |
HOME_JOINTS |
Neutral extended pose (all arm joints at 0°, gripper at 50). |
REST_JOINTS |
Curled-up pose; safe for storage / power-off. |
Safety knobs
--max-relative-target(deg, CLI) /max_relative_target(lerobot config): clips per-step joint jumps insidesend_action. Strongly recommended on hardware.duration_s/fps: control the interpolation rate. Slower = gentler.orientation_weight(default0.0): IK orientation weight.0.0means position-only; pass--target-rpyto add a soft orientation constraint.position_weight(default1.0): IK position weight.
Additional Notes
The URDF file path must be an absolute path from the system root, otherwise it
fails to find the assets/ folder containing the mesh STLs.
Robot Coordinate system
Orientation facing the robot. The EE is pointed forward in the home position.
| Axis | Orientation |
|---|---|
| X | Forward |
| Y | Right |
| Z | Up |
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file so101_control-0.1.1.tar.gz.
File metadata
- Download URL: so101_control-0.1.1.tar.gz
- Upload date:
- Size: 26.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dad3748702974549ca6e394327744e86ceda706c37268967b398bed2b53e6f7e
|
|
| MD5 |
2f0dddcde20184b0ed6d236a80717305
|
|
| BLAKE2b-256 |
b495f989bb70a9960cdfd576bae210a41c9eccab6fe98a47f55a0d1fd65994ce
|
Provenance
The following attestation bundles were made for so101_control-0.1.1.tar.gz:
Publisher:
publish.yml on vi-vi-3482/so101-control
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
so101_control-0.1.1.tar.gz -
Subject digest:
dad3748702974549ca6e394327744e86ceda706c37268967b398bed2b53e6f7e - Sigstore transparency entry: 2050304301
- Sigstore integration time:
-
Permalink:
vi-vi-3482/so101-control@5393b992af792e89d5cbac68be62bf9bce600b44 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/vi-vi-3482
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5393b992af792e89d5cbac68be62bf9bce600b44 -
Trigger Event:
push
-
Statement type:
File details
Details for the file so101_control-0.1.1-py3-none-any.whl.
File metadata
- Download URL: so101_control-0.1.1-py3-none-any.whl
- Upload date:
- Size: 26.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d39828ded8b13a808c5a30d7f8b4b477037e6dc310b73d62fc5f238af408665d
|
|
| MD5 |
a47d92e804f23a05678143ec3090f0a6
|
|
| BLAKE2b-256 |
9c66766d8b97a9ceaf1a034ff46827cdf4759e8215a4dafbda3a1973f780ef9b
|
Provenance
The following attestation bundles were made for so101_control-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on vi-vi-3482/so101-control
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
so101_control-0.1.1-py3-none-any.whl -
Subject digest:
d39828ded8b13a808c5a30d7f8b4b477037e6dc310b73d62fc5f238af408665d - Sigstore transparency entry: 2050304659
- Sigstore integration time:
-
Permalink:
vi-vi-3482/so101-control@5393b992af792e89d5cbac68be62bf9bce600b44 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/vi-vi-3482
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5393b992af792e89d5cbac68be62bf9bce600b44 -
Trigger Event:
push
-
Statement type: