Examples

The examples/ folder in the repository contains runnable MicroPython scripts. Highlights below — each is a full .py file you can copy to the hub’s fs/ directory.

Distance sensor

examples/distanceSensor.py
from hub import on
import hub, lpf2


@on("setup")
def setup():
    print("Distance Sensor Example")
    print("Connect a distance sensor to port B")


@on("loop")
def loop():
    port = hub.ports.B
    if (port.isDeviceConnected()
            and port.getDeviceType() == lpf2.device_type.TECHNIC_DISTANCE_SENSOR):
        print(port.getValueStr(0))
    hub.sleep(0.1)

Dumb-car (two motors + remote)

examples/dumbCar.py
from hub import on
import lpf2, hub


@on("setup")
def setup():
    print("Dumb Car Example")
    print("Connect a 'dumb' motor to port A and a distance sensor to port D")


@on("loop")
def loop():
    motor = hub.ports.A
    sensor = hub.ports.D
    if not (motor.isDeviceConnected()
            and motor.getDeviceType() == lpf2.device_type.TRAIN_MOTOR):
        hub.sleep_ms(50)
        return
    if not (sensor.isDeviceConnected()
            and sensor.getDeviceType() == lpf2.device_type.TECHNIC_DISTANCE_SENSOR):
        hub.sleep_ms(50)
        return

    distance = sensor.getValue(0, 0)
    if distance <= -0.1:
        distance = float('inf')

    if distance < 40:
        motor.startPower(-10)
    else:
        motor.startPower(50)

    hub.sleep(0.01)

Port expander

examples/expander.py
from hub import on
import lpf2, hub

@on("setup")
def setup():
    hub.ports.LED.setRgbColorIdx(lpf2.color.BLUE)
    portA = hub.ports.A
    print("Waiting for expander on port A ...")

    while True:
        dev = portA.device()
        if isinstance(dev, lpf2.devices.port_expander):
            exp1 = dev
            print("Expander found.")
            break
        hub.sleep_ms(100)

    portAB = exp1.getPort(lpf2.port_expander.port_num.B)
    print("Waiting for distance sensor on port AB ...")

    while True:
        dev = portAB.device()
        if isinstance(dev, lpf2.devices.distance_sensor):
            print("distance sensor found.")
            dev.setLight(100, 0, 0, 100)
            print("Distance: ", dev.getDistance(), " cm")
            break
        hub.sleep_ms(100)

    portAC = exp1.getPort(lpf2.port_expander.port_num.C)
    print("Waiting for encoder motor on port AC ...")

    while True:
        dev = portAC.device()
        if isinstance(dev, lpf2.devices.encoder_motor):
            print("motor found.")
            dev.startSpeedForDegrees(720, 100, 100, 0)
            break
        hub.sleep_ms(100)

    portAA = exp1.getPort(lpf2.port_expander.port_num.A)
    print("Waiting for expander on port AA ...")

    while True:
        dev = portAA.device()
        if isinstance(dev, lpf2.devices.port_expander):
            print("Expander found.")
            portAAA = dev.getPort(lpf2.port_expander.port_num.A)
            break
        hub.sleep_ms(100)

    print("Waiting for led on port AAA ...")

    while True:
        if portAAA.getDeviceType() == lpf2.device_type.HUB_LED:
            print("led found.")
            portAAA.setRgbColorIdx(lpf2.color.PURPLE)
            hub.sleep(5)
            portAAA.setRgbColorIdx(lpf2.color.BLACK)
            print("Test finished")
            break
        hub.sleep_ms(100)


@on("loop")
def loop():
    hub.sleep_ms(100)

IMU

examples/imu.py
from hub import on
import hub
import lvgl as lv


_labels = {}


@on("setup")
def setup():
    scr = lv.screen_active()
    scr.clean()
    scr.set_style_bg_color(lv.color_black(), 0)

    title = lv.label(scr)
    title.set_text("IMU")
    title.set_style_text_color(lv.color_white(), 0)
    title.align(lv.ALIGN.TOP_MID, 0, 2)

    attitude = lv.label(scr)
    attitude.set_style_text_color(lv.color_white(), 0)
    attitude.align(lv.ALIGN.TOP_LEFT, 4, 20)

    accel = lv.label(scr)
    accel.set_style_text_color(lv.color_white(), 0)
    accel.align(lv.ALIGN.TOP_LEFT, 4, 74)

    _labels["attitude"] = attitude
    _labels["accel"] = accel


@on("loop")
def loop():
    p = hub.imu.pitch
    y = hub.imu.yaw
    r = hub.imu.roll
    a = hub.imu.acceleration
    _labels["attitude"].set_text(
        "pitch {:>6.1f}\nyaw   {:>6.1f}\nroll  {:>6.1f}".format(p, y, r)
    )
    _labels["accel"].set_text(
        "ax {:>6.0f} mG\nay {:>6.0f} mG\naz {:>6.0f} mG".format(a.x, a.y, a.z)
    )
    hub.sleep(0.1)

LCD (LVGL)

examples/lcd.py
from hub import on
import hub
import lvgl as lv


@on("setup")
def setup():
    hub.lcd.init()
    hub.lcd.on()
    scr = lv.screen_active()
    btn = lv.button(scr); btn.center()
    lv.label(btn).set_text("hi")


@on("loop")
def loop():
    # Idle loop so the runner stays alive and the screen keeps showing.
    # Exit with a 2 s center-button hold.
    hub.sleep_ms(100)

BLE remote hub

examples/remote.py
from hub import on
import lpf2, hub

_hub = lpf2.hub()
_port = None
_dev = None
_done = False


@on("setup")
def setup():
    print("Waiting for a LEGO Hub to turn on.")


@on("loop")
def loop():
    global _hub, _port, _dev, _done

    if _done:
        hub.sleep_ms(100)
        return

    if not _hub.isConnected():
        if _hub.isConnecting():
            _hub.connectHub()
            if not _hub.isConnected():
                print("Failed to connect to HUB")
        else:
            _hub.init()
            hub.sleep(1)
        return

    if _port is None:
        print("Connected to HUB")
        hub.sleep_ms(100)
        port = _hub.getPort(lpf2.port_num.controlplus.LED)
        if isinstance(port, lpf2.port):
            port.setRgbColorIdx(lpf2.color.GREEN)
        _port = port
        return

    A = _hub.getPort(lpf2.port_num.controlplus.A)
    if _dev is None:
        if A is None:
            raise ValueError("Port A not found.")
        if A.isDeviceConnected():
            _dev = A.device()
            print("Waiting for GREEN color")
        else:
            hub.sleep_ms(100)
        return

    if isinstance(_dev, lpf2.devices.color_sensor):
        idx = _dev.getColorIdx()
        if idx == lpf2.color.CYAN:
            print("Done.")
            _done = True
        else:
            print("Color:", idx)
            hub.sleep_ms(100)
    else:
        _done = True

Grove I2C (MPU-6050)

examples/grove_i2c_mpu6050.py
"""MPU-6050 accelerometer over the Grove I2C connector.

Reads WHO_AM_I, wakes the chip, then streams 6-byte accel bursts.
Uses the shared internal I2C bus via ``hub.i2c`` — coexists with the
PCA9685, SC16IS750, BNO085 and everything else already on Wire1.
"""

import hub
import struct

ADDR = 0x68           # MPU-6050 / MPU-6500 default (AD0 low)
WHO_AM_I = 0x75
PWR_MGMT_1 = 0x6B
ACCEL_XOUT_H = 0x3B


@hub.on("setup")
def setup():
    print("scanning I2C bus...")
    devs = hub.i2c.scan()
    print("addresses:", [hex(a) for a in devs])
    if ADDR not in devs:
        print(f"MPU-6050 not found at {hex(ADDR)}")
        return

    who = hub.i2c.readfrom_mem(ADDR, WHO_AM_I, 1)[0]
    print(f"WHO_AM_I: 0x{who:02X}")

    # Wake up (default is sleep mode after reset).
    hub.i2c.writeto_mem(ADDR, PWR_MGMT_1, b"\x00")


_buf = bytearray(6)
_last_print = 0


@hub.on("loop")
def loop():
    global _last_print
    import time
    now = time.ticks_ms()
    if time.ticks_diff(now, _last_print) < 200:
        return
    _last_print = now

    hub.i2c.readfrom_mem_into(ADDR, ACCEL_XOUT_H, _buf)
    ax, ay, az = struct.unpack(">hhh", _buf)
    print(f"accel  x={ax:+6}  y={ay:+6}  z={az:+6}")

NeoPixel on a repurposed LPF2 port

Drives a WS2812 strip off port D’s ID1 line by first disabling the port so LPF2 releases the pin, then handing it to the standard machine.Pin + neopixel.NeoPixel driver.

examples/neopixel.py
import hub, machine
from hub import on
from neopixel import NeoPixel

leds = None  # placeholder, real object created in setup()
_hue = 0.0


def hsv_to_rgb(h, s, v):
    """
    h: hue, 0.0-1.0 (wraps around, so any float works via mod)
    s: saturation, 0.0-1.0
    v: value/brightness, 0.0-1.0
    returns: (r, g, b) each 0-255
    """
    h = h % 1.0
    i = int(h * 6.0)  # always 0-5 since h is 0.0-1.0
    f = (h * 6.0) - i
    p = v * (1.0 - s)
    q = v * (1.0 - s * f)
    t = v * (1.0 - s * (1.0 - f))

    if i == 0:
        r, g, b = v, t, p
    elif i == 1:
        r, g, b = q, v, p
    elif i == 2:
        r, g, b = p, v, t
    elif i == 3:
        r, g, b = p, q, v
    elif i == 4:
        r, g, b = t, p, v
    else:
        r, g, b = v, p, q

    return (int(r * 255), int(g * 255), int(b * 255))


def draw_rainbow(strip, start_hue=0.0, hue_increment=0.125, saturation=1.0, value=1.0):
    """
    Fills `strip` (a NeoPixel-like object) with a rainbow gradient.

    start_hue: hue of pixel 0, 0.0-1.0
    hue_increment: how much hue advances per pixel (wraps automatically)
    saturation, value: constant across the strip, 0.0-1.0
    """
    n = strip.n if hasattr(strip, "n") else len(strip)
    for i in range(n):
        hue = start_hue + i * hue_increment
        r, g, b = hsv_to_rgb(hue, saturation, value)
        strip[i] = (r, g, b)
    strip.write()


@on("setup")
def setup():
    hub.ports.D.disable()

    global leds
    leds = NeoPixel(machine.Pin(hub.board.PORT_D_ID_1), 8)


@on("loop")
def loop():
    global _hue
    draw_rainbow(leds, start_hue=_hue, hue_increment=0.01, saturation=1.0, value=0.2)
    _hue += 0.01  # animate — remove this line for a static rainbow

@hub.buttons.on("center")
def on_exit():
    hub.ports.D.disable(False)
    hub.exit()

Why hub.ports.D.disable() first

An LPF2 port owns its four pins (ID1, ID2, PWM1, PWM2). While the port is live the C++ side is doing three things on them:

  • driving the H-bridge PWM channels (PORT_D_PWM_1 / PORT_D_PWM_2),

  • toggling the ID lines between analog-ID probing and UART TX/RX,

  • polling the LPF2 UART on every update() call.

If a MicroPython driver grabs one of those pins while the port is still active, the two drivers fight for the pad — PWM keeps flipping it, the UART peripheral keeps re-configuring it, and any output the foreign driver writes gets stomped. WS2812 timing tolerates none of that, so the strip either shows garbage or stays dark.

disable() (implemented in lib/Lpf2/src/Lpf2/Local/PortProcess.cpp) is the release step:

  • turns the PCA9685 PWM channels off,

  • calls uartPinsOff() on the LPF2 serial, releasing ID1/ID2 from the UART peripheral,

  • stops polling — subsequent update() calls are no-ops until disable(False) re-enables the port.

Once disabled, the pin is a plain GPIO and any driver (machine.Pin, neopixel, bit-banged I2C, RMT, …) can take it over. The example uses PORT_D_ID_1 (see hub.board), but any of the four port pins works — pick whichever your wiring exposes.

Re-enabling the port

To hand the pin back to LPF2 later:

hub.ports.D.disable(False)

Note that resetDevice() runs on re-enable, so the port re-negotiates analog-ID and re-attaches whatever device is plugged in. Anything still holding a machine.Pin on those pads will now be fighting the port again — drop the foreign driver first.