Programming the robot
Your robot runs MicroPython, and its behaviour lives in one file:
main.py. You do not compile anything. You edit that file, push it
to the robot, and it runs.
Where your code goes
A robot holds a handful of files. main.py is
yours — the only one you ever write. The rest are the library
that talks to the driver station and drives the motors for you; leave them
alone.
One more is worth knowing the name of: calib.json appears on its
own if anyone has trimmed the motors from the driver station, and it quietly
overrides your code. Read the trap below — it catches
people out.
How to edit and upload it
Open Robot code, plug the robot into USB, and click
Connect robot. Then Pull from robot reads its
current main.py into the editor, and
Upload and run writes your version back, restarts the robot,
and shows you what it prints. Chrome or Edge only.
bot.drive_left_moter(0.5) uploads happily
and then fails on the robot, where you will see it in the output as an
AttributeError. Check names against the tables below rather than
trusting them to be caught.
What runs when you power the robot on
-
The robot pauses about 1.5 seconds and prints
[boot] Starting in 1500 ms…. Nothing for you to do. - Your
main.pystarts. -
bot.begin()brings up the radio and the motors, and the robot starts answering the driver station. - Your
while True:loop runs until the power goes off.
Why the 1.5 second pause exists
Your loop never ends, so once it is running nothing else can get a word in — including an upload. That pause is the gap the upload tools use to interrupt the board before your code takes over, which is why Upload and run works on a robot that is already busy driving. Delete it and the only way to reprogram a robot would be a full erase.
Why begin() is separate from creating the bot
Making the Minibot only records your settings. Nothing reaches the
hardware until begin(), and the gap between the two lines is
deliberate.
A saved trim has to be read in between. If someone has used
the Neutral µs boxes on the driver station, that value lives on
the robot and wins over your MinibotConfig. It has to be loaded
before the motor outputs are created, because the very first pulse
those outputs send is the one the ESC believes — set it wrong and correct it a
moment later and the wheels have already moved. So the order is fixed: read
your settings, load any saved trim over them, then start the motors at the
neutral that came out of that.
And creating a robot should not make it twitch. Until
begin() runs there are no pulses on the motor pins and no radio.
That means bot = Minibot(config) is a safe line to sit on — useful
at the REPL, where you may want to look at a robot without arming it. It also
matches the shape you may know from Arduino: begin() is
setup(), and update() is the top of
loop().
The shape of every main.py
This is the starter template, and it is a complete working tank drive. Read it top to bottom before you change anything.
from minibot import Minibot
from minibot_config import MinibotConfig
# Your robot's name, its motor pins, and the radio channel.
config = MinibotConfig("MiniBot1", left_motor_pin=16, right_motor_pin=17, channel=6)
bot = Minibot(config)
bot.begin()
while True:
bot.update() # ALWAYS FIRST — handles comms, enable and the safety stop.
if bot.get_game_status() == Minibot.STANDBY:
# Not enabled by the driver station — stay still.
bot.stop_all_motors()
else:
# Tank drive: left stick drives the left tread, right stick the right.
# Pushing a stick up gives a NEGATIVE value, so flip the sign.
bot.drive_left_motor(-bot.get_left_y())
bot.drive_right_motor(-bot.get_right_y())
Three rules come out of that, and they hold for anything you write:
-
bot.update()is the first line in the loop. It is what receives radio packets, notices the enable switch, and runs the safety stop. Skip it and the robot goes deaf; put it at the bottom and everything you read is one loop stale. -
Check
get_game_status()before you drive. The library will stop the motors for you when the robot is disabled, but code that drives regardless is code that fights it. - Pushing a stick forward gives you a negative number. That is how gamepads report their Y axes, so tank drive needs the minus signs.
Setting up your robot: MinibotConfig
The name is positional; everything else must be passed
by keyword, as in the template above.
MinibotConfig("Bot1", 16, 17, 6) is an error.
| Setting | Default | What it does |
|---|---|---|
robot_id |
required | The name shown in the driver station. Give every robot a different one. 16 characters maximum. |
left_motor_pin |
required | GPIO pin the left motor's ESC signal wire is on. |
right_motor_pin |
required | GPIO pin the right motor's ESC signal wire is on. |
channel |
required |
Wi-Fi channel for the radio. Must match the dongle —
normally 6. Get this wrong and the robot never appears in
the driver station at all, with no error anywhere.
|
neutral_left_us |
1500 |
The pulse width that means "stop" for the left motor. 1500 µs is the RC standard. Raise or lower it by 20–30 at a time if the wheel creeps while the sticks are centred. |
neutral_right_us |
1500 |
The same, for the right motor. |
display_enabled |
True |
Show status on the OLED. Set False if there isn't one. |
ring_colors |
rainbow |
The list of (red, green, blue) colours the LED ring cycles
through, each value 0–255. You can also set it
with .with_ring_colors([...]) on the end of the config. The
default is available as RAINBOW_COLORS, imported from
minibot_config.
|
Motors always swing ±300 µs around their neutral, so a default robot drives between 1200 µs and 1800 µs.
Everything you can ask the robot
Call all of these on bot.
Setup and the loop
| Call | What it does |
|---|---|
bot.begin() |
Start the radio and the motors. Once, before the loop. |
bot.update() |
Handle comms, the enable switch and the safety stop. First line of every loop. |
Reading the gamepad
| Call | Gives you |
|---|---|
bot.get_left_x()bot.get_left_y()
|
Left stick, -1.0 to 1.0. Exactly
0.0 when centred — small resting wobble is swallowed for
you, so you don't need your own deadzone.
|
bot.get_right_x()bot.get_right_y()
|
Right stick, the same. |
bot.get_left_trigger()bot.get_right_trigger()
|
Triggers, -1.0 to 1.0. These are
not deadzoned, so a trigger at rest may read a hair off
zero — compare against a small threshold rather than
== 0.
|
bot.get_cross()bot.get_circle()bot.get_square()bot.get_triangle()
|
True while the button is held. These are the PlayStation
names; on an Xbox pad they are A, B, X and Y in that order.
|
bot.get_game_status() |
Minibot.TELEOP if the driver station has enabled this
robot, else Minibot.STANDBY.
|
Driving the motors
| Call | What it does |
|---|---|
bot.drive_left_motor(v) |
Drive the left motor. v runs -1.0 (full
reverse) through 0.0 (stop) to 1.0 (full
forward).
|
bot.drive_right_motor(v) |
The same, right motor. |
bot.stop_all_motors() |
Cut both motors to neutral, immediately. |
bot.clear_calibration() |
Delete a saved motor trim, handing control back to your
main.py. There is a button for this at
Robot code, which is easier.
|
The LED ring and the display
| Call | What it does |
|---|---|
bot.set_ring_delay(ms) |
Set how many milliseconds the ring waits between steps of its colour
rotation. Smaller is faster. The colours themselves come from
ring_colors in your MinibotConfig.
|
bot.set_display_line2(text) |
Stage a line of text on the second row of the OLED. Line one always
shows the robot's name. Nothing appears until you call
bot.display_show().
|
bot.display_show() |
Push the staged text to the OLED. |
Both display calls need a display. With
display_enabled=False, or if the OLED failed to start, they raise an
error rather than doing nothing.
Three things the library does whether you like it or not
These are safety and fairness features, and robot code cannot switch any of them off. Knowing they exist saves you hunting for bugs that aren't there.
Motor commands ramp
drive_left_motor() and drive_right_motor() ease
toward the number you pass rather than jumping to it — a full
forward-to-reverse reversal takes about half a second. So if
you print the value you asked for and watch the wheel, they will disagree for a
moment. That is correct, and there is no way to switch it off.
stop_all_motors() is never ramped, so real stops
are still instant.
Why the ramp exists
The reason is electrical. Slamming a stick through neutral puts the battery voltage and the motor's back-EMF in series, drawing roughly twice stall current. The rail sags, the ESP32 resets itself, and from the driver station that looks exactly like the robot dropping its radio link — so a brownout gets misdiagnosed as a radio problem. The ramp bounds the current instead.
The link-loss failsafe
If the robot hears nothing from the driver station for more than
250 ms, it stops the motors itself. This is what makes a robot
with a dead battery in the dongle, or one carried out of range, stop instead of
driving away. It is checked inside bot.update() — another reason
that call belongs at the top of your loop.
The driver station can cap your speed
There is a Speed limit slider on the
driver station that applies to every robot at once, and
it caps the same number you pass to drive_left_motor(). With the
limit at 0.60, asking for 1.0 gets you
0.60; anything already below that is untouched.
Robot code cannot raise it — a coach who has slowed the field down for a demo
should not be undone by an edit to main.py. If your robot feels slower than your numbers suggest, check the slider
before you go looking for a bug.
The cap is not stored on the robot, but the driver station remembers it
between visits, so one set last week is still set today — see
Driving the robot.
The trap that catches everyone
main.py.
When someone uses the Neutral µs boxes on the driver station,
the value is saved on the robot in calib.json and loaded
over whatever your MinibotConfig says. That is deliberate
— it means a trim survives a brownout mid-match — but it also means
editing neutral_left_us= in your code will appear to do
absolutely nothing
while a saved trim exists. To hand control back to your code, use
Clear saved calibration at
Robot code, which also shows you the saved values so
you can see whether one is there at all.
The same page shows a robot's saved trim whenever you pull from it, so when a robot behaves in a way your code does not explain, that is the first place to look.
Trimming a robot that creeps
A wheel that turns slowly while the sticks are centred needs its neutral adjusted. You can do it from either end:
- From the driver station, with the Neutral µs boxes and Apply. Fastest way to find the right number, because you can watch the wheel while you change it — but remember it then overrides your code. Driving the robot walks through it.
-
In your code, with
neutral_left_us=andneutral_right_us=inMinibotConfig. This is where the value belongs once you know it. Clear any saved trim first or you won't see your change.
Either way, values are clamped to the RC window, 1000–2000 µs.
Seeing what your robot is doing
print() works, and its output has somewhere to go.
Robot code streams it after an
Upload and run, so printing a value is the normal way to find
out why the robot is doing something odd. That output pane is also where a
crash in your code shows up, with the line number.
Print sparingly inside the loop, though — it runs thousands of times a second, and a print on every pass will bury anything useful.
Now drive it
Code on the robot does nothing until the driver station enables it. Driving the robot covers connecting the dongle, pairing your gamepad to your robot, and the key that stops everything.