Your first sketch#
This reads a colour sensor on port 0 and prints what it sees. It is about as small as a useful Expander sketch gets.
#include <BBRDigitalExpander.h>
#include <Wire.h>
BBRDigitalExpander expander;
void setup() {
Serial.begin(115200);
Wire.begin();
Wire.setClock(400000);
if (!expander.begin()) {
Serial.println(expander.lastErrorText());
while (true) { delay(1000); }
}
Serial.print(F("sensor on port 0: "));
Serial.println((int)expander.sensorType(0));
}
void loop() {
Serial.print(F("colour class "));
Serial.print(expander.colorClass(0));
Serial.print(F(" encoder 0 "));
Serial.println(expander.encoderCount(0));
delay(100);
}
Open the serial monitor at 115200 and 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#
colorClass() 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.teachColor(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 colorClass(0) returns 1 whenever it sees that colour again — including after a power cycle, and including from a completely different sketch or a different host entirely.
There is a full walkthrough in Colour sensors.
Errors, without exceptions#
Arduino builds disable exceptions on most cores, so nothing in this library throws. Calls that can fail return false, or a documented sentinel value, and leave the reason behind for you to read:
float mm = expander.distanceMm(0);
if (!expander.ok()) {
Serial.println(expander.lastErrorText()); // a sentence, usually with the fix in it
Serial.println((int)expander.lastStatus()); // a BBRStatus value you can branch on
}
The sentinels are chosen so the obvious code is still correct when there is no answer:
| Call | When it cannot answer |
|---|---|
distanceMm(port) |
INFINITY, so distanceMm(0) < 300 is simply false |
heading() |
NAN, which compares false against everything |
colorClass(port) |
0, the same as “none of the colours I know” |
anything returning bool |
false |
Error text lives in flash rather than RAM, so carrying all of it costs an Uno nothing it can’t spare.
How reads work#
Every everyday getter — encoders, sensors, colours, distances — is served from a snapshot the library 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:
int32_t a = expander.encoderCount(0);
int32_t b = expander.encoderCount(1);
bool red = expander.seesColor(2, 1);
float mm = expander.distanceMm(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. setTelemetryMaxAgeMs() changes the window if you want a faster or slower refresh, and invalidateTelemetry() 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. A 100 Hz loop reading the whole telemetry block at 400 kHz spends well under a millisecond per pass on I2C.
If you need less than that, use the triggers: the board watches the condition itself and drives a pin, and your loop cost drops to one digitalRead(). See Digital outputs and triggers.
What to do next#
- Four extra encoders → Encoders
- Detect a colour → Colour sensors
- Detect an object → Distance sensors
- Get the board to do the watching for you → Digital outputs and triggers