Skip to Python documentation

RoboBuddy Python Docs

Arduino, SO-101, LeKiwi, OpenArm, and Unitree G1 Reference

Quick Start
Quick Start

Write real Python that builds safe robot commands.

This interpreter runs real Python, but the robot API is provided by RoboBuddy. Your Python code uses the robot object to build a command list. The older arm object remains available for Arduino arm compatibility. RoboBuddy validates that list, then runs it through the same preview and robot runner used by Blockly. The fixed Quick Start below targets arduino_arm; use the Robot API section for SO-101, LeKiwi, and OpenArm names and examples.

  1. 1WriteBuild commands
  2. 2ValidateCheck limits
  3. 3RunUse the runner
python-quick-start.py
robot.home()
robot.move_joint("base", 100, speed=50)
robot.wait(0.5)
print(robot.get_joints())

Common commands

robot.home()
Move to home position
robot.stop()
Stop all motion
robot.wait(seconds)
Wait before the next command
robot.move_joint(...)
Move one joint to a target
View full Robot API

Partial cheat sheet. See the full reference for every command.

Python builds commands. RoboBuddy stays in control. RoboBuddy validates joint limits, speed, timing, and command shape before motion.
Open this example in Python
See an angle-outside-range example

arm.move_joint("base", 180) fails because the Arduino Arm base must stay within 20–130 degrees.

Safety Model
The Python code does not write directly to the robotic arm. It creates commands, then RoboBuddy checks limits, speed, timing, and command shape before running them.

Real Python

The editor runs Python in Pyodide. Variables, loops, lists, functions, math expressions, and print() work.

Restricted Sandbox

No direct serial, DOM, file, network, package install, input(), or normal import access is exposed to student code.

Runner Controlled

After validation, movement is executed by RoboBuddy's existing program runner, preview, stop, and emergency-stop paths.

Robot API

The robot object

robot works with the active robot pack. It only creates validated RoboBuddy command JSON; it cannot access serial, files, network, the DOM, imports, packages, hardware SDKs, or system APIs. Select the robot on python.html before running: one Python run has one active robot and cannot switch packs or control SO-101 and LeKiwi concurrently.

robot.home(), robot.stop(), robot.wait(seconds)

Universal commands available to all Tier 1 robots. STOP is also available through the page STOP button.

robot.emergency_stop()

Add an emergency-stop command. The page's red STOP control remains the immediate operator stop path.

robot.move_joint(joint, value, speed=50)

Move one active robot joint to an absolute target within its manifest limits. The value is an end position, not a relative amount.

robot.move_joints({...}, speed=50)

Build one validated absolute pose command containing any subset of the active robot's joints. SO-101, LeKiwi, and OpenArm preserve it as one multi-joint runtime command. The Arduino runner accepts the pose but sends its servo targets sequentially.

robot.open_gripper(), robot.close_gripper(), robot.set_gripper(value)

Use the active robot manifest's gripper range and open/close values. Bimanual robots accept side="left", "right", or "both".

robot.smooth_move(joint, start, end, seconds=1.5)

Interpolate one joint between absolute start and end positions. This is not a synchronized multi-joint trajectory or an exact hardware-duration guarantee.

robot.get_joints(), robot.get_state()

Read planned interpreter state. These methods do not perform a blocking live hardware observation.

LeKiwi drive helpers

robot.drive_forward, drive_backward, strafe_left, strafe_right, turn_left, and turn_right work for lekiwi_sim. LeKiwi is simulation-only in Tier 1.

Several SO-101 or LeKiwi joints can be dispatched together. Put the targets in one robot.move_joints({...}) dictionary. They remain one pose command, but synchronized arrival is not guaranteed: travel distance, load, servo tuning, and physical constraints can make joints finish at different times.

Planned state, not live feedback

robot.get_joints() returns the interpreter's planned joint map. Later calls reflect commands already planned in the same run; they do not block for a fresh hardware observation or prove that a target was reached.

Finite pose, not Leader streaming

move_joints is a one-shot program command. SolU's Virtual Leader and Phone Leader are separate continuously refreshed sessions with their own ownership, freshness, hold, workspace, and fault rules.

One robot at a time

Cross-robot command queues are rejected. A Python program cannot create a second robot instance, switch robot packs, or synchronize separate browser tabs.

SO-101 Guide

Program the SO-101 Follower

On python.html, select SO-101 Follower before running the program. Start in simulation. The browser builds validated commands for so101_follower; it never opens the arm's serial port or imports LeRobot directly.

Joint names, limits, and Home values

Use the six manifest ids below. The displayed ranges are disconnected defaults. A connected session may install narrower, hardware-derived limits, and those active limits take precedence.

Joint id Label Unit Manifest range Home
shoulder_panShoulder Pandegrees-90..900
shoulder_liftShoulder Liftdegrees-90..90-90
elbow_flexElbow Flexdegrees-120..12085
wrist_flexWrist Flexdegrees-90..9072
wrist_rollWrist Rolldegrees-180..18088
gripperGripperpercent0..10085

SO-101 semantic gripper endpoints are 20 for open and 85 for close.

Joint numbers are absolute end positions. robot.move_joint("elbow_flex", 40) means move the elbow toward position 40°; it does not add 40°. To request a relative change, read the planned pose and calculate a new absolute target, such as pose["elbow_flex"] -= 5. Repeating an already reached target is idempotent. Degrees use the active calibrated joint coordinate: zero and positive direction are joint-specific, not world-space angles.
Give the final absolute target; the bridge handles bounded progress. Before every program stage, the bridge reads measured feedback and computes a common progress fraction capped at 90% of each joint's active relative-step guard. The requested values remain absolute destinations. Hardware programs require bridge API 0.5 with absolute_program_targets_v1 and bounded_absolute_program_motion_v1; older bridges are blocked before program observation or motion.

Move several SO-101 servos in one pose

# Select SO-101 Follower and start in simulation.
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. In simulation, the included values update together as one immediate state change. On hardware, one pose remains one program command and one bridge execution request. A nearby destination uses one LeRobot action map; a larger destination is automatically expanded into measured, bounded action maps. Included joints share one straight joint-space progress fraction per stage, but physical arrival is not guaranteed to be synchronized.

Build a complete pose from planned state

pose = robot.get_joints()
pose["shoulder_pan"] = 10
pose["elbow_flex"] = 25
robot.move_joints(pose, speed=40)

robot.get_joints() returns planned interpreter state. For connected SO-101 hardware it is seeded from fresh measured bridge state when the run starts, then reflects commands planned in that run. It is not a blocking hardware read and does not prove physical arrival.

Send the final absolute target

robot.move_joints({
    "shoulder_pan": 15,
    "elbow_flex": 30,
}, speed=30)
robot.wait(0.5)

The bridge calculates any required intermediate safety stages from fresh measured feedback. Users do not hand-author those increments, and move_joints continues to mean absolute end positions.

Hardware readiness checklist

  1. Start the supported SolU-local Python and LeRobot bridge environment.
  2. Select the correct controller port and verify the calibration for that arm and port.
  3. Confirm that hardware-derived joint limits are loaded and that Home is compatible.
  4. Connect the browser to the bridge with the current token.
  5. Explicitly arm motion for the current session.
  6. Clear the workspace and keep STOP or disarm immediately available.
Do not treat the Python speed value as hardware velocity control. The current SO-101 program bridge validates speed from 1 through 100 but ignores it when sending move_joint and move_joints. It does not set duration or common arrival time. Before motion, the bridge compares every absolute target with fresh measured feedback and the active max_relative_target guard. If any step is too large, the whole pose is rejected without a write. The bridge never reports a silently clipped program action as success.

Position limits do not provide collision avoidance, payload protection, torque/current control, or workspace planning. Finite Python poses use ordinary program ownership. Virtual Leader and Phone Leader are separate continuously refreshed, qualification-gated control modes.

Setup references: local bridge, GitHub Pages to localhost, and SO-101 hardware guidance.

LeKiwi Guide

Program the LeKiwi mobile robot

On python.html, select LeKiwi mobile robot. The lekiwi_sim pack is simulation-only in Tier 1: it has a six-joint arm, gripper, and simulated holonomic base, but no physical LeKiwi bridge or hardware adapter.

Joint names, limits, and Home values

Joint id Label Unit Range Home
shoulder_panArm Pandegrees-90..900
shoulder_liftShoulder Liftdegrees-90..900
elbow_flexElbow Flexdegrees-120..1200
wrist_flexWrist Flexdegrees-90..900
wrist_rollWrist Rolldegrees-90..900
gripperGripperpercent0..10050

The initial preview uses gripper = 85, while robot.home() restores the manifest Home value 50. Semantic open is 20; semantic close is 85.

LeKiwi arm targets are absolute too. A joint value of 10 means target position 10°, not “add 10°.” Simulation immediately replaces each named joint value; omitted joints keep their previous simulated position.

Move several LeKiwi arm servos in one pose

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.

Drive the mobile base

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 use the manifest maximum of 1.0 world unit per second. Angular speed is limited to -90..90 degrees per second. Convenience helpers include drive_forward, drive_backward, strafe_left, strafe_right, turn_left, and turn_right.

Sequence the base, then the arm

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)
LeKiwi base and arm commands are sequential. The runner completes the drive duration, then the wait, then the arm pose. SolU has no combined frame for simultaneous base-and-arm motion, and one Python run cannot control LeKiwi and SO-101 together.
OpenArm Guide

Program the OpenArm V2 Bimanual simulator

Select OpenArm V2 Bimanual · Low Stand + Rotating Base on an operational page. This pack is simulation-only and exposes a programmable base_yaw, two independent seven-joint arm chains, and two independent pinch grippers.

Move both arms in one absolute pose

robot.home()
robot.move_joint("base_yaw", 45, speed=30)
robot.move_joints({
    "left_j1": 20,
    "left_j2": -7,
    "left_j4": 0,
    "right_j1": -20,
    "right_j2": 7,
    "right_j4": 0,
}, speed=40)

Control the grippers independently

robot.open_gripper(side="left", speed=25)
robot.close_gripper(side="right", speed=25)
robot.open_gripper(side="both", speed=25)

# The grippers are also ordinary manifest joints.
robot.move_joint("left_gripper", 12, speed=25)
robot.move_joint("right_gripper", 36, speed=25)
Simulation does not qualify physical OpenArm motion. No hardware adapter, calibration profile, motor map, torque limits, collision planner, or mechanical validation of the shortened stand and turntable is included.
Unitree G1 Guide

Program all 29 joints in the kinematic G1 simulator

Select Unitree G1 29-DoF on an operational page. Every leg, waist, arm, and wrist joint is available through robot.move_joint(...) and robot.move_joints({...}). The complete joint list and pinned mesh provenance are in the G1 simulator guide.

robot.home()
robot.set_posture("wide_stance", seconds=0.8)
robot.walk("forward", steps=3, step_length=0.08, speed=45)
robot.turn(90, seconds=1.2)
robot.set_posture("work_reach", seconds=0.8)
robot.pick_nearest("right_hand")
robot.release("right_hand")
# Or run the authored walk, grab, return preview:
robot.run_demo()
G1 locomotion is scripted kinematics only. No dynamics, balance control, contact solving, collision avoidance, articulated fingers, torque control, or Unitree hardware adapter is included. STOP interrupts the visual action through RoboBuddy's normal runner.
Robot Support & Limits

Know what each programming method guarantees

Control method What it sends Timing meaning Use
move_jointOne target for one jointOne finite commandSingle-joint changes
move_jointsAny subset of manifest joints in one poseOne dispatch; arrival is not synchronizedOne-shot multi-joint poses
Virtual LeaderContinuously refreshed complete posesSeparate ownership, freshness, hold, and feedback rulesLeader-style continuous control
Phone LeaderPhone-tracked requests resolved by the bridgeSeparate workspace, hold, and fault rulesSupervised phone-led control

Tier 1 support matrix

Robot Simulation Hardware Multi-joint pose Mobile base
arduino_armYesWeb SerialAccepted, then expanded into sequential servo movesNo
so101_followerYesOptional local bridge with qualification gatesYesNo
lekiwi_simYesNo; simulation-onlyYes, arm and gripperYes, simulated
openarm_v2_bimanualYesNo; simulation-onlyYes, both arms and grippersRotational base joint only
unitree_g1_29dofYes, kinematicNo; simulation-onlyYes, all 29 jointsScripted planar visual root

Runtime limits

Commands
1000 max
Python time
8 seconds
Speed
1–100
Wait
0–30 seconds
Smooth move
0.2–10 seconds

Current execution boundaries

One active robot

No robot-pack switching, second robot instance, SO-101/LeKiwi concurrency, or cross-tab synchronization is available inside a run.

No live blocking read

get_joints() and get_state() return planned interpreter state, not measured arrival.

No synchronized trajectory

A multi-joint pose is dispatched together, but no non-Arduino path promises common duration or arrival time.

Legacy poses are not modern robot APIs

Do not use arm.move_arm, save_pose, or go_to_pose as SO-101 or LeKiwi support. Use explicit robot.move_joints dictionaries.

SO-101 and LeKiwi troubleshooting

Wrong robot or joint name

Select so101_follower or lekiwi_sim before the run. Use only shoulder_pan, shoulder_lift, elbow_flex, wrist_flex, wrist_roll, and gripper.

Value outside active limits

Keep targets inside the active limits shown by RoboBuddy. Connected SO-101 limits may be narrower than the manifest table.

Bridge offline, uncalibrated, or not armed

Restore the local bridge, correct port and calibration, verified limits, and explicit session arming. Do not bypass the block.

SO-101 program target is rejected or not reached

TARGET_STEP_TOO_LARGE means no write for that program pose occurred; use an explicit supervised intermediate absolute target. ACTION_CLIPPED or TARGET_NOT_REACHED means the bridge faulted, attempted to hold measured position, and disarmed. FEEDBACK_UNAVAILABLE disarms without writing cached coordinates as a hold. Inspect the workspace and measured state before explicitly re-arming. Do not enlarge or disable the protection.

Expecting speed to synchronize motion

Simulation applies poses immediately, and the current SO-101 program bridge does not use this field as physical velocity or duration.

Expecting two robots or live feedback

Run one selected robot at a time and treat get_joints() as planned state only.

The same material remains available as a maintainable source reference in the SO-101 and LeKiwi programming guide.

Arm API

The arm object

arm is a RoboBuddy classroom object. It is available automatically in the Python editor. Do not import it. It remains as a backward-compatible alias for Arduino arm programs.

arm.move_joint(joint, angle, speed=50)

Move one joint to a target angle. Use a joint name such as "base" or a number from 0 to 5. Speed must be 1–100.

arm.move_joint("shoulder", 120, speed=45)

arm.move_arm(..., speed=50)

Plan all six Arduino joints in base, shoulder, elbow, wrist rotation, wrist tilt, and gripper order. Omitted joints keep their current planned angle. The runner sends included Arduino servo targets sequentially. Speed must be 1–100.

arm.move_arm(
  base=90,
  shoulder=110,
  elbow=70,
  wrist_rot=90,
  wrist_tilt=100,
  gripper=80,
  speed=45
)

arm.home()

Move to the default home pose: all six joints at 90 degrees.

arm.home()

arm.wait(seconds)

Add a delay between movements. The accepted range is 0–30 seconds.

arm.wait(0.5)

arm.gripper_open(speed=55)

Open the gripper to the classroom default open angle, 50. Speed must be 1–100.

arm.gripper_open()
arm.wait(0.3)

arm.gripper_close(speed=55)

Close the gripper to the classroom default close angle, 120. Speed must be 1–100.

arm.gripper_close(speed=45)

arm.smooth_move(joint, start, end, seconds=1.5)

Move one joint gradually from a start angle to an end angle. Duration must be 0.2–10 seconds.

arm.smooth_move("shoulder", 65, 120, seconds=1.2)

arm.save_pose(name)

Save the current planned angles under a pose name for the current Python run.

arm.save_pose("pickup")

arm.go_to_pose(name, speed=50)

Ask the runner to move to a saved pose. Speed must be 1–100. If the pose name is unknown, the runner reports that it was not found.

arm.go_to_pose("pickup", speed=45)

arm.emergency_stop()

Add an emergency-stop command to the generated command list. Use the red STOP button for immediate operator stop.

arm.emergency_stop()

arm.get_angles()

Return the current planned angles as a list in joint order.

print(arm.get_angles())
Arduino Joint Reference

Legacy arduino_arm names and ranges

These names belong to the Arduino Arm. SO-101 and LeKiwi use the six manifest ids listed in the Robot API section.

Joint Index Allowed angle Example
base020–130arm.move_joint("base", 90)
shoulder115–165arm.move_joint("shoulder", 120)
elbow20–180arm.move_joint("elbow", 75)
wrist_rot30–180arm.move_joint("wrist_rot", 90)
wrist_tilt40–180arm.move_joint("wrist_tilt", 110)
gripper525–130arm.move_joint("gripper", 80)
Python Basics In This Lab

Allowed helpers

These built-ins are available:

absbooldictenumeratefloatintlenlistmaxminprintrangeroundstrsumtuple

Math is already provided

Use math directly. Do not write import math in this sandbox.

angle = 90 + round(20 * math.sin(1))
arm.move_joint("base", angle)

Not exposed here

This student interpreter does not provide file access, package installs, network calls, browser DOM access, direct serial access, or normal module imports.

Avoid infinite loops. Python execution stops after 8 seconds, and a program can generate at most 1000 commands.
Gemini Page Syntax

Writing Gemini requests on the Gemini page

The Gemini page uses Google's google.genai style, but it runs through a RoboBuddy classroom shim. Your code captures one client.models.generate_content(...) request, the page sends it with the Session API Key field and Model selector, then response.text is replayed into your code.

Keep keys, model choice, and images in the page controls. Do not put an API key, local file path, or image bytes in Python. The Model selector is the model used for the real request, so keep the model string in code matched to the selector to avoid confusion.

Basic text request

Create a client with no API key argument, call generate_content once, then read response.text.

from google import genai

client = genai.Client()
response = client.models.generate_content(
  model="gemini-3.5-flash",
  contents="Explain one safe wave motion for RoboBuddy."
)
print(response.text)

Text parts

contents can be a string or a list of user text parts. Use parts when separate instructions make the prompt easier to read.

from google import genai

client = genai.Client()
response = client.models.generate_content(
  model="gemini-3.5-flash",
  contents=[
    {
      "role": "user",
      "parts": [
        {"text": "Plan a safe classroom robot motion."},
        {"text": "Use short steps and avoid joint limits."}
      ]
    }
  ]
)
print(response.text)

Supported config

The page accepts common GenerateContentConfig fields and converts them for the browser request.

from google import genai

types = genai.types
client = genai.Client()
response = client.models.generate_content(
  model="gemini-3.5-flash",
  contents="Return a short JSON plan for a safe wave.",
  config=types.GenerateContentConfig(
    temperature=0.2,
    thinking_config=types.ThinkingConfig(thinking_budget=0),
    response_mime_type="application/json",
    response_schema={
      "type": "object",
      "properties": {
        "summary": {"type": "string"},
        "steps": {
          "type": "array",
          "items": {"type": "string"}
        }
      },
      "required": ["summary", "steps"]
    }
  )
)
print(response.text)

Image input

Choose an image with the Gemini page upload or camera controls. RoboBuddy adds that image to the first request part.

from google import genai

client = genai.Client()
response = client.models.generate_content(
  model="gemini-3.5-flash",
  contents="Use the selected page image to name objects the arm should avoid."
)
print(response.text)

Robot Python generation

Click Generate robot Python for robot code. Review the generated arm.* code, validate it, then run it.

from google import genai

client = genai.Client()
response = client.models.generate_content(
  model="gemini-3.5-flash",
  contents="Create a safe three-step wave for RoboBuddy."
)
print(response.text)
roboadmin.use_response(response.text)

Page limits

Use only one generate_content call per run. roboadmin.use_response(...) is a page replay helper, not direct hardware control.

The sandbox rejects API keys in Python and does not support streaming, chat sessions, local files, package imports, or multi-call scripts.

These examples adapt Google's Gemini API patterns for the RoboBuddy page. For full SDK behavior outside this lab, see Google's Generate content API, structured output guide, and image understanding guide.

Student Examples

Wave motion

arm.home()
for _ in range(4):
  arm.move_joint("base", 60, speed=55)
  arm.wait(0.4)
  arm.move_joint("base", 120, speed=55)
  arm.wait(0.4)

Open and close the gripper

arm.gripper_open()
arm.wait(0.5)
arm.gripper_close(speed=45)

Move to a full pose

arm.move_arm(
  base=85,
  shoulder=120,
  elbow=60,
  wrist_rot=90,
  wrist_tilt=115,
  gripper=70,
  speed=45
)

Save and return to a pose

arm.move_arm(base=70, shoulder=115, elbow=65, gripper=80)
arm.save_pose("pickup")
arm.home()
arm.wait(0.5)
arm.go_to_pose("pickup", speed=45)

Smooth movement

arm.home()
arm.smooth_move("shoulder", 80, 130, seconds=1.5)
arm.smooth_move("shoulder", 130, 80, seconds=1.5)

Use planned angles

arm.home()
angles = arm.get_angles()
print("Starting pose:", angles)
arm.move_joint("base", angles[0] + 20)

Use math without importing

arm.home()
for step in range(6):
  angle = 90 + round(20 * math.sin(step))
  arm.move_joint("base", angle, speed=50)
  arm.wait(0.2)
Common Errors

Wrong joint name

# Wrong
arm.move_joint("wrist", 90)

# Use one of the documented names
arm.move_joint("wrist_tilt", 90)

Angle outside range

# Base is 20–130, so this fails
arm.move_joint("base", 180)

Speed outside range

# Speed must be 1–100
arm.move_joint("elbow", 90, speed=200)

Infinite loop timeout

# This does not finish building commands
while True:
  arm.wait(0.1)

The worker stops Python execution after 8 seconds. Use for loops with a fixed count.

Importing modules

# Do not do this in the lab sandbox
import math

# math is already available
print(math.sin(1))

Expecting a native robot library

arm is injected by RoboBuddy for this lab. It is not a standard Python module and does not exist outside this page.

Direct hardware access

Python cannot write to serial hardware directly. Use the manifest-driven robot API, or legacy arm commands for Arduino, and RoboBuddy will route validated commands through the active runtime.