Binding Commands to Triggers
The Trigger system is the edge-detection, event-driven, and sensor-reactive heart of Command-Based's TeleOp layer. Choosing the right binding method — onTrue, whileTrue, toggleOnTrue, or a custom sensor trigger — determines how the robot feels to drive and how robust it is under competition pressure.
By the end of this lesson, you will:
- Use all five core Trigger binding methods correctly:
onTrue,whileTrue,onFalse,toggleOnTrue,toggleOnFalse - Create a custom
Triggerfrom any boolean supplier — including sensor values - Use
CommandXboxControllerto access named trigger-based inputs without raw button numbers - Combine triggers with logical operators:
.and(),.or(),.negate() - Choose the correct binding method for hold-to-run, fire-once, toggle, and sensor-driven patterns
What a Trigger Actually Is
In WPILib, a Trigger is an object that wraps a boolean supplier — any condition that evaluates to true or false. The scheduler polls that condition every loop and fires events when its value changes.
A button is the most common trigger, but it's not special. A trigger can represent a beam break sensor, a gyro angle, a timer condition, a combination of buttons, or any custom logic you can express as a boolean. The binding methods are identical regardless of the trigger's source.
The Five Binding Methods
| Method | When it fires | What it does to the command | Best for |
|---|---|---|---|
| .onTrue(cmd) | Rising edge (false → true) | Schedules once. Command runs to completion or interruption. | Fire-once on press: eject, shoot, toggle state |
| .whileTrue(cmd) | While condition is true | Schedules when true, cancels when false. | Hold-to-run: intake, slow mode, assist features |
| .onFalse(cmd) | Falling edge (true → false) | Schedules once when condition becomes false. | Run on button release; cleanup after whileTrue |
| .toggleOnTrue(cmd) | Rising edge | First press schedules; second press cancels. | On/off toggles: enable a mode, lock a mechanism |
| .toggleOnFalse(cmd) | Falling edge | Same as toggleOnTrue but on release. | Rarely used; niche toggle scenarios |
Interactive Binding Simulator
Press and hold the button for each binding mode to see what happens to command scheduling.
CommandXboxController: The Modern Approach
In Lesson 4, we used XboxController with raw button numbers and JoystickButton wrappers. WPILib provides a cleaner alternative: CommandXboxController. It exposes each button and axis directly as a Trigger object, letting you chain binding methods without wrapping.
private final CommandXboxController m_driver = new CommandXboxController(0);
private final CommandXboxController m_operator = new CommandXboxController(1);
private void configureButtonBindings() {
// Clean named-button access — no raw numbers
m_driver.start()
.onTrue(Commands.runOnce(() -> m_gyro.reset(), m_gyro));
m_operator.a()
.whileTrue(new IntakeCommand(m_intake).withTimeout(3.0));
m_operator.y()
.onTrue(new ShootCommand(m_shooter).onlyIf(m_shooter::atTargetRPM));
// Trigger from a sensor — not a button at all
Trigger hasPieceTrigger = new Trigger(m_intake::hasGamePiece);
hasPieceTrigger.onTrue(
Commands.runOnce(() -> m_led.setGreen(), m_led));
// Trigger from trigger axis — leftTrigger() returns a Trigger too
m_operator.leftTrigger(0.5)
.whileTrue(new SlowModeCommand(m_drive));
// Logical composition: A AND B simultaneously
m_driver.a().and(m_driver.b())
.onTrue(new EmergencyStopCommand());
}
The most failure-prone bindings I've seen are sensor-based Triggers that fire at the wrong time. A beam break that vibrates during robot movement can cause spurious game piece detection. A gyro angle trigger with no hysteresis can oscillate on and off. The rules for sensor Triggers: (1) test the sensor reading in isolation before wiring it to a command, (2) add dead zones or debounce with .debounce(0.1), (3) always add a name with .withName() so you can see it in the Scheduler widget. Sensors that misbehave will fire your commands at exactly the wrong moment.
Before competition, run through every binding while watching Shuffleboard Scheduler:
- For each
onTruebinding: press the button once, verify the command name appears and then disappears when it completes. Press again. Verify again. It should not get "stuck." - For each
whileTruebinding: press and hold. Verify the command name appears. Release. Verify it disappears and the motor/mechanism has stopped. - For each
toggleOnTruebinding: first press — command appears. Second press — command disappears. Third press — command reappears. The toggle state should persist across other actions. - For sensor-based Triggers: watch the Scheduler widget while handling the mechanism physically. Confirm the trigger fires exactly once on the relevant event, not multiple times. Add
.debounce(0.05)if you see rapid toggling.
Knowledge Check
1. The operator wants to run the intake continuously while holding A, then stop when they release. Which binding method is correct and why?
2. You create a sensor Trigger: new Trigger(m_intake::hasGamePiece).onTrue(new FlashLEDCommand(m_led)). During driving, the beam break occasionally flickers on and off rapidly. What symptom will you see?
3. You want a climber to extend when Y is pressed and retract when Y is pressed again — a toggle. Which method is correct, and what does it do on the third press?
Full Trigger Suite
- Migrate your
RobotContainerfromXboxController+JoystickButtontoCommandXboxController. Verify all existing bindings still work. Note how much cleaner the named method access is:m_operator.a()vsnew JoystickButton(m_operator, XboxController.Button.kA.value). - Add a
toggleOnTruebinding for a mechanism that should latch: a climber lock, a brake mode toggle, or a slow-mode latch. Confirm the toggle state in the Scheduler widget: command present on odd presses, absent on even presses. - Create a sensor-based Trigger from
m_intake.hasGamePiece(). Bind it with.onTrue()to flash an LED or print a message. Test with the beam break physically blocked and unblocked. Then add.debounce(0.1)and repeat the test — confirm spurious triggers are suppressed. - Use trigger composition: bind a command to fire only when both the left bumper AND the A button are pressed simultaneously:
m_driver.leftBumper().and(m_driver.a()).onTrue(...). Verify in the Scheduler widget that neither button alone triggers the command. - Stretch goal: Implement a trigger that fires when the drivetrain encoder distance exceeds 36 inches:
new Trigger(() -> m_drive.getDistanceInches() > 36). Bind it to a stop command. Drive 3 feet and confirm the robot stops automatically. This is the seed of sensor-based autonomous control in TeleOp.