BBR Digital Expander Documentation

Arduino API summary#

The everyday tier, which is what most projects need. Every method validates its arguments and, on failure, sets an error you can read rather than returning a plausible-looking wrong number.

Nothing here throws and nothing allocates.

Getting the device#

#include <BBRDigitalExpander.h>
#include <Wire.h>

BBRDigitalExpander expander;                          // 0x38 on Wire
BBRDigitalExpander expander(0x39);                    // jumpered address
BBRDigitalExpander expander(BBR_I2C_ADDR_DEFAULT, Wire1);   // second bus

Wire.begin();
expander.begin();   // false if it is not there, or not what it should be

Errors#

Method Returns
ok() Did the most recent call succeed
lastStatus() A BBRStatus — Ok, Transport, NoImu, BadArgument, …
lastErrorText() A flash-resident sentence, printable straight to Serial
lastCommandResult() The firmware’s error code from the last failed command

Encoders#

Method Returns
encoderCount(channel) Signed accumulated counts
encoderVelocity(channel) Signed counts per second, computed on the board
pulseWidthUs(channel) Pulse width in µs (PULSE_WIDTH mode only)
resetEncoder(channel) bool
resetAllEncoders() bool

Sensors#

Method Returns
sensorConnected(port) bool
sensorType(port) BBRSensorType::Color, Distance, Empty or Unknown
colorClass(port) 0 for no match, else the colour slot 1–7
seesColor(port, classSlot) bool
distanceMm(port) Distance in mm, or INFINITY 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, or on a button press, never in a loop. See Set up once.

Method Effect
teachColor(port, classSlot) Learn the colour currently in view
triggerOnColor(output, port, classSlot) Output high while that colour is seen
triggerWhenNear(output, port, maxMm) Output high while something is within range
triggerWhenEncoderPast(output, channel, counts) Output high past a threshold
triggerWhenFacing(output, headingDeg, toleranceDeg) Output high while facing that heading
disableOutput(output) Stop driving that output at all
outputState() / outputLatched() Bit per output, read back over I2C
setEncoderDirection(channel, direction) Forward or Reverse for one channel
clearOutputLatch(output) Un-latch one output
clearAllOutputLatches() Un-latch all four

Heading and pose#

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

Method Returns
heading() Degrees CCW-positive, or NAN if the gyro cannot be trusted
resetHeading() bool
calibrateGyro() bool (robot must be still, ~1 s)
readImu(BBRImuState&) bool
getPose(BBRPose&) bool — mm, radians, +X forward, +Y left
setPose(xMm, yMm, headingRad) bool
readLocalizer(BBRLocalizerState&) bool — fails unless RUNNING
readLocalizerRaw(BBRLocalizerState&) bool — any state, check it yourself
waitForLocalizerReady(timeoutMs) bool
resetLocalizerAndCalibrateImu() bool
setLocalizerParams(...) / getLocalizerParams(...) bool

Mounting and direction#

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

Method Effect
getEncoderDirection(channel, out) Which way that channel counts
setEncoderInvertMask(mask) All four at once; bit per channel, set = inverted
getEncoderInvertMask() Current mask, or -1 on a failed read
setImuAxisUp(axis) Which board axis points up (BBR_AXIS_*, flat is BBR_AXIS_POS_Z)
getImuAxisUp() Current setting, or -1 on a failed read

Board identity#

Method Returns
capabilities() / hasCapability(bit) Capability bits (BBR_CAP_*)
isOdometryVariant() Is an IMU fitted
protocolMinor() / hardwareVariant() Version numbers
deviceStatus() BBR_STATUS_* bits
isConfigDirty() Are there unsaved config changes
printDeviceInfo(Serial) The whole identity block, human-readable

Blocks and raw access#

Method Returns
readTelemetry(BBRTelemetry&) Everything in one snapshot
setTelemetryMaxAgeMs(ms) Cache window behind the everyday getters (default 10)
invalidateTelemetry() Force the next getter to read the device
encoderPinState() Live A/B input levels — a wiring diagnostic
readRegisters(reg, buf, len) / writeRegisters(reg, data, len) Raw block access
readRegister(reg) / writeRegister(reg, value) One byte; read returns -1 on failure
runCommand(opcode, arg, maxMs) The command protocol, done correctly

Advanced tier#

The full register map is available underneath: configureOutput(), writeColorClass(), writeDistanceClass(), setChannelMode(), setPwmChannelParams(), setPwmWrapEnabled(), setLocalizerParams(), readTelemetry(), runCommand().

Nothing in the advanced tier saves automatically. Call saveConfigToFlash(), or the configuration is gone at power-off. isConfigDirty() 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 returns false 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.

Big reads survive a small Wire buffer. The 96-byte telemetry block does not fit an Uno’s 32-byte buffer, so it arrives in chunks — but the register pointer is written only once and the chunks continue from it, which is what keeps the whole block inside a single firmware snapshot. Nothing tears across the seam.

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 library writes, reads back and compares.

IMU access fails 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 reported.