Sensor drivers

All sensors construct against a machine.I2C bus (or a TCA9548A mux channel, which quacks the same) except the GPIO-driven HC-SR04.

from machine import I2C, Pin
from openbricks.drivers.bno055 import BNO055
from openbricks.drivers.tcs34725 import TCS34725
from openbricks.drivers.tca9548a import TCA9548A

i2c = I2C(0, sda=Pin(15), scl=Pin(16), freq=400_000)  # ESP32-S3 pins
mux = TCA9548A(i2c)

color = TCS34725(mux[0])          # two same-address sensors ...
color2 = TCS34725(mux[1])         # ... on separate mux channels
imu = BNO055(i2c=mux[3], address=0x29)

print(color.rgb())                # (r, g, b) each 0-255
print(color.ambient())            # clear channel, 0-100
print(imu.heading())              # degrees, CW-positive

ICM-45686 (raw IMU, hard-tick heading)

TDK ICM-45686 raw 6-axis IMU — the hard-tick heading source.

Unlike the BNO055 (fused heading over I2C, pumped from Python at ~50-100 Hz during moves), this part is read INSIDE the 1 kHz hard tick over SPI (~13 µs per burst): gyro-Z integrates into a continuous heading in C (imu_yaw_core — stationarity-gated bias learning, Pybricks-Prime architecture), and a gyro DriveBase consumes it every millisecond with no Python in the loop.

Wiring: 4 free GPIOs to the breakout’s SPI pins (SCLK/MOSI(SDI)/ MISO(SDO)/CS) — the I2C bus, mux and color sensors are untouched.

Example:

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

imu = ICM45686(sck=8, mosi=9, miso=17, cs=18)
left  = ST3032Motor(servo_id=2, uart_id=1, tx=14, rx=6, invert=True)
right = ST3032Motor(servo_id=1, uart_id=1, tx=14, rx=6)
db = DriveBase(left, right, wheel_diameter_mm=88,
               axle_track_mm=138, imu=imu)
db.use_gyro(True)      # heading now corrects at 1 kHz in C

Calibration: gyro bias learns automatically during stillness (fast at boot rest, slow tracking after). save_calibration() persists the learned bias to NVS; the next construction seeds from it (the pbio trick), so boot-and-immediately-run starts corrected.

class openbricks.drivers.icm45686.ICM45686(sck, mosi, miso, cs, hz=8_000_000, mode=3, scale=-1.0)[source]

Bases: object

heading()[source]

Continuous body heading in degrees, CW-positive — the hard-tick integrator’s value. Same contract as BNO055.heading() except unwrapped (multi-turn) rather than [-180, 180); DriveBase accepts both.

reset_heading()[source]

Zero the heading frame (calibration is kept).

gyro()[source]

(x, y, z) rates in dps from the last hard-tick sample.

acceleration()[source]

(x, y, z) in g from the last hard-tick sample.

calibrated()[source]

True once the bias estimator has locked (robot has been still for ~0.5 s since boot, or a saved calibration was loaded).

save_calibration()[source]

Persist the learned gyro bias to NVS so the next boot starts corrected instead of waiting for stillness.

stats()[source]

(reads_ok, read_errors, configured) — hard-tick health.

BNO055 (IMU)

BNO055 — re-export of the native BNO055 C type.

The implementation lives in native/user_c_modules/openbricks/bno055.c so the drivebase tick can read imu.heading() every ms (when use_gyro=True) without a Python frame on the hot path. This module exists so existing user imports stay the same:

from openbricks.drivers.bno055 import BNO055

The class itself is implemented in C (so the drivebase can read the heading on its 1 kHz tick); its Python-facing API:

class openbricks.drivers.bno055.BNO055(i2c, address=0x28)

Bosch BNO055 9-axis IMU in 6-DOF fusion mode (accelerometer + gyro; the magnetometer is deliberately unused — motor magnets and steel in floors bend the local field, and a drive robot only needs relative heading). Heading zeroes where the robot points at construction.

Parameters:
  • i2cmachine.I2C (or a mux channel).

  • address – 0x28, or 0x29 on breakouts whose ADR pin straps high.

heading()

Body heading in degrees, wrapped to [-180, 180). CW-positive: turning right (clockwise viewed from above) increases it — compass and Pybricks convention. This is what DriveBase(imu=...) reads.

euler()

(heading, roll, pitch) tuple in degrees.

angular_velocity()

(x, y, z) gyro rates in deg/s.

acceleration()

(x, y, z) accelerometer in m/s².

TCS34725 (color)

AMS TCS34725 RGB + clear light-to-digital sensor.

The TCS34725 returns four 16-bit channels (clear, red, green, blue) over I2C at address 0x29. There’s an onboard LED that we leave under user control — some breakout boards wire it to the LED pin on reset, others require GPIO control.

Reference: TCS34725 datasheet (AMS / ams-OSRAM), sections 2.4 and 3.

I2C command byte format (from datasheet):

bit 7 (CMD) = 1 (always for command byte) bits 6:5 (TYPE) = 01 (auto-increment) or 00 (single) bits 4:0 (ADDR) = register address

class openbricks.drivers.tcs34725.TCS34725(i2c, address=_ADDR, integration_ms=2.4, gain=16)[source]

Bases: ColorSensor

RGB + clear color sensor, fixed at I2C address 0x29.

Implements the ColorSensor contract: rgbc() raw 16-bit channels, reflection() and the calibrated helpers built on it. Two or more on one robot need a TCA9548A mux (the address is not configurable).

raw()[source]

Return the raw (clear, red, green, blue) 16-bit readings.

rgb()[source]

Return (r, g, b) scaled to 0..255 using the clear channel.

Dividing by the clear channel normalizes for ambient brightness, so a white object reports roughly (255, 255, 255) at any light level within the sensor’s range.

ambient()[source]

Return clear-channel brightness scaled to 0..100.

100 means the clear ADC is saturated for the configured integration time — 1024 counts per 2.4 ms cycle, capped at 65535 (datasheet “MAX COUNT”). Scale non-linearly would be nicer but keep it simple.

HC-SR04 (ultrasonic distance)

HC-SR04 ultrasonic distance sensor.

The HC-SR04 has two pins: trig (input — we drive it) and echo (output — it pulls high for as long as the round-trip echo took). Sequence:

  1. Drive trig high for ~10 µs.

  2. Read the duration of the resulting pulse on echo.

  3. distance_mm = pulse_us × speed_of_sound_mm_per_us / 2.

Speed of sound in air at room temperature is ~0.343 mm/µs, so a 60 cm target gives ~3.5 ms of echo pulse — well within the 30 ms default timeout.

This driver is pure Python — there’s no closed-loop control on a range sensor, so the 1 kHz hot-path concern doesn’t apply. The machine.time_pulse_us builtin does the actual measurement; typical call latency is ~1 ms (pulse round-trip) which is fine for the cold-path use cases (line-following, wall avoidance, mission “approach until N mm” loops at <100 Hz).

class openbricks.drivers.hcsr04.HCSR04(trig, echo, timeout_us=_DEFAULT_TIMEOUT_US)[source]

Bases: DistanceSensor

HC-SR04 ultrasonic distance sensor.

Parameters:
  • trig – GPIO pin number wired to the sensor’s TRIG pin.

  • echo – GPIO pin number wired to ECHO.

  • timeout_us – how long to wait for the echo. time_pulse_us returns -1 past this; we map that to -1 from distance_mm() to mean “no return”.

distance_mm()[source]

Return the round-trip distance to the nearest reflector ahead, in millimetres, or -1 if no echo arrived inside timeout_us.

VL53L0X (laser distance)

ST VL53L0X laser time-of-flight distance sensor.

The VL53L0X talks I2C at default address 0x29. A measurement cycle:

  1. Write 0x01 to SYSRANGE_START (register 0x00).

  2. Poll RESULT_INTERRUPT_STATUS (0x13) bit 0 until set (typical 33 ms at 33 Hz default rate).

  3. Read RESULT_RANGE_STATUS + 10 (0x14 + 10 = 0x1E) as a 16-bit big-endian integer — the raw distance in millimetres.

  4. Write 0x01 to SYSTEM_INTERRUPT_CLEAR (0x0B).

Out of range / blocked targets show as 8190 mm in the raw register; we surface that as -1 to match the DistanceSensor contract.

This driver implements the minimum register sequence to get usable single-shot readings on a power-on-default VL53L0X. ST’s reference API ships an extensive calibration / VHV / measurement-budget setup that improves accuracy on tuned hardware — that’s a follow-up patch when we have boards to validate against. The default-init ranging here is what most off-the-shelf MicroPython VL53L0X drivers use, and is good for the WRO use cases (line-of-sight, 30–1500 mm).

class openbricks.drivers.vl53l0x.VL53L0X(i2c, address=_DEFAULT_ADDR, timeout_ms=200)[source]

Bases: DistanceSensor

ST VL53L0X laser time-of-flight distance sensor.

Parameters:
  • i2c – a machine.I2C (or compatible) instance.

  • address – 7-bit I2C address (default 0x29). XSHUT-strapping multiple sensors onto one bus requires assigning each a unique address before constructing — outside this driver’s scope.

  • timeout_ms – how long to poll for a measurement to finish. Default 200 ms; the chip is typically done in 33–50 ms.

distance_mm()[source]

Distance ahead in millimetres; -1 if no echo / out of range.

VL53L1X (laser distance, long range)

ST VL53L1X laser time-of-flight distance sensor.

Successor to the VL53L0X with 4 m range (vs 2 m on the L0X) and a different register map. I2C default address 0x29 (same as L0X — XSHUT strapping needed if both share a bus).

The chip-ID lives at 16-bit register 0x010F and reads back 0xEACC for a VL53L1X (or 0xEBAA for VL53L4CD, an L1X-pin-compatible variant).

Like the L0X driver this module ships the minimum register sequence to get usable single-shot ranging on a power-on-default chip. ST’s reference API does an extensive calibration / VHV / SPAD-array selection pass for tuned operation; that lives behind a separate calibrate() method we’ll add when we have hardware to validate against.

Measurement cycle:

  1. Write 0x40 to SYSTEM_INTERRUPT_CLEAR (16-bit reg 0x0086).

  2. Write 0x40 to SYSTEM_MODE_START (16-bit reg 0x0087) — kicks a single-shot ranging.

  3. Poll GPIO_TIO_HV_STATUS (16-bit reg 0x0031) bit 0 until clear.

  4. Read RESULT_FINAL_CROSSTALK_CORRECTED_RANGE_MM_SD0 (16-bit reg 0x0096) as a 16-bit big-endian value — distance in mm.

  5. Write 0x01 to SYSTEM_INTERRUPT_CLEAR (16-bit reg 0x0086).

VL53L1X registers are 16-bit (vs L0X’s 8-bit) — addresses go through two byte-swaps on the I2C wire. The driver wraps that in _read_u8_16 / _write_u8_16 helpers.

class openbricks.drivers.vl53l1x.VL53L1X(i2c, address=_DEFAULT_ADDR, timeout_ms=200)[source]

Bases: DistanceSensor

ST VL53L1X laser time-of-flight distance sensor (4 m range).

Parameters:
  • i2c – a machine.I2C (or compatible) instance.

  • address – 7-bit I2C address (default 0x29).

  • timeout_ms – how long to poll for a measurement to finish. Default 200 ms; the chip is typically done in ~50 ms.

distance_mm()[source]

Distance ahead in millimetres; -1 if no echo / out of range.