BBR Digital Expander Documentation

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 loop cannot match, and reports the answer on a pin. Your 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 routine — an OpMode, a sketch or a script. Run it once. It configures the board, and 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.buildingblockrobotics. 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 of them take an output number 0–3, apply sensible debounce and hysteresis for you, and save to flash automatically. See Heading (IMU) for what the heading trigger 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 routine you run once, or behind a button press, and never in your main 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 loop slows to a crawl. The board accepts at most one real save per second and rejects anything sooner. Both drivers wait that limit out rather than failing, so every call blocks 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 powered on the bench with a save in the loop.

Keep teachColor() and every triggerOn… / triggerWhen… helper out of your main loop. Run them in a setup routine, or on a button press with edge detection so holding the button doesn’t re-fire them.

Changing a trigger at runtime#

If a threshold genuinely has to move while the robot is running, 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. The question stops being “is it true right now?” and becomes “has it been true since I last looked?”, which is usually what you actually wanted to ask.

There is a clip of this running on a real board in Example videos.

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.

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.

Worked example#

BBRColorTriggerSetup, BBRColorLatchExample and BBRThreeColorExample cover setup, latching and the one-pin-per-colour pattern. See Example OpModes.