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.