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
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)
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
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
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)
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
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)
"""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.
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 untildisable(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.