Examples

The repo ships runnable example programs in examples/. Push any of them to a hub with openbricks run -n <name> <script> or try them in the simulator with openbricks sim run <script>.

Two representative ones are reproduced below.

Drive a square (ST-3032 drivebase)

# SPDX-License-Identifier: MIT
"""
Drive a square with the ST-3032 drivebase.

Four sides of ``SIDE_MM`` straight + ``+90°`` turn each. Demonstrates
``DriveBase.straight`` / ``turn`` composing into a closed loop — if
the chassis geometry (``WHEEL_DIAMETER_MM`` / ``AXLE_TRACK_MM``) is
calibrated correctly, the robot returns to within a few cm of its
starting pose after one lap.

Uses the new ``then="coast"`` default end-state (1.6.7), so the
wheels free-wheel briefly between segments. If your bench has
significant momentum carryover and you'd rather pin the wheels
between sides, pass ``then="brake"`` to the ``straight`` / ``turn``
calls (or ``then="hold"`` for active position lock — ST-3032
supports it).

Hardware: identical to ``examples/st3032_drivebase_test.py``.

Run with:
    openbricks run -n ls examples/st3032_drivebase_square.py
"""

from openbricks.drivers.st3032 import ST3032Motor
from openbricks.robotics import DriveBase


LEFT_ID, RIGHT_ID = 2, 1
UART_ID, TX, RX   = 1, 14, 41

WHEEL_DIAMETER_MM = 88
AXLE_TRACK_MM     = 136

SIDE_MM   = 200
NUM_LAPS  = 1

STRAIGHT_SPEED_MM = 150
TURN_RATE_DPS     = 200


def main():
    print("--- ST-3032 drivebase square (%d mm sides × %d laps) ---" %
          (SIDE_MM, NUM_LAPS))

    left  = ST3032Motor(servo_id=LEFT_ID,  uart_id=UART_ID, tx=TX, rx=RX, invert=True)
    right = ST3032Motor(servo_id=RIGHT_ID, uart_id=UART_ID, tx=TX, rx=RX)

    db = DriveBase(left, right,
                   wheel_diameter_mm=WHEEL_DIAMETER_MM,
                   axle_track_mm=AXLE_TRACK_MM)
    db.settings(straight_speed=STRAIGHT_SPEED_MM, turn_rate=TURN_RATE_DPS)

    for lap in range(NUM_LAPS):
        for side in range(4):
            print("  lap %d side %d: straight(%d) → turn(+90)" %
                  (lap + 1, side + 1, SIDE_MM))
            db.straight(SIDE_MM)
            db.turn(90)

    print("--- done ---")


main()

Rounded square (DriveBase.curve)

curve(radius, angle) follows the Pybricks contract — positional order, parameter names, and sign semantics: positive angle arcs right (clockwise), a negative radius drives the arc backward, and curve(0, angle) degrades to a turn in place. The forward and turn profiles run with proportionally scaled speed and acceleration, so the path is a true circle even through the ramps, and the outer wheel is automatically capped at the straight_speed setting. (One deviation: then defaults to "coast" like every openbricks move; pass then="hold" for the Pybricks end state.)

# SPDX-License-Identifier: MIT
"""Drive a rounded square with DriveBase.curve().

Four straights joined by four quarter-circle arcs — the robot never
stops to pivot, so the lap is faster and smoother than the
straight+turn square. curve(radius, angle) follows Pybricks: positive
angle arcs right, the radius sign picks forward/backward, and the
outer wheel is automatically capped at the straight_speed setting.

Run with:
    openbricks run -n ls examples/st3032_drivebase_curve.py
"""

from openbricks.drivers.st3032 import ST3032Motor
from openbricks.robotics import DriveBase

LEFT_ID, RIGHT_ID = 2, 1
UART_ID, TX, RX = 1, 14, 41

WHEEL_DIAMETER_MM = 88
AXLE_TRACK_MM = 136

SIDE_MM = 150
RADIUS_MM = 60
NUM_LAPS = 1

left = ST3032Motor(servo_id=LEFT_ID, uart_id=UART_ID, tx=TX, rx=RX,
                   invert=True)
right = ST3032Motor(servo_id=RIGHT_ID, uart_id=UART_ID, tx=TX, rx=RX)
db = DriveBase(left, right,
               wheel_diameter_mm=WHEEL_DIAMETER_MM,
               axle_track_mm=AXLE_TRACK_MM)
db.settings(straight_speed=150, turn_rate=200)

print("rounded square: %d mm sides, %d mm corner radius" %
      (SIDE_MM, RADIUS_MM))
for lap in range(NUM_LAPS):
    for side in range(4):
        db.straight(SIDE_MM)
        db.curve(radius=RADIUS_MM, angle=90)
print("done")

Square up on a line (QTR sensor bar)

The classic align move on the QTRLineSensor window: each half of the ten-element bar acts as one virtual corner sensor, in two passes. Seek: drive slowly toward the line — the wheel whose half reaches it first stops while the other keeps rolling, pivoting the chassis square. Edge: servo each wheel proportionally — the follower’s KP discipline — until its half reads ambient of about 50, the elements straddling the black/white boundary, parked right ON the line’s edge. Calibrate once with examples/qtr_calibrate.py first; mount the bar ahead of the wheels.

# SPDX-License-Identifier: MIT
"""Square up on the edge of a perpendicular line, QTRLineSensor.

The classic FLL/WRO align move, on one sensor bar instead of two
corner sensors, in two passes: drive slowly toward the line until
each half of the window reads solidly dark (mean ambient under 30
— the wheel whose half arrives first stops, the other pivots the
chassis on), then servo each wheel proportionally — the follower's
KP discipline — until its half reads ambient of about 50, the
elements straddling the black/white boundary. Both halves end ON the edge, so the bar —
and the chassis — is square right at it.

Run ``examples/qtr_calibrate.py`` once first. The bar must be
mounted ahead of the wheels; the farther ahead, the finer the final
heading.
"""

import time

from openbricks.drivers.qtr import QTRLineSensor
from openbricks.drivers.st3032 import ST3032Motor
from openbricks.robotics import DriveBase

# --- control law (pure logic, unit-tested in tests/test_qtr_align.py) ---

SEEK_DPS = 100
KP = 1.3
EDGE_TOLERANCE = 5
SIDE_COUNT = 5


def side_ambient(elements, target):
    total = 0
    for e in elements:
        total += e.ambient()
    return total // len(elements) - target


def edge_dps(elements):
    error = side_ambient(elements, 50)
    if abs(error) <= EDGE_TOLERANCE:
        return 0
    return int(KP * error)


def seek_wheel_speeds(reading):
    left_on = side_ambient(reading.elements[:SIDE_COUNT], 30) < 0
    right_on = side_ambient(reading.elements[-SIDE_COUNT:], 30) < 0
    if left_on and right_on:
        return None
    return (0 if left_on else SEEK_DPS,
            0 if right_on else SEEK_DPS)


def edge_wheel_speeds(reading):
    left = edge_dps(reading.elements[:SIDE_COUNT])
    right = edge_dps(reading.elements[-SIDE_COUNT:])
    if left == 0 and right == 0:
        return None
    return (left, right)

# --- end control law ---


qtr = QTRLineSensor()
qtr.load_calibration("/qtr.cal")

left_motor = ST3032Motor(servo_id=2, uart_id=1, tx=14, rx=41,
                         invert=True)
right_motor = ST3032Motor(servo_id=1, uart_id=1, tx=14, rx=41)
db = DriveBase(left_motor, right_motor,
               wheel_diameter_mm=88, axle_track_mm=136)

print("aligning on the line ...")
while True:
    speeds = seek_wheel_speeds(qtr.read())
    if speeds is None:
        break
    db.move_wheels(speeds[0], speeds[1])
    time.sleep_ms(5)

while True:
    speeds = edge_wheel_speeds(qtr.read())
    if speeds is None:
        break
    db.move_wheels(speeds[0], speeds[1])
    time.sleep_ms(5)
db.stop(then="brake")
print("aligned - square on the edge")

Square up on a line (two color sensors)

The same maneuver with a corner-mounted color sensor per side, for rigs without the QTR bar.

# SPDX-License-Identifier: MIT
"""
Demo: square the robot up on a dark line using two colour sensors.

The classic FLL/WRO "align on a line" move: drive slowly toward a
black line with one colour sensor mounted near each front corner,
ahead of the wheels. The moment a sensor crosses onto the line its
wheel brakes — the other wheel keeps rolling, pivoting the chassis
until *its* sensor reaches the line too. When both have stopped, the
sensor pair (and therefore the chassis) is parallel to the line, no
matter how crooked the approach was.

Geometry matters: the sensors must sit ahead of the axle and be
mounted symmetrically. The farther apart they are, the more accurate
the final heading.

Line detection reuses the idea from ``color_array.py``: a black line
on a light mat is simply "too dark to be the mat" — ``ambient()``
below a threshold. Print ``sensor.ambient()`` over your own mat and
line and put ``LINE_AMBIENT`` between the two readings.

Hardware (same bus layout as ``color_array.py`` / ``full_robot.py``):
    * ESP32-S3 (I2C on 15/16; serial bus UART on 14/6)
    * 2x ST-3032 wheel servos, IDs 1 (left) / 2 (right)
    * TCA9548A mux, one TCS34725 per channel: 0 = left, 1 = right,
      both facing the mat at the front of the chassis
"""

from machine import I2C, Pin
from openbricks.tools import wait

from openbricks.drivers.st3032 import ST3032Motor
from openbricks.drivers.tca9548a import TCA9548A
from openbricks.drivers.tcs34725 import TCS34725
from openbricks.robotics import DriveBase


left_motor = ST3032Motor(servo_id=2, uart_id=1, tx=14, rx=41)
right_motor = ST3032Motor(servo_id=1, uart_id=1, tx=14, rx=41, invert=True)
db = DriveBase(left_motor, right_motor,
               wheel_diameter_mm=88, axle_track_mm=136)

i2c = I2C(0, sda=Pin(15), scl=Pin(16), freq=400_000)
mux = TCA9548A(i2c)
left_sensor = TCS34725(mux[1])
right_sensor = TCS34725(mux[0])


LINE_AMBIENT = 20


def align_on_line():

    approach_dps = 100
    poll_ms = 10
    timeout_ms = 8000

    left_done = False
    right_done = False
    left_ambient = None
    right_ambient = None
    speeds = [approach_dps, approach_dps]
    db.move_wheels(speeds[0], speeds[1])
    try:
        for _ in range(max(1, timeout_ms // poll_ms)):
            if not left_done:
                left_ambient = left_sensor.ambient()
                if left_ambient < LINE_AMBIENT:
                    speeds[0] = 0
                    db.move_wheels(speeds[0], speeds[1])
                    left_done = True
                    print('left wheel stopped (ambient=%d)' % left_ambient)
            if not right_done:
                right_ambient = right_sensor.ambient()
                if right_ambient < LINE_AMBIENT:
                    speeds[1] = 0
                    db.move_wheels(speeds[0], speeds[1])
                    right_done = True
                    print('right wheel stopped (ambient=%d)' % right_ambient)

            if left_done and right_done:
                return
            wait(poll_ms)
    finally:
        db.stop(then="brake")
    raise RuntimeError(
        "no line found within %d ms (last ambient: left=%r right=%r) — "
        "is the line in reach, and is LINE_AMBIENT calibrated for "
        "your mat?" % (timeout_ms, left_ambient, right_ambient))


def main():

    print("aligning on the line ...")
    align_on_line()
    print("aligned — square to the line.")
    wait(500)
    left_motor.coast()
    right_motor.coast()


if __name__ == "__main__":
    main()

Full robot (drivebase + IMU + color sensor array)

# SPDX-License-Identifier: MIT
"""
Example: a small robot that rolls forward until its colour sensor sees red,
then demos a servo wave.

Hardware:
    * ESP32-S3 DevKitC-1 (or classic ESP32 DevKitC-V4)
    * 2× JGB37-520 DC gear motors (with Hall-effect quadrature encoders)
        driven by a shared L298N (or TB6612FNG) H-bridge
    * 1× BNO055 9-DOF IMU on I2C
    * 1× TCS34725 RGB colour sensor on the same I2C bus
    * 1× ST-3215 serial bus servo on UART (optional — the servo demo
        at the bottom just prints a message if it isn't attached)

Edit the GPIOs at the top to match your wiring.
"""

import time

from machine import I2C, Pin, UART

from openbricks.drivers.bno055 import BNO055
from openbricks.drivers.jgb37_520 import JGB37Motor
from openbricks.drivers.st3215 import ST3215
from openbricks.drivers.tcs34725 import TCS34725
from openbricks.robotics import DriveBase


I2C_SDA  = 15
I2C_SCL  = 16

LEFT_IN1,  LEFT_IN2,  LEFT_PWM  = 4, 5, 6
LEFT_EA,   LEFT_EB               = 7, 8

RIGHT_IN1, RIGHT_IN2, RIGHT_PWM = 9, 10, 11
RIGHT_EA,  RIGHT_EB              = 12, 13

SERVO_TX, SERVO_RX = 17, 18
SERVO_ID = 1

WHEEL_DIAMETER_MM = 56
AXLE_TRACK_MM     = 114


i2c   = I2C(0, sda=Pin(I2C_SDA), scl=Pin(I2C_SCL), freq=400_000)
imu   = BNO055(i2c)
color = TCS34725(i2c)

left  = JGB37Motor(in1=LEFT_IN1,  in2=LEFT_IN2,  pwm=LEFT_PWM,
                   encoder_a=LEFT_EA,  encoder_b=LEFT_EB)
right = JGB37Motor(in1=RIGHT_IN1, in2=RIGHT_IN2, pwm=RIGHT_PWM,
                   encoder_a=RIGHT_EA, encoder_b=RIGHT_EB)

drivebase = DriveBase(left, right,
                      wheel_diameter_mm=WHEEL_DIAMETER_MM,
                      axle_track_mm=AXLE_TRACK_MM)

try:
    uart = UART(1, baudrate=1_000_000, tx=SERVO_TX, rx=SERVO_RX)
    arm  = ST3215(uart, servo_id=SERVO_ID)
except Exception as e:
    print("no servo attached:", e)
    arm = None


while True:
    r, g, b = color.rgb()
    heading = imu.heading()

    print("rgb=({:3d},{:3d},{:3d})  heading={:6.1f}".format(r, g, b, heading))

    if r > g and r > b and r > 120:
        print("Red detected — stopping.")
        drivebase.stop()
        break

    drivebase.drive(speed_mm_s=150, turn_rate_dps=0)
    time.sleep_ms(50)

if arm is not None:
    arm.move_to(180, speed=500)
    time.sleep_ms(500)
    arm.move_to(0, speed=500)