MiniCore Programming · Team 2367

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.

The editor does not check your spelling. A syntax error — a missing colon, an unclosed bracket — surfaces when you upload. A misspelled method name does not: 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

  1. The robot pauses about 1.5 seconds and prints [boot] Starting in 1500 ms…. Nothing for you to do.
  2. Your main.py starts.
  3. bot.begin() brings up the radio and the motors, and the robot starts answering the driver station.
  4. 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 0255. 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

A motor trim set from the driver station overrides your 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= and neutral_right_us= in MinibotConfig. 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.

Neutral is the pulse driven on every stop — including the 250 ms failsafe. A neutral near either end of that 1000–2000 range therefore makes "motors stopped" mean near-full throttle in that direction. Keep the robot on blocks the first time you apply an unfamiliar value, and watch the readout to confirm what actually landed.

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.