BBR Digital Expander Documentation

Python API summary#

The everyday tier, which is what most projects need. Every method validates its arguments and, on failure, raises an exception naming the problem rather than returning a plausible-looking wrong number.

Getting the device#

from bbr_digital_expander import BBRDigitalExpander

expander = BBRDigitalExpander()                    # 0x38 on /dev/i2c-1
expander = BBRDigitalExpander(0x39)                # jumpered address
expander = BBRDigitalExpander(bus="/dev/i2c-3")    # another adapter

with BBRDigitalExpander(bus=1) as expander:        # begin() on entry, close() on exit
    ...

Errors#

Exception Raised when
BBRError Base class — catch this for “anything went wrong”
TransportError The device did not answer, or answered short
WrongDeviceError / ProtocolMismatchError / UnknownVariantError begin() refused
BadArgumentError A port, channel, slot or value was out of range (also a ValueError)
CommandFailedError The firmware ran the command and refused it; .command_result says why
NoImuError / ImuFaultError No IMU fitted, or one fitted and not answering
WrongSensorTypeError / WrongChannelModeError That port or channel is the other kind
LocalizerNotRunningError A pose was read before the localizer was ready

Every one carries .message and .status, a Status value you can branch on.

Failed reads serve the last known good value. When a snapshot read cannot reach the device — or, for the localizer, when three reads in a row fail their checksum — the driver returns the previous snapshot and is_data_fresh() goes false, rather than raising. A bus being torn down, or a burst of noise, should not kill a running program. Fail-loud survives where it matters: a device that has never answered raises TransportError, and a streak still failing after half a second (5+ reads) raises BBRError. The FTC driver differs here: by default it logs that streak and keeps running, because ending an OpMode mid-match costs more than stale data that isDataFresh() already flags.

Method Returns
is_data_fresh() Did the last snapshot read actually reach the device

Encoders#

Method Returns
encoder_count(channel) Signed accumulated counts
encoder_velocity(channel) Signed counts per second, computed on the board
pulse_width_us(channel) Pulse width in µs (PULSE_WIDTH mode only)
reset_encoder(channel) —
reset_all_encoders() —

Sensors#

Method Returns
sensor_connected(port) bool
sensor_type(port) SensorType.COLOR, DISTANCE, EMPTY or UNKNOWN
color_class(port) 0 for no match, else the colour slot 1–7
sees_color(port, class_slot) bool
distance_mm(port) Distance in mm, or math.inf when nothing is in range

Teaching and triggers#

All of these save to flash automatically, which is why they are setup calls: run them once, never in a loop. See Set up once.

Method Effect
teach_color(port, class_slot) Learn the colour currently in view
trigger_on_color(output, port, class_slot) Output high while that colour is seen
trigger_when_near(output, port, max_mm) Output high while something is within range
trigger_when_encoder_past(output, channel, counts) Output high past a threshold
trigger_when_facing(output, heading_deg, tolerance_deg) Output high while facing that heading
disable_output(output) Stop driving that output at all
output_state() / output_latched() Bit per output, read back over I2C
set_encoder_direction(channel, direction) FORWARD or REVERSE for one channel
clear_output_latch(output) Un-latch one output
clear_all_output_latches() Un-latch all four

Heading and pose#

These raise rather than return a heading or pose the board cannot stand behind.

Method Returns
heading() Degrees CCW-positive; raises if the gyro cannot be trusted
reset_heading() —
calibrate_gyro() bool — False if the robot moved; the previous bias is kept (~1 s, keep it still)
read_imu() ImuState
get_pose() Pose — mm, radians, +X forward, +Y left
set_pose(x_mm, y_mm, heading_rad) —
read_localizer() LocalizerState — raises unless RUNNING
read_localizer_raw() LocalizerState — any state, check it yourself
wait_for_localizer_ready(timeout_ms) —
reset_localizer_and_calibrate_imu() bool — False if the robot moved; the pose still resets and the localizer still starts
set_localizer_params(params) / get_localizer_params() LocalizerParams

Mounting and direction#

Set once when the board is bolted on. Unlike set_encoder_direction() above, nothing here auto-saves — follow with save_config_to_flash().

Method Effect
get_encoder_direction(channel) Which way that channel counts
set_encoder_invert_mask(mask) All four at once; bit per channel, set = inverted
get_encoder_invert_mask() Current mask
set_imu_axis_up(axis) Which board axis points up (AxisUp, flat is AxisUp.POS_Z)
get_imu_axis_up() Current setting

Board identity#

Member Returns
capabilities / has_capability(bit) Capability bits (regmap.CAP_*)
is_odometry_variant Is an IMU fitted
firmware_version (major, minor, patch)
protocol_minor / hardware_variant Version numbers
device_status() regmap.STATUS_* bits
is_config_dirty() Are there unsaved config changes
print_device_info() / device_info_lines() The whole identity block, human-readable

Blocks and raw access#

Method Returns
read_telemetry() Telemetry — everything in one snapshot
telemetry_max_age_ms Cache window behind the everyday getters (default 10)
invalidate_telemetry() Force the next getter to read the device
encoder_pin_state() Live A/B input levels — a wiring diagnostic
read_registers(reg, length) / write_registers(reg, data) Raw block access
read_register(reg) / write_register(reg, value) One byte
run_command(opcode, arg, max_ms) The command protocol, done correctly

Advanced tier#

The full register map is available underneath: configure_output(), write_color_class(), write_distance_class(), set_channel_mode(), set_pwm_channel_params(), set_pwm_wrap_enabled(), set_localizer_params(), read_telemetry(), run_command().

Nothing in the advanced tier saves automatically. Call save_config_to_flash(), or the configuration is gone at power-off. is_config_dirty() tells you whether you have unsaved changes.

Behaviour worth knowing#

begin() refuses loudly. A wrong device ID, an unsupported protocol version or an unrecognised hardware variant raises rather than producing corrupt data later.

One transaction per snapshot. The everyday getters share a snapshot refreshed roughly every 10 ms, so four questions in one loop iteration cost one transaction and the answers are mutually consistent.

Block reads are raw I2C, not SMBus. The 96-byte telemetry block does not fit an SMBus block transfer’s 32-byte cap, so the driver uses i2c_rdwr messages instead. Pass chunk_size= if some adapter you are using cannot manage a long transfer; the register pointer is still written only once, so the whole block stays inside a single firmware snapshot either way.

Commands are idempotent. Every command carries a token, so a bus retry cannot execute it twice.

Config writes are verified by readback. The board never NAKs for addressing reasons — an out-of-range write is silently discarded — so the driver writes, reads back and compares.

IMU access raises when the gyro cannot be trusted rather than returning zeros, because a heading of 0.00 that never changes is indistinguishable from working software.

The localizer block is checked with CRC16. I2C acknowledges bytes without verifying them, so a noise-flipped bit would otherwise arrive looking like a valid pose. A mismatch is re-read, then raised.

A uniform block is treated as a failed transfer. A read that comes back all 0x00 or all 0xFF never came from a live board — every block the driver guards this way contains a timestamp, a CRC or a status byte with reserved bits. It is the reason a bus-level fault surfaces as an exception rather than as wrong numbers.