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.
- 1WriteBuild commands
- 2ValidateCheck limits
- 3RunUse the runner
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
Partial cheat sheet. See the full reference for every command.
See an angle-outside-range example
arm.move_joint("base", 180) fails because the Arduino Arm base must stay within 20–130 degrees.
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.
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.
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.
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_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 semantic gripper endpoints are 20 for open and 85 for close.
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.
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
- Start the supported SolU-local Python and LeRobot bridge environment.
- Select the correct controller port and verify the calibration for that arm and port.
- Confirm that hardware-derived joint limits are loaded and that Home is compatible.
- Connect the browser to the bridge with the current token.
- Explicitly arm motion for the current session.
- Clear the workspace and keep STOP or disarm immediately available.
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.
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_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 preview uses gripper = 85, while robot.home() restores the manifest Home value
50. Semantic open is 20; semantic close is 85.
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)
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)
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()
Know what each programming method guarantees
| Control method | What it sends | Timing meaning | Use |
|---|---|---|---|
move_joint | One target for one joint | One finite command | Single-joint changes |
move_joints | Any subset of manifest joints in one pose | One dispatch; arrival is not synchronized | One-shot multi-joint poses |
| Virtual Leader | Continuously refreshed complete poses | Separate ownership, freshness, hold, and feedback rules | Leader-style continuous control |
| Phone Leader | Phone-tracked requests resolved by the bridge | Separate workspace, hold, and fault rules | Supervised phone-led control |
Tier 1 support matrix
| Robot | Simulation | Hardware | Multi-joint pose | Mobile base |
|---|---|---|---|---|
arduino_arm | Yes | Web Serial | Accepted, then expanded into sequential servo moves | No |
so101_follower | Yes | Optional local bridge with qualification gates | Yes | No |
lekiwi_sim | Yes | No; simulation-only | Yes, arm and gripper | Yes, simulated |
openarm_v2_bimanual | Yes | No; simulation-only | Yes, both arms and grippers | Rotational base joint only |
unitree_g1_29dof | Yes, kinematic | No; simulation-only | Yes, all 29 joints | Scripted 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.
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())
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 |
|---|---|---|---|
base | 0 | 20–130 | arm.move_joint("base", 90) |
shoulder | 1 | 15–165 | arm.move_joint("shoulder", 120) |
elbow | 2 | 0–180 | arm.move_joint("elbow", 75) |
wrist_rot | 3 | 0–180 | arm.move_joint("wrist_rot", 90) |
wrist_tilt | 4 | 0–180 | arm.move_joint("wrist_tilt", 110) |
gripper | 5 | 25–130 | arm.move_joint("gripper", 80) |
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.
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.
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.
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)
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.