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, 6
WHEEL_DIAMETER_MM = 88 # EDIT to your wheels
AXLE_TRACK_MM = 136 # EDIT to your chassis
# Geometry of the square. SIDE_MM 200 traces a 20 cm × 20 cm square —
# fits comfortably on a small mat. Bump for a larger arena.
SIDE_MM = 200
NUM_LAPS = 1
# Conservative chassis speeds for a small bench robot — well below
# the ST-3032 mechanical limit (datasheet no-load 888 dps at 12 V,
# loaded working point ~600 dps; see ``docs/datasheets/feetech_sts3032.pdf``).
# Dial down further if the chassis slips or the bus drops packets.
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()
Square up on a line (two color sensors)
# 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=6)
right_motor = ST3032Motor(servo_id=1, uart_id=1, tx=14, rx=6, invert=True)
# ``db.move_wheels(left, right)`` puts both setpoints in ONE
# sync-write packet, so the wheels change together. It replaces the
# SyncServoGroup this example used to build: adopting wheels into a
# DriveBase hands their UART to the native driver, and a
# SyncServoGroup writes through the MicroPython one — two drivers on
# a single wire. The chassis dimensions matter only to
# straight()/turn(); this routine steers by wheel speeds.
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])
# Below this ambient (0..100) the surface counts as the line. Between
# a typical mat (~30+) and a matte black line (~5-10); calibrate on
# your own surfaces.
# Retuned 5 -> 20 when the driver defaults moved to gain=16 /
# integration_ms=2.4: normalized ambient() reads ~4x higher
# (gain x4, full scale /10). 20 matches line_follow.py's
# threshold under the same configuration.
LINE_AMBIENT = 20
def align_on_line():
# Drive forward until each sensor sees the line; brake that side.
#
# No print() inside the poll loop: each one streams over the BLE
# console and stretches a 10 ms tick to many times that, so the
# wheel brakes long after its sensor crossed the line. Readings
# are collected and reported after each wheel stops instead.
approach_dps = 100
poll_ms = 10
timeout_ms = 8000
left_done = False
right_done = False
left_ambient = None
right_ambient = None
# Per-wheel speeds, updated as each side arrives. Zeroing one
# side stops that wheel while the other keeps creeping — same
# independent-stop behaviour as the old per-motor brake(), but
# every change still leaves in a single packet.
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:
# Whatever ends the loop — success, timeout, Ctrl-C — no
# wheel keeps creeping. One call, both wheels.
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
# ----- wiring -----
# Pins below are for the ESP32-S3 DevKitC-1 (avoid GPIO 19/20 = USB,
# 26-37 = flash/PSRAM, 0/3/45/46 = strapping). On a classic ESP32
# DevKitC-V4 use I2C 21/22 and remap the rest to that board's pins.
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
# ----- init -----
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)
# Optional: a serial bus servo on a second UART.
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
# ----- main loop: drive until red -----
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)
# Wave the arm once if present.
if arm is not None:
arm.move_to(180, speed=500)
time.sleep_ms(500)
arm.move_to(0, speed=500)