Command Triggers¶
Triggers are the primary way to start commands in response to external events, such as a button press, a sensor value reaching a threshold, or a specific robot state. Triggers are checked (or “polled”) at the start of every Scheduler.run() method call ( How the Scheduler Works goes over the specific phases in more detail).
Triggers cache their signal when they are polled. Calling getAsBoolean() reads the most recently polled value, not necessarily the live value of the underlying button or sensor at that exact instant. This is what lets triggers reliably detect rising and falling edges within a scheduler cycle and have consistency in telemetry.
Creating Triggers¶
A Trigger is created by providing a BooleanSupplier (a lambda function that returns true or false) or by combining existing triggers.
// A trigger for a gamepad button
Trigger button = xboxController.a();
// A trigger for a limit switch
Trigger atTop = new Trigger(topLimitSwitch::get);
// A trigger for a complex condition by combining two triggers
Trigger isReady = new Trigger(turret::isAtTarget).and(shooter::isAtSpeed);
Trigger Bindings¶
Once you have a trigger, you can bind commands to it. There are several types of bindings that determine how the command responds to changes in the trigger’s state.
State-Based Bindings¶
Triggers implement the Java BooleanSupplier interface, making them compatible with any method that accepts a boolean condition, such as Coroutine.waitUntil. The binding methods below schedule or cancel commands based on the trigger’s cached signal and its previous cached signal.
onTrue(Command): Schedules the command when the trigger transitions fromfalsetotrue(a rising edge). The command runs until it finishes or is interrupted, even if the trigger signal becomesfalse.onFalse(Command): Schedules the command when the trigger transitions fromtruetofalse(a falling edge). The command runs until it finishes or is interrupted, even if the trigger signal becomestrue.whileTrue(Command): Schedules the command on a rising edge and cancels it on a falling edge. If the command stops while the trigger is stilltrue, it is not restarted.whileFalse(Command): Schedules the command on a falling edge and cancels it on a rising edge. If the command stops while the trigger is stillfalse, it is not restarted.
Use onTrue and onFalse for commands that should run until they finish and use whileTrue and whileFalse for commands whose lifetime should be tied to the signal. For example, “move while the bumper is held” is a whileTrue binding, while “start an intake sequence when the bumper is pressed” is usually an onTrue binding.
Note
Most triggers for operator control should use whileTrue. This lets drivers easily halt a command just by letting go of the button it’s bound to.
Continuous/Retry Bindings¶
retryWhileTrue(Command): LikewhileTrue, but if the command finishes while the trigger is stilltrue, it is immediately restarted.retryWhileFalse(Command): LikewhileFalse, but restarts if the command finishes while the trigger is stillfalse.
Retry bindings continuously attempt to schedule their command while the signal remains in the requested state. If the command ends naturally, it will be started again. If it was interrupted by another same-priority command that requires the same mechanism, the retry binding may immediately schedule it again and interrupt the would-be interrupter. Use retry bindings when repeated attempts are intentional, not as a default replacement for whileTrue.
Retry bindings are particularly useful for suspend-like behavior when combined with command priorities. Multiple retry bindings for the same mechanism can be created at once; higher priority commands will take precedence over lower priority ones. This approach can be simpler than a state machine when states don’t interact with each other.
For example, if a robot has an LED strip to indicate robot state and error conditions to the drivers or spectators, the commands for indicating more severe errors can have higher priorities than those for less severe errors. The triggers and bindings for coordinating the LED state can be configured in a single command:
Command ledControl = leds.run(coroutine -> {
// Highest severity error: continually set the comms warning while the DS is disconnected.
new Trigger(RobotState::isDSAttached).retryWhileFalse(leds.flashCommsWarning());
// Medium severity error: continually set the intake warning while the intake is broken.
// If interrupted by the comms warning, it will resume when DS connection is restored.
new Trigger(intake::isJammed).retryWhileTrue(leds.flashIntakeWarning());
// Lowest priority, not even an error: continually indicate the alliance color.
// NOTE: Because ledControl is the default command, we can't set a command-scoped
// default command; it'll override the external setting and immediately cancel ledControl.
new Trigger(() -> true).retryWhileTrue(leds.showAllianceColor());
// All the logic is in the triggers. Park the coroutine to keep the command
// alive, but there's nothing to do otherwise.
coroutine.park();
}).named("LED Control");
leds.setDefaultCommand(ledControl);
Toggle Bindings¶
toggleOnTrue(Command): Schedules the command on afalsetotruetransition, and cancels it on the nextfalsetotruetransition.toggleOnFalse(Command): Schedules the command on atruetofalsetransition, and cancels it on the nexttruetofalsetransition.
Toggle bindings are best for operator controls where the driver explicitly switches a behavior on and off. They are usually a poor fit for safety behavior because the command’s lifetime depends on remembering how many edges have occurred.
Multi-Press Bindings¶
The multiPress(int, Time) modifier allows commands to be bound when a trigger signal has had a minimum number of rising edges within a specific time period.
For example, driverController.a().multiPress(2, Seconds.of(1.5)) will go high when there have been at least two rising edges on the driver’s “A” button within the last 1.5 seconds, and will go low when there are fewer. This can be used with onTrue to respond to a double-press. The multi-press trigger remains high as long as enough presses are still inside the time window; it is not only high on the final button press.
Combining Triggers¶
Triggers can be combined using standard boolean operators to create more complex conditions. These operations create new trigger objects and do not modify the originals.
Trigger.and(BooleanSupplier): High when both signals are high.Trigger.or(BooleanSupplier): High when either signal is high.Trigger.negate(): High when the original signal is low.
Trigger bothButtons = buttonA.and(buttonB);
Trigger eitherButton = buttonA.or(buttonB);
Trigger notButton = buttonA.negate();
Modifying Trigger Behavior¶
You can also modify how a trigger responds to the underlying condition:
debounce(Time duration): Creates a trigger that only becomestrueif the original condition istruefor at least the specified duration.risingEdge(): Creates a trigger that is onlytruefor a single loop cycle when the original condition transitions fromfalsetotrue.fallingEdge(): Creates a trigger that is onlytruefor a single loop cycle when the original condition transitions fromtruetofalse.
Warning
Because risingEdge() and fallingEdge() are only high for a single scheduler cycle, bind commands to them with onTrue. A whileTrue binding on a one-cycle edge trigger will schedule the command and then cancel it on the next cycle.
Scopes¶
Trigger bindings exist in scopes. When a scope exits, any trigger binding that was created in that scope will be removed and the commands attached to that binding will be canceled. This is a critical safety feature of the library to prevent commands being triggered at unexpected moments.
Warning
A scoped trigger is only polled while its scope is active. If a scoped trigger is cached and referenced outside the scope where it was created, it will always return the value it had at the moment its scope exited.
Global Scope: Bindings created in the
Robotconstructor or methods called by it. These are always active.OpMode Scope: Bindings created while a specific OpMode is running. These are automatically removed when the OpMode ends.
Command Scope: Bindings created inside a running command. These are removed when the command finishes or is canceled.
Note
See Scopes for more information about scopes.
import first.robot.mechanisms.*;
import org.wpilib.framework.OpModeRobot;
import org.wpilib.command3.button.RobotModeTriggers;
public class Robot extends OpModeRobot {
public final Arm arm = new Arm();
public final Intake intake = new Intake();
public final Drive drive = new Drive();
public final LEDs leds = new LEDs();
public final CommandGamepad driverController = new CommandGamepad(1);
public Robot() {
// GLOBAL SCOPE: This binding is always active
driverController.a().onTrue(arm.up());
}
}
// OPMODE SCOPE: A trigger binding made in an opmode is only active while that opmode is running.
@Autonomous
public class ExampleAuto implements OpMode {
public ExampleAuto(Robot robot) {
new Trigger(RobotState::isFMSAttached).whileFalse(robot.leds.flashCommsWarning());
}
}
// COMMAND SCOPE: A trigger binding inside a command is only active while that command is running.
public Command sweepAndScore() {
return Command.noRequirements(coroutine -> {
// This binding only exists while the 'sweepAndScore' command is running
intakeTrigger.onTrue(intake.intakeOnce());
coroutine.await(drive.followPath("SweepPath"));
}).named("Sweep and Score");
}
Game Controller Triggers¶
The org.wpilib.command3.button package provides specialized classes for creating triggers from game controllers, such as Xbox and PS5 (DualSense) controllers. These classes provide methods that return Trigger objects for every button, d-pad direction, and trigger on the controller. Prefer these named methods over raw button numbers when possible; the resulting robot code is easier to read and easier to audit during an event.
For a full list of available controller classes and their methods, see the org.wpilib.command3.button Javadoc.
Advanced Controller Triggers¶
You can also use axis values (like the analog sticks or analog triggers) to create triggers by using the axisGreaterThan, axisLessThan, or axisMagnitudeGreaterThan methods, or by providing a custom BooleanSupplier. Axis-based triggers usually need a threshold and sometimes a debounce so small joystick noise does not repeatedly schedule and cancel commands.
// Trigger when the left Y axis is pushed more than 50% forward
Trigger highThrottle = new Trigger(() -> driverController.getLeftY() > 0.5).debounce(Milliseconds.of(200));
highThrottle.onTrue(Command.noRequirements(_ -> System.out.println("High throttle!")).named("High Throttle Print"));