Digital outputs and triggers#
This is the feature that makes the Expander different from “some more sensor ports”.
The idea#
You configure a condition once. The board watches for it continuously, at a speed your OpMode loop cannot match, and reports the answer on a pin. Your match code reads that pin as an ordinary digital input.
The result: no I2C traffic, no BBR driver on the hot path, no loop cost, and the condition keeps being enforced even if your code crashes or never runs.
Setting one up#
Write a setup OpMode. Run it once. It never needs to run again.
@TeleOp(name = "BBR: Setup colour trigger")
public class BBRSetupTrigger extends LinearOpMode {
@Override
public void runOpMode() {
BBRDigitalExpander exp = hardwareMap.get(BBRDigitalExpander.class, "expander");
waitForStart();
exp.teachColor(0, 1); // teach the target colour
exp.triggerOnColor(0, 0, 1); // output 0 follows it
telemetry.addLine("Saved. This OpMode never needs to run again.");
telemetry.update();
sleep(3000);
}
}
Then your match code:
DigitalChannel overTarget = hardwareMap.get(DigitalChannel.class, "over_target");
while (opModeIsActive()) {
if (overTarget.getState()) {
// do the thing
}
}
That match OpMode does not import anything from com.botbuilders. It doesn’t know the Expander exists.
The four helpers#
exp.triggerOnColor(output, sensorPort, classSlot); // while a taught colour is seen
exp.triggerWhenNear(output, sensorPort, maxMm); // while something is within range
exp.triggerWhenEncoderPast(output, channel, counts); // once an encoder passes a threshold
exp.triggerWhenFacing(output, headingDeg, toleranceDeg); // while facing a heading
All four take an output number 0–3, apply sensible debounce and hysteresis for you, and save to flash automatically. See Heading (IMU) for what triggerWhenFacing does at the ±180 seam.
Set up once — never every loop#
Saving to flash is what makes a trigger survive power-off. It is also why these helpers belong in a setup OpMode you run once, or behind a button press, and never in your match loop.
Calling one repeatedly with the same values is harmless. The board compares the new configuration against what it already holds and, finding no difference, does nothing at all — no write, no delay.
Calling one repeatedly with changing values — a threshold following a stick, a heading re-armed every loop — is a different story. Three things go wrong, in the order you will notice them:
Your OpMode slows to a crawl. The board accepts at most one real save per second and rejects anything sooner. The driver waits that limit out rather than failing, so every call blocks your loop for up to a full second. A loop that should run at 50 Hz runs at 1 Hz.
The board stops sensing while it writes. A save halts sampling for up to 500 ms. During that window outputs hold their last state, debounce counters stop, and anything that passes a sensor is never seen — including in LATCHED mode, which is exactly the case you chose latching to catch. Nothing on the pin indicates this happened.
Eventually the flash wears out. Endurance is roughly 100,000 erase cycles per sector, and the board alternates two sectors, so about 200,000 saves. At the one-per-second ceiling that is around 55 hours of continuous saving. Not a number you hit by accident, but well within a season of a robot left enabled on the bench with a save in the loop.
Keep teachColor() and every triggerOn… / triggerWhen… helper out of your match loop. Run them in a setup OpMode, or on a button press with edge detection so holding the button doesn’t re-fire them.
Changing a trigger during a match#
If a threshold genuinely has to move at runtime, use the advanced tier, which does not auto-save:
BBRDigitalExpander.OutputConfig out = new BBRDigitalExpander.OutputConfig();
out.source = BBRDigitalExpander.OutputSource.ENCODER;
out.srcIndex = 0;
out.threshMin = liftTarget;
out.threshMax = Integer.MAX_VALUE;
exp.configureOutput(0, out); // RAM only: no flash write, no stall
configureOutput() changes what the board is watching for immediately, costs one I2C transaction, and touches flash not at all. Call saveConfigToFlash() separately, once, when you actually want the value to outlive the power cycle — at the end of a tuning session, not every loop.
Latching#
By default an output follows its condition — high while true, low while false. Sometimes you want the opposite: catch a brief event and hold it until you’re ready to deal with it.
That is LATCHED mode, from the advanced tier:
BBRDigitalExpander.OutputConfig out = new BBRDigitalExpander.OutputConfig();
out.source = BBRDigitalExpander.OutputSource.SENSOR_CLASS;
out.srcIndex = 0; // sensor port
out.classIndex = 1; // colour slot
out.mode = BBRDigitalExpander.OutputMode.LATCHED;
exp.configureOutput(0, out);
exp.saveConfigToFlash();
Once latched, the output stays high until you clear it:
exp.clearOutputLatch(0);
exp.clearAllOutputLatches();
This catches things your loop would otherwise miss — a line of tape flashing past under the robot at speed, for instance, which might be true for less than one loop iteration.
BBRColorLatchExample shows this working.
One pin per colour#
With four outputs you can watch four conditions at once. The usual pattern for a three-colour season is one pin per colour:
exp.teachColor(0, 1);
exp.teachColor(0, 2);
exp.teachColor(0, 3);
exp.triggerOnColor(0, 0, 1); // output 0 = colour 1
exp.triggerOnColor(1, 0, 2); // output 1 = colour 2
exp.triggerOnColor(2, 0, 3); // output 2 = colour 3
Your code then reads three digital inputs and knows exactly which colour is underneath, with no I2C at all. BBRThreeColorExample walks through the whole thing.
Advanced options#
OutputConfig exposes the rest: activeLow to invert the pin, pulseMs for a fixed-length pulse rather than a level, debounceAssert / debounceRelease to tune how many consistent readings are needed before the pin moves, and threshMin / threshMax for two-sided encoder or heading windows (heading in degrees × 100).
Nothing in the advanced tier auto-saves. Call saveConfigToFlash() or lose it at power-off.