Troubleshooting#
The board isn’t found at all#
“Not a BBR Digital Expander: DEVICE_ID …” — something answered at the address, but it wasn’t an Expander. Check you picked the right I2C bus in the robot configuration, and that nothing else on the bus is at address 0x38.
Device not found in hardwareMap — the name in your code doesn’t match the name in the robot configuration. The examples use expander.
Configuration doesn’t list “BBR Digital Expander” — the driver folder isn’t in your project, or the project didn’t rebuild. Confirm com/buildingblockrobotics/expander/ is under TeamCode/src/main/java/ and rebuild.
If you used this board before it was renamed, your old robot configuration entry will no longer resolve. Delete it and add “BBR Digital Expander” fresh.
begin() returning false prints a reason through lastErrorText(). The three you will actually see:
“the expander did not acknowledge” — nothing answered at that address. In order of likelihood:
- No bus pull-ups. The board has none, and neither does your Arduino unless you fitted them. See Wiring it up. This is the single most common cause on a non-Hub host.
- Wrong address. The jumpers select
0x38–0x3B; the constructor defaults to0x38. - No power, or not enough of it. The board needs 3.3 V at about 55 mA.
- SDA and SCL swapped.
“not a BBR Digital Expander” — something answered, but it isn’t this board. Check for an address clash with another device on the same bus.
“firmware speaks a protocol major this driver does not” — update whichever of the two is older. See Updating the firmware.
An I2C scanner sketch is a good next step: it separates “nothing on the bus at all” (pull-ups, power, wiring) from “something is there at an address I didn’t expect” (jumpers, clash).
begin() raises, and the exception says which of these it is.
TransportError: “could not open I2C bus” — the bus device is not there, or you cannot reach it:
- I2C is not enabled.
sudo raspi-config nonint do_i2c 0, then reboot. - Permissions.
sudo usermod -aG i2c "$USER", then log out and back in. Running as root works too, but do not build a habit of it. - Wrong adapter number.
bus=1is the header on every current Pi; a Pi Zero or a software-I2C overlay may be a different number.ls /dev/i2c-*lists what you have.
TransportError: “the expander did not acknowledge” — the bus opened, nothing answered at that address:
- Wrong address. The jumpers select
0x38–0x3B; the driver defaults to0x38. - No power, or not enough of it. 3.3 V at about 55 mA, from header pin 1.
- SDA and SCL swapped, or no shared ground.
WrongDeviceError — something answered, but it isn’t this board. Check for an address clash with another device on the same bus.
ProtocolMismatchError — update whichever of the driver and the firmware is older. See Updating the firmware.
i2cdetect -y 1 is the fastest next step: it separates “nothing on the bus at all” (power, wiring, I2C not enabled) from “something is there at an address I didn’t expect” (jumpers, clash).
Reads work sometimes, then stop#
Almost always electrical rather than software.
- Pull-ups too weak. Internal microcontroller pull-ups are tens of kilohms — fine on a short jumper at 100 kHz, marginal at 400 kHz, and worse the longer the cable. Fit 2.2 kΩ–4.7 kΩ to 3.3 V.
- Try 100 kHz. If dropping
Wire.setClock()to100000fixes it, the problem is signal integrity, not the board. - Cable routing. Keep the I2C cable away from motor leads. This is the failure that shows up only when the robot is driving.
- Shared ground. The board and the host need one, and it needs to carry current back without a long thin wire in the way.
A sensor reads EMPTY#
The board genuinely cannot see a sensor on that port.
- Check the cable at both ends.
- Try the sensor on a different port — that separates a bad sensor from a bad port.
- Dump all four ports at once:
BBRSensorDumpTeston FTC, theSensorDumpexample on Arduino.
Teaching a colour refuses#
Working as intended. It refuses to store a colour that would never fire or always fire — too dark, saturated, or no sensor on the port.
- Too dark — more light, or hold the target closer.
- Saturated — less light, or move the target back.
- Then teach again.
A stored bad colour is much harder to debug than a refused teach, which is why it fails at the point where you’re standing there holding the target.
A digital output never goes high#
- Was the colour taught? A colour trigger refers to a slot that teaching has to have filled first.
- Is the sensor seeing it? Check the colour class returns the slot number you expect.
- Is it saved? The trigger helpers auto-save, but advanced-tier
configureOutput()does not. CheckisConfigDirty(). - Is the input side set up? On FTC the digital input needs its own entry in the robot configuration; on Arduino it needs
pinMode(pin, INPUT). - Is it wired to the pin you think? Output 0 is not necessarily the first pin on the connector.
A digital output flickers#
The condition is sitting right on the threshold. Use hysteresis rather than trying to be more precise:
- Distance: raise
hysteresisMmin a distance class. - Any output: raise
debounceAssert/debounceReleasein an output config.
Both are set to sensible defaults by the everyday helpers, so if you are seeing chatter you are probably on the advanced tier.
Heading or pose fails#
The board refuses to hand you a heading it cannot stand behind, rather than returning a plausible-looking 0.00. Read the message — it names which case you are in:
- This board has no IMU. It is the base variant. Heading and odometry need the odometry variant.
- The gyro is fitted but not responding. A hardware fault, or a board that lost power mid-transaction. Power-cycle; if it persists on every boot, the board needs looking at.
- The board stopped answering entirely. Check the I2C cable, then see The board isn’t found at all.
- The localizer was never started. Pose calls need the localizer reset first — heading alone does not.
A failure here is the driver working correctly. A heading of 0.00 that never changes is indistinguishable from working software right up until it matters.
Heading drifts#
- Calibrate the gyro with the robot completely still.
- Check the bias-valid flag — false means it hasn’t had a good calibration.
- Check the gyro-saturated flag — if the robot spun faster than the gyro can measure, the estimate is no longer trustworthy and needs a re-zero.
The pose is wrong#
- Pose calls fail unless the localizer is running — check the status first.
- Verify pod geometry and ticks-per-mm. The board cannot detect a wrong measurement; it just accumulates error.
- Check the pods stay in contact with the floor. Skipping loses counts permanently.
- Calibrate with the robot still.
The loop crawls, roughly one iteration per second#
A setup helper is being called inside your main loop with values that keep changing.
Teaching and the trigger helpers save to flash, and the board accepts only one save per second. Both drivers wait that limit out rather than failing, so each call costs up to a full second.
- Move the call into a setup routine, or behind a button press with edge detection so holding the button doesn’t re-fire it every loop.
- If the value genuinely has to change at runtime, use advanced-tier
configureOutput(), which changes the trigger in RAM without touching flash, and save only when you want it to persist.
Repeating a call with unchanged values is not the cause — the board recognises there is nothing new to store and skips the write. It is changing values every loop that costs you.
See Set up once for what this also does to your triggers while the board is writing.
Configuration lost after power-off#
Advanced-tier calls don’t auto-save. Call saveConfigToFlash(). isConfigDirty() tells you whether you have unsaved changes.
The everyday teach and trigger helpers save for you.
Platform-specific#
Readings freeze mid-match#
Check isDataFresh(). When a read can’t reach the device the driver serves the last known good value rather than throwing, so a stale reading looks like a working one that stopped changing.
A sustained streak of failures logs a warning to the Robot Controller log and keeps serving the old value, so the OpMode keeps running. Check isDataFresh() before acting on a reading. To make that streak throw instead while you track the fault down, call exp.setReadFailurePolicy(BBRDigitalExpander.ReadFailurePolicy.STOP_OPMODE).
Did the board reset, or did the bus drop out?#
On firmware 1.1.0 and later, readDiagnostics() answers it in one call. Read it when the OpMode starts and again after a dropout:
BBRDigitalExpander.Diagnostics d = exp.readDiagnostics();
telemetry.addData("last reset", d.resetCause);
telemetry.addData("resets since power-on", d.resetsSincePowerOn);
telemetry.addData("bus recoveries", d.i2cRecoveryCount);
telemetry.addData("bus arbitration lost", d.i2cArbitrationLostCount);
resetsSincePowerOnabove 0 withWATCHDOG: the firmware hung and restarted. The driver also logsexpander REBOOTED. On 1.1.0 bus noise could trigger this; update to 1.1.1, which fixes it.POWER_ONmid-match, with the Hub still running: the board lost power briefly. Check the I2C cable’s power pins and connectors.- No reset, but the bus counters climb: the board kept running and the I2C link itself was disturbed.
i2cArbitrationLostCountgoing up usually means electrical noise on the cable. Route the I2C cable away from motor and motor-controller wiring, and check whether the errors follow particular encoder cables (unplug them one at a time).
The Hub stops talking to the board until it is power-cycled#
If readings stop and only a robot power cycle brings them back, the Hub’s I2C port has given up after too many corrupted transfers. First update to 1.1.1, so the board itself never holds the bus. Then reduce what corrupts the transfers:
- Route the I2C cable away from motor and motor-controller wiring, and away from the encoder cables of the noisiest motors.
- Read less often. Every transaction is another chance to be hit by noise. Read once per loop, and only what you use.
- Watch the counters.
i2cArbitrationLostCountandi2cRecoveryCountfromreadDiagnostics()climbing while a mechanism runs points at that mechanism’s wiring.
Everything breaks when the OpMode stops#
Expected, and already handled. The SDK tears the I2C device down at stop, so a read already in flight comes back empty. The driver raises BBRTransportException for that case.
If you have a polling loop, catch it and break — the IMU and localizer examples show the pattern. It means “the read never reached the device”, not “something is broken”.
Every call returns false after one failure#
The library does not latch a failure — each call sets its own status. But if begin() failed, calls that need the device to have been identified fail with BBRStatus::NotInitialized until begin() succeeds. Check begin()’s return value and stop there rather than looping on it.
The sketch compiles but does nothing#
begin() needs Wire.begin() to have run first. Nothing enforces the order, and the symptom is a clean compile followed by a failed identity read.
Not enough RAM on an Uno#
BBRTelemetry is about 100 bytes and the block read uses a 96-byte buffer while it decodes, so a readTelemetry() call needs roughly 200 bytes of stack. On a 2 KB part that is affordable but not free — if you are already close to the limit, use the everyday getters, which keep one cached block rather than one per call site, and avoid keeping several BBRTelemetry copies alive at once.
Error text prints as garbage#
lastErrorText() returns a const __FlashStringHelper *, which Serial.println() handles correctly. Passing it somewhere expecting a char * prints the flash address instead of the string.
Occasional corrupt reads on a Pi, but the board is fine on a Hub#
Not something you should meet — a Pi 4 and a Pi 5 both run the whole driver at the stock bus rate with nothing added to config.txt — but here is the cause and the fix if you do.
The expander is an RP2040 in I2C target mode, so it stretches the clock while its firmware services a transfer, and the Pi’s hardware I2C controller has a long-standing bug handling clock stretching.
The driver catches the damage rather than passing it on — a uniform block raises TransportError and a corrupted localizer block raises CrcMismatchError after three tries — so the symptom is intermittent exceptions rather than silently wrong numbers.
Fix it at the bus, in /boot/firmware/config.txt:
dtparam=i2c_arm_baudrate=50000, then reboot. This is usually enough.- Or switch to software I2C, which has no such bug:
dtoverlay=i2c-gpio,i2c_gpio_sda=23,i2c_gpio_scl=24, then pass that adapter’s number asbus=.
Every call raises NotInitializedError#
begin() has not succeeded. The with form runs it for you and raises on the way in, which is why the examples use it — a bare constructor does not talk to the board at all.
A loop that reads several values is slower than expected#
Each everyday getter serves from one cached telemetry block, refreshed when it is older than telemetry_max_age_ms (10 ms by default). If your loop runs faster than that and you want a fresh block each pass, call invalidate_telemetry() — or read read_telemetry() once and use the snapshot, which is what sensor_dump.py does.
smbus2 is not installed#
pip install smbus2. On a system-managed Python (Bookworm and later) either use a virtual environment or sudo apt install python3-smbus2.