Program lifecycle

User programs are plain Python scripts executed by the on-device runner in ports/b2op/fs/runner.py. 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(0.1)

Button callbacks (hub.buttons.on()) fire automatically from the C firmware loop (Hub::loop) — no poll() call needed anywhere in user code.

Exiting a program

  • Call hub.exit() — raises SystemExit; the runner shows the “Done” screen. Safe from anywhere (button callbacks, loops, top-level code).

  • 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 Exception and shows the “Error” screen with its repr.

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:

  1. Disables LPF2 ports and zeroes all motor PWM outputs.

  2. Turns off the LCD backlight.

  3. Deinitialises BLE and WiFi.

  4. Signals the MicroPython task to exit gracefully — the garbage collector runs before the task deletes itself.

  5. Throttles the CPU to 80 MHz.

  6. Enters a low-activity loop that keeps the battery status LED updated every 2 s.

Pressing the right button restarts the firmware.

Background poll hook

hub.on() registers Python callables that the C firmware schedules on the MicroPython task every ~20 ms via mp_sched_schedule. This runs even while a user script is blocking inside exec. Currently only the "poll" event is defined.

@hub.on("poll")
def my_poll():
    # called ~every 20 ms on mp_task
    ...

The on-device protocol layer registers its stdin reader with hub.on("poll") so USB/BLE frames are processed even while a user script is running.

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