Programming SO-101 and LeKiwi with RoboBuddy Python
This guide is the maintainable SolU reference for programming the SO-101 Follower and the LeKiwi mobile robot from RoboBuddy Python. The learner-facing browser version is python-docs.html.
RoboBuddy Python uses the robot selected on python.html. A program builds a finite list of validated commands for that one active robot; after Python finishes, RoboBuddy's runner executes the list. Student code does not import LeRobot, open a serial port, call the browser DOM, or access hardware directly.
Quick start
- Open
python.html. - Select SO-101 Follower or LeKiwi mobile robot in the Robot selector.
- Leave execution in simulation while learning and validating poses.
- Use the canonical
robot.*API and the six manifest joint ids in this guide. - Use one
robot.move_joints({...})call when several joint targets should be sent as one pose command.
robot.move_joints({
"shoulder_pan": 5,
"elbow_flex": 10,
}, speed=30)
robot.wait(0.5)
What “several servos at once” means
RoboBuddy supports a multi-joint pose for one active robot. Put all intended targets in one dictionary and call robot.move_joints(...) once.
- Every joint number is an absolute end position in the active manifest coordinate system, not an amount to add or subtract.
- The included targets are sent together as one RoboBuddy command.
- The joints are not guaranteed to arrive together or reach their targets at the same time.
- Several sequential
robot.move_joint(...)calls remain several commands; dictionary key order is not a timing contract. robot.wait(...)is an elapsed-time delay, not measured “target reached” feedback.- A finite Python pose is not equivalent to SolU's separate continuous Virtual Leader or Phone Leader control sessions.
- One Python run controls one active robot pack. It cannot control
so101_followerandlekiwi_simconcurrently, create two robot instances, or switch robots during the run.
Movement methods compared
| Control method | What it sends | Timing meaning | Intended use |
|---|---|---|---|
robot.move_joint(joint, value, speed=50) |
One target for one joint | One finite command | Simple single-joint changes |
robot.move_joints({joint: value, ...}, speed=50) |
A subset or all six targets in one pose command | Sent together; synchronized arrival is not guaranteed | Coordinated one-shot poses |
| Virtual Leader | Continuously refreshed complete pose frames | Separate live session with ownership, freshness, hold, and feedback rules | Leader-style continuous control |
| Phone Leader | Continuously refreshed phone-tracked requests resolved by the bridge | Separate bridge-owned Cartesian session with its own workspace, hold, and fault rules | Supervised phone-led control, not a Python pose |
Partial pose
Only named joints change. Omitted joints retain their current state or target. Each supplied value is still an absolute target.
robot.move_joints({
"shoulder_pan": 10,
"wrist_roll": 15,
}, speed=40)
Complete one-shot pose frame
robot.get_joints() returns the interpreter's planned state. A connected SO-101 run starts from fresh measured bridge state; simulation starts from the current simulated state. Later calls reflect commands already planned in the same run. It is not a blocking live sensor read.
pose = robot.get_joints()
pose["shoulder_pan"] = 10
pose["elbow_flex"] = 25
robot.move_joints(pose, speed=40)
Shared robot API
RoboBuddy injects robot; do not import it.
| API | Meaning |
|---|---|
robot.home() |
Request the active manifest's Home pose. SO-101 hardware still requires a compatible verified Home pose and an armed session. |
robot.stop() / robot.emergency_stop() |
Add a stop command. The page's red STOP control remains the immediate operator stop path. |
robot.wait(seconds) |
Add a delay from 0 through 30 seconds. This does not measure joint settling. |
robot.move_joint(joint, value, speed=50) |
Build one absolute joint-target command using an id or label from the active manifest. |
robot.move_joints({joint: value, ...}, speed=50) |
Build one absolute multi-joint pose command. The dictionary must contain at least one valid joint. |
robot.set_gripper(value, speed=55) |
Set the active robot's gripper value. |
robot.open_gripper() / robot.close_gripper() |
Use the active manifest's semantic gripper endpoints. |
robot.smooth_move(joint, start, end, seconds=1.5) |
Interpolate one joint between absolute start and end positions. This is not a multi-joint synchronized trajectory or exact hardware-duration guarantee. |
robot.get_joints() / robot.get_state() |
Return planned interpreter state, not a live blocking hardware read. |
LeKiwi additionally supports drive, drive_forward, drive_backward, strafe_left, strafe_right, turn_left, and turn_right.
save_pose / go_to_pose are not documented as SO-101 or LeKiwi support. The current goPose runner branch uses the legacy servo execution path. Use explicit robot.move_joints(...) dictionaries for modern robot packs.
SO-101: so101_follower
The SO-101 pack supports simulation and optional physical execution through the local bridge. Start in simulation. The browser never controls SO-101 serial hardware directly.
All revolute-joint values are absolute positions in the active calibrated joint coordinate. For example, robot.move_joint("elbow_flex", 40) targets position 40°; it does not add 40° to the current elbow position. Repeating that command after the joint reaches 40° requests no further change. Zero and positive direction are joint-specific calibration/manifest coordinates, not world-space or end-effector angles. The gripper value is likewise an absolute percentage in RoboBuddy's semantic open-to-close coordinate.
Joint reference
The listed ranges are the disconnected manifest defaults. A connected hardware session installs verified hardware-derived limits, and those active limits can be narrower.
| Joint id | Label | Unit | Manifest range | Home |
|---|---|---|---|---|
shoulder_pan |
Shoulder Pan | degrees | -90..90 | 0 |
shoulder_lift |
Shoulder Lift | degrees | -90..90 | -90 |
elbow_flex |
Elbow Flex | degrees | -120..120 | 85 |
wrist_flex |
Wrist Flex | degrees | -90..90 | 72 |
wrist_roll |
Wrist Roll | degrees | -180..180 | 88 |
gripper |
Gripper | percent | 0..100 | 85 |
SO-101 gripper semantic endpoints are open 20 and close 85.
Simulation-first coordinated pose
robot.move_joints({
"shoulder_pan": 5,
"shoulder_lift": -85,
"elbow_flex": 80,
"wrist_flex": 68,
"wrist_roll": 83,
"gripper": 80,
}, speed=30)
All six targets remain one validated pose command. SO-101 simulation applies the included values together as one immediate state update. It does not turn speed into a duration-synchronized trajectory.
Direct absolute targets on connected hardware
After the bridge is ready and motion is armed, specify the desired absolute destination directly:
robot.move_joints({
"shoulder_pan": 15,
"elbow_flex": 30,
}, speed=30)
robot.wait(0.5)
The bridge reads measured feedback and automatically advances a larger move through bounded look-ahead goals. The user does not calculate those safety steps. The final numbers remain absolute targets, not additions or subtractions.
Hardware readiness
Before any SO-101 hardware motion:
- Use the supported Python/LeRobot environment and start RoboBuddy's localhost bridge.
- Select the correct controller port.
- Verify the relevant LeRobot calibration for that arm and port.
- Confirm that hardware-derived joint limits were loaded.
- Connect the browser to the bridge with the current token.
- Explicitly arm motion for the current session.
- Keep the workspace clear and keep STOP/disarm immediately available.
Use the existing setup references:
- Local bridge architecture and operation
- GitHub Pages to localhost SO-101 setup
- SO-101 hardware guidance
What one hardware move_joints command does
For the included motors, one Python pose remains:
- one RoboBuddy program command;
- one localhost-bridge execution request;
- one direct LeRobot action map when the destination is already within every active relative-step guard; or
- multiple measured, bounded action maps when the destination is farther away.
For each bounded look-ahead goal, the included joints are dispatched together along one straight joint-space progress fraction. This is coordinated dispatch, not synchronized arrival. The bridge verifies that LeRobot accepted each goal, then advances the goal from fresh measured feedback while motion continues; it requires full settling only at the exact final destination. Joints can still finish at different instants because of travel distance, load, servo tuning, and physical constraints.
SO-101 limitations that must remain visible
speedis validated from1..100, but the current SO-101 program bridge ignores it formove_jointandmove_joints. It does not set physical velocity, duration, or synchronized arrival.- LeRobot relative-target protection defaults to
max_relative_target = 12.0(or an equivalent per-joint map) unless configured otherwise. The bridge reads fresh measured feedback before every look-ahead update and advances at no more than 90% of each active guard. It treats a changed/clipped returned action or stopped measured progress as a fault, holds measured position when possible, and disarms. - Verified Home, finite Python poses, manual sliders, and Leader modes have separate ownership paths. Each can use bounded intermediate actions while its requested target remains absolute.
- Hardware programs require bridge API
0.5withabsolute_program_targets_v1andbounded_absolute_program_motion_v1. Older bridges are rejected before program observation or motion rather than falling back to simulation. - Hardware position limits do not provide collision avoidance, payload protection, torque/current control, or workspace planning.
- A Python pose uses ordinary
programownership, not Virtual Leader ownership. - Virtual Leader hardware is separately qualification-gated. Documentation and software tests are not evidence of completed physical qualification.
- Phone Leader is also a separate bridge-owned mode. Its software/fake-evaluation evidence does not authorize physical motion.
LeKiwi: lekiwi_sim
lekiwi_sim is simulation-only in Tier 1. It has an arm, gripper, and simulated holonomic mobile base. There is no physical LeKiwi bridge or hardware adapter in this snapshot.
LeKiwi arm joint numbers have the same absolute meaning: 10 means target position 10° in that joint's manifest coordinate, not “add 10°” or a world-space angle. Simulation replaces each named joint target immediately; omitted joints keep their previous simulated position. Relative changes must be calculated explicitly from robot.get_joints().
Joint reference
| Joint id | Label | Unit | Range | Home |
|---|---|---|---|---|
shoulder_pan |
Arm Pan | degrees | -90..90 | 0 |
shoulder_lift |
Shoulder Lift | degrees | -90..90 | 0 |
elbow_flex |
Elbow Flex | degrees | -120..120 | 0 |
wrist_flex |
Wrist Flex | degrees | -90..90 | 0 |
wrist_roll |
Wrist Roll | degrees | -90..90 | 0 |
gripper |
Gripper | percent | 0..100 | 50 |
The initial LeKiwi preview uses gripper = 85, while robot.home() restores the manifest Home value 50. Semantic open is 20; semantic close is 85.
Coordinated arm pose in simulation
robot.move_joints({
"shoulder_pan": 20,
"shoulder_lift": -15,
"elbow_flex": 30,
"wrist_flex": 10,
"wrist_roll": 20,
"gripper": 40,
}, speed=40)
The included arm and gripper values update as one simulation command. Application is immediate; speed is not a timed trajectory and does not create synchronized arrival behavior.
Base, then arm: sequential commands
robot.drive_forward(30, seconds=1.0)
robot.wait(0.2)
robot.move_joints({
"shoulder_pan": 15,
"shoulder_lift": -20,
"elbow_flex": 35,
}, speed=40)
The runner completes the drive command's elapsed time, then the explicit wait, then issues the arm pose. This is sequential, not simultaneous base-and-arm motion. RoboBuddy currently has no combined command frame that drives the LeKiwi base while changing arm joints.
Mobile-base API
robot.drive(
vx_percent,
vy_percent=0,
omega=0,
seconds=1.0,
frame="robot",
)
LeKiwi is modeled as a holonomic three-wheel simulator. Linear percentages are scaled through the manifest maximum of 1.0 world unit per second. Angular speed is limited to -90..90 degrees per second. Convenience helpers provide forward, backward, strafing, and turning commands.
Tier 1 support matrix
| Robot | Simulation | Hardware | Arm joints | Gripper | Mobile base | Multi-joint pose | Continuous Leader |
|---|---|---|---|---|---|---|---|
arduino_arm |
Yes | Web Serial | Yes | Yes | No | Yes; runner expands the pose into sequential servo moves | No |
so101_follower |
Yes | Optional local bridge | Yes | Yes | No | Yes | Virtual Leader and Phone Leader are separate modes; physical use remains qualification-gated |
lekiwi_sim |
Yes | No—simulation-only | Yes | Yes | Yes, simulated | Yes, arm/gripper | No |
Execution and safety model
Student Python runs in Pyodide as a restricted command builder. Variables, loops, lists, dictionaries, functions, math expressions, and print() are available, but direct serial, DOM, file, network, package installation, input(), and ordinary import access are not exposed.
RoboBuddy validates robot identity, capabilities, joint names, active limits, speed fields, command shape, and timing before executing the command list through the existing runner, ownership, STOP, simulation, or bridge paths.
Current limits include:
- at most 1000 generated commands;
- an 8-second Python execution budget;
- command
speedfrom 1 through 100; robot.wait(...)from 0 through 30 seconds; andsmooth_moveduration from 0.2 through 10 seconds.
What is not supported
- No guarantee that joints sent together arrive together.
- No multi-joint duration-synchronized trajectory in the non-Arduino simulation paths.
- No single Python run that controls SO-101 and LeKiwi concurrently.
- No robot-pack switching or second robot instance inside a run.
- No live blocking joint read from
robot.get_joints(). - No combined LeKiwi base-and-arm command frame.
- No direct browser-to-SO-101 serial control; the local bridge, calibration, verified limits, and armed state are required.
- No LeKiwi hardware control in Tier 1.
- No claim that SO-101
speedcontrols hardware velocity or synchronized arrival. - No promise that a large SO-101 request bypasses relative-target clipping.
- No modern SO-101/LeKiwi promise for legacy
save_pose/go_to_poserunner behavior. - No claim that a finite Python pose is interchangeable with SolU's Virtual Leader or Phone Leader sessions.
Common errors and corrections
Wrong robot selected
Symptom: A valid joint or drive helper is rejected.
Correction: Select so101_follower or lekiwi_sim before the run. A program cannot switch the active robot internally.
Wrong joint name
Symptom: Validation reports an unknown joint.
Correction: Use exactly shoulder_pan, shoulder_lift, elbow_flex, wrist_flex, wrist_roll, or gripper. Do not use Arduino names such as base, shoulder, elbow, wrist_rot, or wrist_tilt for SO-101 or LeKiwi.
Value outside active limits
Symptom: Validation rejects the command before execution.
Correction: Keep the target within the active robot limits shown by RoboBuddy. For connected SO-101 hardware, verified limits may be narrower than the disconnected manifest table.
Bridge offline, not calibrated, or not armed
Symptom: SO-101 hardware execution is blocked.
Correction: Restore the localhost bridge connection, select and verify the correct port and calibration, confirm hardware limits, then explicitly arm the current session. Do not bypass the block.
SO-101 program target is not reached
Symptom: The bridge returns ACTION_CLIPPED, TARGET_NOT_REACHED, FEEDBACK_UNAVAILABLE, or PROGRAM_STAGE_LIMIT.
Correction: Users should provide the final absolute destination; the bridge calculates the intermediate safety stages. ACTION_CLIPPED, TARGET_NOT_REACHED, and PROGRAM_STAGE_LIMIT are terminal motion faults: the bridge attempts to hold fresh measured position and disarms. FEEDBACK_UNAVAILABLE disarms without writing cached coordinates as a hold. Inspect the workspace, measured state, physical obstruction, power, and communication before explicitly re-arming. Do not weaken max_relative_target merely to force a move.
Expecting speed to synchronize motion
Symptom: Joints do not arrive together or the SO-101 physical velocity does not reflect the Python number.
Correction: Treat speed as a validated command field. LeKiwi and SO-101 simulation apply poses immediately, and the current SO-101 program bridge ignores speed for the physical send path.
Trying to control two robots
Symptom: A cross-robot queue is rejected or code assumes separate SO-101 and LeKiwi objects.
Correction: Run one active robot at a time. Separate tabs are not a RoboBuddy synchronization system.
Expecting live feedback from get_joints()
Symptom: Code assumes the returned dictionary confirms measured physical arrival.
Correction: Treat the dictionary as planned interpreter state only.
Legacy Arduino compatibility
The historical arm.* API belongs to the active arduino_arm pack. In particular, arm.move_arm(base=..., shoulder=..., elbow=..., wrist_rot=..., wrist_tilt=..., gripper=...) constructs Arduino-era joint names.
Do not use arm.move_arm(...) for SO-101 or LeKiwi. Their joint manifests do not use those names. Use robot.move_joints(...) plus the six canonical ids instead.
The browser help retains the full Arduino reference as a clearly secondary legacy section so existing learners and saved materials remain understandable without presenting it as the modern universal API.