Program lifecycle
User programs are plain Python scripts executed by the on-device runner
(ports/b2op/app/Runner.cpp). Write a normal script — no special
decorators are needed.
Running a program
The runner execs the file and waits for it to finish. A program
that needs to keep running (sensor polling, animation) uses its own
while True loop and time.sleep_ms / time.sleep:
import hub, time
hub.led.setColorIdx(lpf2.color.GREEN)
while True:
print(hub.imu.pitch)
time.sleep_ms(10)
Note
Always include a small sleep (time.sleep_ms(5) or longer) in
tight loops. MicroPython’s internal scheduler — which delivers button
callbacks, STOP commands, and KeyboardInterrupt — only runs at
safe points such as time.sleep_ms. A loop with no yield can
prevent the host from stopping a runaway program.
Button callbacks (hub.buttons.on()) fire automatically — the C
firmware detects rising edges in the hardware loop and the MicroPython
task starts each callback in its own thread. No poll call is needed in
user code. hub.stop_callbacks() stops callbacks still running;
at program end the runner stops them (100 ms grace) before deleting
all remaining user threads.
Exiting a program
Return from top-level code or raise
SystemExit— the runner shows the “Done” screen. Programs are responsible for deciding when to quit.Hold the centre button >= 2 s — the C firmware schedules a
KeyboardInterrupt; the runner catches it and shows “Done”.The runner also catches any unhandled
Exceptionand shows the “Error” screen with itsrepr.
Powering off
Hold the centre button >= 5 s — hardware power-off from C.
Call
hub.powerOff()— immediate power-off from Python. Does not return.
If the hardware power latch does not hold (e.g. because USB power is present), the firmware performs an orderly shutdown:
Disables LPF2 ports and zeroes all motor PWM outputs.
Turns off the LCD backlight.
Deinitialises BLE and WiFi.
Signals the MicroPython task to exit gracefully — the garbage collector runs before the task deletes itself.
Throttles the CPU to 80 MHz.
Enters a low-activity loop that keeps the battery status LED updated every 2 s.
Pressing the right button restarts the firmware.
Background tasks
Two C FreeRTOS tasks run alongside the MicroPython task and handle communication and hardware polling with no Python involvement:
Protocol task (
hub_protocol_task, core 0) — reads USB CDC and BLE NUS input every 5 ms (1 ms while a transfer is active), reassembles frames, and dispatches commands (RUN, STOP, UPLOAD, …). Each poll is non-blocking: file transfers advance one bounded slice per poll, so other commands (and ABORT) are always answered. Runs continuously regardless of what the user script is doing.Hub hardware loop (
hub_main_task, core 0) — polls IMU, battery, and power button every 5 ms; detects button rising edges and schedules a dispatcher on the MicroPython task viamp_sched_schedule, which starts each registered callback in its own thread.
No manual poll wiring is needed — but include a time.sleep_ms in
any tight loop so the MicroPython scheduler can run (see the note
in Running a program above).
Log level
hub.log (hub._log_module) controls the firmware log
verbosity:
hub.log.setLevel(2) # 0=none, 1=err, 2=warn, 3=info, 4=debug, 5=verbose