The student-facing surface, which must behave the same on the robot, from the laptop and in the simulator. It is the single reference for all three, and it replaces the earlier surface in docs/python-api/. The robot runs student code in pocketpy, and the current robot has no buzzer, so the piezo functions are simulator-only.
| Code | Target | How the script runs |
|---|---|---|
| R | robot | uploaded to the ESP32-P4 and run by pocketpy against the C firmware |
| P | proxy | runs on the laptop, each call goes to the robot through the dongle |
| S | sim | runs in the browser IDE (or the Python package) against the simulated robot |
A function marked R P S behaves the same on all three. The exception is the piezo (tone, no_tone), simulator-only until the board that carries it. A script starts with import bugbot; bugbot.go() on every target; go() connects where a connection is needed and injects every public name into the script's globals. from bugbot import * is the explicit equivalent.
- Number arguments must be numbers (
intorfloat). Text is refused withTypeError("forward: distance must be a number, not the text '35'. input() gives text: make it a number with int() or float()"), the sentence aboutinput()only when the text looks like a number, and never converted, so a value typed intoinput()behaves the same on every target and learners meet the type error Python itself would give. - Speed: 0 to 100, percent of full power. Out-of-range values are clamped silently.
- Distance: centimetres. Velocity: centimetres per second.
- Angles: degrees. Heading is 0 to 360, clockwise positive, absolute from the IMU's magnetometer;
reset_heading()makes the current facing 0. - Position: (x, y) in centimetres from where the robot started or was last reset; x is right, y is forward.
- Robot frame for
drive: forward positive, right positive, clockwise rotation positive. - Blocking calls are marked. Everything else returns immediately.
- Motion deadman: if no motion command arrives for 500 ms the robot stops. Non-blocking motion must be re-sent; blocking calls handle this internally.
- Errors: a stopped script raises
RuntimeError("BugBot: script stopped")from any call; a sensor that cannot answer within 2 s raisesTimeoutError.
| Function | Targets | Behaviour |
|---|---|---|
forward(speed=50, distance=None) |
R P S | Drive forward. With distance (cm), blocks until the optical-flow odometry says it has been covered, then stops. |
backward(speed=50, distance=None) |
R P S | Same, backward. |
left(speed=50, distance=None) |
R P S | Strafe left (holonomic). Same distance option. |
right(speed=50, distance=None) |
R P S | Strafe right. |
spin_left(speed=50) |
R P S | Spin anticlockwise on the spot, non-blocking. |
spin_right(speed=50) |
R P S | Spin clockwise, non-blocking. |
turn(degrees, speed=30) |
R P S | Spin by an angle, closed loop on the IMU, blocking. Positive is clockwise. Safety timeout at 4x the expected time. |
drive(fwd, lat, rot=0) |
R P S | The primitive: three components, each -100 to 100, robot frame. Non-blocking. |
stop() |
R P S | All motors off. |
wait(seconds) |
R P S | Sleep, but keeps the deadman fed for the current motion and honours a stop request. |
clock() |
R P S | Seconds since the program started, float. Simulated time in the sim. |
bumped() |
R P S | True for about 0.3 s after the robot bumps into something (accelerometer jolt on the robot; a contact in the sim). |
Removed: the laptop's tank-style drive(left, right); left/right as spins.
| Function | Targets | Behaviour |
|---|---|---|
led(colour) or led(r, g, b) |
R P S | The one RGB LED. Colour names: red, green, blue, yellow, cyan, magenta, white, orange, purple, pink, off; or a hex code, "#RRGGBB" (added 13 September 2026). |
servo(index, angle) |
R P S | index 0 or 1, angle 0 to 180. Non-blocking; a hobby servo needs about 0.4 s to get there. Servo 0 is the gripper: 90 closes the jaws (holding a 40 mm ball that is within about 3 cm of the front), 0 opens them. Servo 1 is the kicker, a continuous-rotation servo: 90 stops it, 91 turns it slowly clockwise; one turn (about a second) winds and releases the spring-loaded lever, which sends the ball in front about 45 cm along the heading. gripper() and kick() below are the named versions of these moves. |
gripper(state) |
R P S | "open" or 1 opens the jaws (servo 0 to 0), "close" or 0 closes them (servo 0 to 90, holding a ball in reach). Blocks about 0.4 s while the jaws move. Anything else is a ValueError. Added 15 September 2026. |
kick() |
R P S | One kick: servo 1 to 91 for one turn (1 s), then 90. The spring sends the ball in front about 45 cm along the heading. Blocks for the turn. Added 15 September 2026. |
holding() |
R P S | The colour of the ball in the gripper, or None. |
| tone(freq, seconds=None) | S now; R P with the piezo board | A passive piezo: a square wave at freq hertz, 100 to 10000, loudest near its resonance (a few kilohertz). With seconds it blocks until the note is done and then stops; without, it plays until the next tone() or no_tone(). tone(0) is silence. |
| no_tone() | S now; R P with the piezo board | Silence the piezo. |
Removed: beep(). Added on 13 September 2026: tone() and no_tone() for a passive piezo, which the simulated robot has now and a later board version adds (one PWM-capable GPIO, a transistor driver and the piezo). Until then the robot and the laptop library raise RuntimeError("BugBot: this robot has no piezo") from both, rather than doing nothing. These two are the only functions in version 1 not on every target, and only until that board.
| Function | Targets | Behaviour |
|---|---|---|
send(text) |
R P S | Broadcast a short text message (up to 200 characters) to every other robot on the mat. Non-blocking. On the robot it goes over the dongle link, which relays it to the others. |
messages() |
R P S | Every message received since the last call, oldest first, as [from, text] pairs (from is the sender's name). Your own messages are not included. |
There is no addressing: everyone hears everything, so a message that is meant for one robot carries that robot's name in its text. In a game me.send(text) and me.messages do the same.
| Function | Targets | Returns |
|---|---|---|
distance() |
R P S | Nearest object ahead, cm, from the ToF grid's centre columns. |
tof_grid() |
R P S | 64 ints, cm, 8x8 row-major, 45 x 45 degrees. Rows are elevation: row 0 looks up, row 7 down at the mat a few cm ahead; rows 2 and 3 look level. Column 0 is the left. |
heading() |
R P S | Absolute heading, degrees 0 to 360. |
position() |
R P S | (x, y) cm. |
velocity() |
R P S | (vx, vy) cm/s from the optical-flow sensor, robot frame. New. |
imu() |
R P S | (heading, pitch, roll) degrees. New. |
battery() |
R P S | 0 to 100 percent. |
reset_heading() |
R P S | Current facing becomes 0. |
reset_position() |
R P S | Current spot becomes (0, 0). New. |
tof_grid() in proxy mode needs a new READ type on the dongle link; it is no longer robot-only.
| Function | Targets | Returns |
|---|---|---|
set_cv(mode, colour=None) |
R P S | "apriltag", "blob", "line", "contour", "face", "none". Runs on the P4. "blob" needs the colour to track: "red", "green", "blue" or "yellow" (one detector, one colour at a time). |
marker_tags(), robot_tags() |
R P S | apriltags() split by id: markers on the mat are tags 0 to 99, every robot wears a tag from 100 up (100 + its slot in a game). Same list format, nearest first, same detector setting. |
apriltags() |
R P S | list of [id, cx_px, cy_px, dist_cm]. |
camera_image(width=32, height=24) |
R P S | The camera frame scaled down to at most 64 by 48: a list of height rows of width (r, g, b) pixels, 0 to 255, row 0 at the top. For the data representation lessons (added 13 September 2026). Independent of set_cv. |
blobs() |
R P S | list of [cx, cy, area, x0, y0, x1, y1, aspect]. |
line() |
R P S | [cx_px, angle_deg] for the dark line on the mat ahead, [] if none in view. cx is where the line crosses the picture about 10 cm ahead (160 = under the nose); angle is its direction, positive to the right. Needs set_cv("line"). |
edges() |
R P S | [edge_count, dominant_angle_deg]. |
faces() |
R P S | list of [x1, y1, x2, y2, score, [10 keypoints]]. |
camera_suspend(), camera_resume() |
R P S | Power the camera down and up. |
In the sim, vision functions run on the rendered camera view, so the same lesson code works. tinyml_result() is dropped from version 1 and returns when the P4 model pipeline is defined.
Added 19 September 2026. These do nothing to the robot. In the sim they draw on the page; on the robot and in proxy mode they send what the program wants to see back to the laptop, where the dashboard's Charts window draws it, and plot() data is saved as <script>_plot.csv when the program ends.
| Function | Targets | Behaviour |
|---|---|---|
plot(name, value) |
R P S | Add a point to a chart. Up to eight named lines. |
draw(name, points, colour="yellow", style="dots", size=None) |
R P S | Draw a layer: points is a list of (x, y) in cm; style is "dots", "line" or "squares". Drawing a name again replaces it. Up to eight layers, 2000 points each. |
trace(name, ..., every="change") |
R P S | Print a trace table as the program runs: a column for each variable named (as text), and a new row, headed by the line that changed it, whenever one of them changes. every="row" writes every value on every row. On the robot it watches only the program's top-level variables. |
On the robot and in proxy mode each plot() and draw() call is one line of output, tagged so the laptop can tell it from the program's own print() lines (fields separated by a tab):
#BB plot <seconds> <name> <value>
#BB draw <name> <colour> <style> <size or empty> <x>,<y>;<x>,<y>;...
trace() prints ordinary lines, because its table is meant to be read. Until the firmware defines these three natively, the laptop library adds Python versions of them to the first line of an uploaded script (bugbot-python/bugbot/_report.py), so line numbers do not move; for trace() it also adds a check after each simple statement, since pocketpy has no trace hook.
motor_ok(), motor_raw_test(ms) stay on the robot for bring-up. They are not part of the contract and may change.
pocketpy on the robot returns lists, not tuples, for multi-value results. The laptop and sim implementations return tuples where this document says tuple. Student code that unpacks (x, y = position()) works on all three; code that compares types does not, and lessons avoid it.
This document is version 1. Firmware, dongle, laptop library and IDE each report which contract version they implement, and the IDE refuses to run a lesson written for a newer version than the connected robot.
Each implementation carries the same test script, api_conformance.py, which calls every function in this table and checks types, ranges and units. It runs in the sim in CI, on the laptop against a robot in proxy mode, and on the robot in upload mode before a release.
The camera: a 120 degree (horizontal) wide-angle lens; detections are reported in a 320 x 240 frame, so the focal length is about 92 px: a thing w cm wide that shows p pixels wide is about w * 92 / p cm away, and a pixel near the middle of the picture is about 0.375 degrees (a wide lens squeezes the edges, so treat that as a rule of thumb, or use atan((cx - 160) / 92)).
Tags on other robots: every BugBot wears an AprilTag on its body (ids from 100 up), so apriltags() reports other robots with their distance and bearing, the same as a marker. A lit LED on another robot shows up in blobs() as a small patch of the nearest named colour.