BBR Digital Expander Documentation

Your first script#

This reads a colour sensor on port 0 and prints what it sees. It is about as small as a useful Expander script gets.

import time

from bbr_digital_expander import BBRDigitalExpander, BBRError

try:
    with BBRDigitalExpander(bus=1) as expander:
        print(f"sensor on port 0: {expander.sensor_type(0).name}")

        while True:
            print(f"colour class {expander.color_class(0)}  "
                  f"encoder 0 {expander.encoder_count(0)}")
            time.sleep(0.1)
except KeyboardInterrupt:
    pass
except BBRError as exc:
    print(f"expander: {exc}")

You should see what kind of sensor is on port 0 before anything else happens. If it reports EMPTY, the sensor isn’t detected — check the connection.

Teaching it a colour#

color_class() returns 0 until you have taught the board what to look for. Teaching takes one call, and the result is saved to the board’s flash automatically:

expander.teach_color(0, 1)   # what the sensor sees right now is "colour 1"

Run that once, with the target held in front of the sensor, and from then on color_class(0) returns 1 whenever it sees that colour again — including after a power cycle, and including from a completely different script or a different host entirely.

There is a full walkthrough in Colour sensors.

Errors are exceptions#

Everything that can fail raises a subclass of BBRError, carrying a sentence that names the failure and usually the fix:

from bbr_digital_expander import BBRError, NoImuError, WrongSensorTypeError

try:
    mm = expander.distance_mm(0)
except WrongSensorTypeError:
    print("that port has a colour sensor on it")
except BBRError as exc:
    print(exc)            # the sentence
    print(exc.status)     # a Status value you can branch on

Catch BBRError when you just want a message, and a specific subclass when you want to do something different about it. BadArgumentError is also a ValueError, because a port number out of range is a bug in your code rather than a fault in the hardware.

There are exactly two sentinels rather than exceptions, chosen so the obvious code is still correct when there is no answer:

Call When it cannot answer
distance_mm(port) math.inf, so distance_mm(0) < 300 is simply false
color_class(port) 0, the same as “none of the colours I know”

A heading is not one of them. heading() raises on a board with no IMU, or one whose IMU has stopped answering, because a heading of 0.00 that never changes is indistinguishable from working software.

How reads work#

Every everyday getter — encoders, sensors, colours, distances — is served from a snapshot the driver refreshes when it is older than 10 ms.

This matters because it means asking four questions in one loop iteration costs one I2C transaction, not four:

a = expander.encoder_count(0)
b = expander.encoder_count(1)
red = expander.sees_color(2, 1)
mm = expander.distance_mm(3)
# all four answers came from the same snapshot, one transaction

It also means those values are mutually consistent — they were all captured at the same instant by the board, not read one at a time while the robot moved.

You don’t have to manage this. Just call the getters. telemetry_max_age_ms changes the window if you want a faster or slower refresh, and invalidate_telemetry() forces the next getter to go to the device.

Which loop speed?#

The board samples continuously on its own, so your loop rate does not affect what it measures — only how often you look.

This is worth more on a Pi than on a microcontroller. Linux is not a real-time system: your process can be descheduled for tens of milliseconds at a time, and time.sleep(0.01) is a request rather than a promise. Because the board does the sampling, a late loop misses nothing — the next snapshot is still current, and encoder counts accumulated while you were away.

Where timing genuinely matters, use the triggers: the board watches the condition itself and drives a pin, and your loop cost drops to one GPIO read that the scheduler cannot make wrong. See Digital outputs and triggers.

What to do next#