PhysicalAI LeKiwi Plugin
Third-party LeKiwi mobile manipulator plugin for PhysicalAI, the Python library and runtime for robot control, transport, and CLI workflows. It registers with Physical AI Studio, the application that discovers catalog plugins and provides robot setup, teleoperation, and workflow experiences. Part of the physicalai-plugins monorepo.
Features
- Concrete implementation of the
Robotprotocol — no inheritance or registration required - 6-DOF SO-ARM100 arm + 3-wheel holonomic base, in normalized or raw-ticks units
- Follower and leader (read-only) roles for teleoperation
- Bundled URDF for kinematics and gravity compensation
KeyboardTeleopandCompositeTeleopaction sources forphysicalai run
Hardware
| Class | Robot | Motors | Protocol |
|---|---|---|---|
LeKiwi |
6-DOF SO-ARM100 arm + 3-wheel holonomic base | Feetech STS3215 (via scservo_sdk) |
POSITION / VELOCITY |
Screenshots
Placeholder images — replace them with real screenshots.
Installation
uv add physicalai-lekiwi-plugin
scservo_sdk (Feetech servo serial SDK) is included as a core dependency.
No calibration JSON is bundled. You must provide your own calibration file (LeRobot format) or use uncalibrated/ticks mode.
Quick start
Basic follower (with calibration file)
import numpy as np
from physicalai.robot import connect
from physicalai_lekiwi_plugin import LeKiwiCalibration, LeKiwi
robot = LeKiwi(
port="/dev/ttyACM0",
role="follower",
calibration=LeKiwiCalibration.from_path("calibration.json"),
)
with connect(robot) as robot:
obs = robot.get_observation()
action = obs.joint_positions.copy()
robot.send_action(action)
In Studio catalog payloads, calibration_id is optional. When provided, the
plugin loads that calibration and runs in normalized units. When omitted, the
driver falls back to explicit uncalibrated ticks mode.
Calibration from a dict
cal = LeKiwiCalibration.from_dict({
"arm_shoulder_pan": {"id": 1, "drive_mode": 0, "homing_offset": 0, "range_min": 0, "range_max": 4095},
"arm_shoulder_lift": {"id": 2, "drive_mode": 0, "homing_offset": -512, "range_min": 512, "range_max": 3583},
"arm_elbow_flex": {"id": 3, "drive_mode": 0, "homing_offset": 0, "range_min": 0, "range_max": 4095},
"arm_wrist_flex": {"id": 4, "drive_mode": 1, "homing_offset": 0, "range_min": 0, "range_max": 4095},
"arm_wrist_roll": {"id": 5, "drive_mode": 0, "homing_offset": 0, "range_min": 0, "range_max": 4095},
"arm_gripper": {"id": 6, "drive_mode": 0, "homing_offset": 0, "range_min": 2048, "range_max": 3072},
"base_left_wheel": {"id": 7, "drive_mode": 0, "homing_offset": 0, "range_min": 0, "range_max": 4095},
"base_back_wheel": {"id": 8, "drive_mode": 0, "homing_offset": 0, "range_min": 0, "range_max": 4095},
"base_right_wheel": {"id": 9, "drive_mode": 0, "homing_offset": 0, "range_min": 0, "range_max": 4095},
})
robot = LeKiwi(port="/dev/ttyACM0", role="follower", calibration=cal)
Uncalibrated bringup mode
robot = LeKiwi.uncalibrated(port="/dev/ttyACM0", role="follower")
Observations and actions use raw servo ticks (0–4095). No calibration file needed.
Leader (read-only teleoperation)
robot = LeKiwi.uncalibrated(port="/dev/ttyACM0", role="leader")
with connect(robot) as robot:
while True:
obs = robot.get_observation()
print(obs.joint_positions)
Leader mode disables torque on all motors so the arm can be moved manually.
Run with the PhysicalAI CLI
The PhysicalAI CLI run
subcommand executes a RobotRuntime from a YAML config. Run the bundled
configs from the repo root:
# Keyboard drive of the base (arm holds its position)
uv run physicalai run --config packages/physicalai-lekiwi-plugin/examples/runtime/drive-keyboard.yaml
# Composite teleop: leader arm positions the arm, keyboard drives the base
uv run physicalai run --config packages/physicalai-lekiwi-plugin/examples/runtime/teleop.yaml
Press Ctrl+C to stop. Optionally cap the run with --run.duration_s=60.
Keyboard controls
| Key | Action |
|---|---|
w / s |
forward / backward |
a / d |
rotate left / right |
q / e |
strafe left / right |
space |
stop (zero the base) |
Teleop action sources
physicalai-lekiwi-plugin bundles reusable action sources:
from physicalai_lekiwi_plugin.teleop import CompositeTeleop, KeyboardTeleop
| Source | Purpose |
|---|---|
KeyboardTeleop |
WASD/QE base velocities; arm held at its observed pose |
CompositeTeleop |
Combine a leader arm with a base source into one action |
Sinusoidal motion and joint reading
uv run physicalai run --config packages/physicalai-lekiwi-plugin/examples/runtime/move-joints.yaml
uv run physicalai run --config packages/physicalai-lekiwi-plugin/examples/runtime/read-joints.yaml
URDF Model
from physicalai_lekiwi_plugin import get_urdf_path
urdf_path = get_urdf_path()
| URDF | Model | Use |
|---|---|---|
lekiwi/urdf/LeKiwi.urdf |
LeKiwi mobile manipulator | Kinematics & gravity compensation |
The URDF references original STL mesh files from the SIGRobotics-UIUC/LeKiwi project.
Kinematics Model
The base uses a three-wheel holonomic drive with wheels at 240°, 0°, and 120°:
# Body-frame velocities -> individual wheel raw speeds
wheel_raw = robot._body_to_wheel_raw(vx, vy, vtheta)
# Individual wheel raw speeds -> body-frame velocities
body_vel = robot._wheel_raw_to_body(left_raw, back_raw, right_raw)
The helper methods above are internal; prefer the Robot protocol
(get_observation / send_action) in application code.
| Parameter | Value |
|---|---|
| Wheel angles | 240°, 0°, 120° |
| Wheel radius | 0.05 m |
| Base radius | 0.125 m |
| Max raw wheel | 3000 |
Development
uv sync
uv run pytest packages/physicalai-lekiwi-plugin/tests/
Acknowledgments
The URDF model for the LeKiwi is from the SIGRobotics-UIUC/LeKiwi project.
Metadata
Release files for physicalai-lekiwi-plugin 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| physicalai_lekiwi_plugin-0.3.0.tar.gz | 22.1 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| physicalai_lekiwi_plugin-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 44.1 MB
Release files / physicalai_lekiwi_plugin-0.3.0.tar.gz
| Download URL | physicalai_lekiwi_plugin-0.3.0.tar.gz |
|---|---|
| Size | 22.1 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
eb6d805632a104c70cf8cc0144cf5e9faaa4d95b81b2df6305df545e9e88523d
|
|
BLAKE2b-256 checksum How to use checksums |
4e92a9331a2813d1c55b15d59a2178b083f6b6a1c4461a3f68f0016a3c002e48
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 4, 2026.
Transparency logRelease files / physicalai_lekiwi_plugin-0.3.0-py3-none-any.whl
| Download URL | physicalai_lekiwi_plugin-0.3.0-py3-none-any.whl |
|---|---|
| Size | 22.1 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2599ed1d0aaa85fc660a9c8f60c66140de6b990bb002d9cb50aee96ae942f23a
|
|
BLAKE2b-256 checksum How to use checksums |
3b04305c45e24f5239396fe8743b9820fc654d67cee938d4d26819b158b3d8c4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 4, 2026.
Transparency log