Unit 6 · Lesson 7

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 Trigger from any boolean supplier — including sensor values
  • Use CommandXboxController to 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

MethodWhen it firesWhat it does to the commandBest 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 trueSchedules 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 edgeFirst press schedules; second press cancels.On/off toggles: enable a mode, lock a mechanism
.toggleOnFalse(cmd)Falling edgeSame 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.

Trigger binding behavior — press and hold each button
.onTrue()
Waiting for press…
.whileTrue()
Waiting for press…
.toggleOnTrue()
State: OFF — press to start
.onFalse()
Waiting for release…
Press any button above to see the scheduler effect described here.

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.

RobotContainer.java — CommandXboxController and custom Triggers
// Modern approach: CommandXboxController returns Trigger objects directly
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());
}
← click a highlighted token
🔍 LRI Perspective: "Sensor-based Triggers are where bugs hide at competition"

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.

🔌 System Check — Trigger Verification

Before competition, run through every binding while watching Shuffleboard Scheduler:

  • For each onTrue binding: 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 whileTrue binding: press and hold. Verify the command name appears. Release. Verify it disappears and the motor/mechanism has stopped.
  • For each toggleOnTrue binding: 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?

  • A .onTrue() — schedules the command when A is first pressed.
  • B .whileTrue() — schedules the command when A is pressed and automatically cancels it (calling end(true)) when A is released. The hold duration determines how long the command runs.
  • C .onFalse() — runs the intake when the button is released.
  • D .toggleOnTrue() — starts on press and requires a second press to stop.

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?

  • A No visible effect — the Trigger only fires once per match.
  • B The LED flash command is scheduled and interrupted repeatedly in rapid succession — potentially dozens of times per second — causing the LED to flicker erratically and the scheduler to spend significant time managing the spurious commands.
  • C The scheduler detects rapid firing and automatically debounces the trigger.
  • D The command runs normally — onTrue() only responds to the first true value, ignoring subsequent firings.

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?

  • A .onTrue() — first press extends, but a second press re-schedules the same command, causing undefined behavior.
  • B .toggleOnTrue() — first press schedules the command (extend). Second press cancels it, calling end(true) on the extend command (retract in end). Third press reschedules it again (extend again).
  • C .whileTrue() — holds the climber extended while Y is held, retracts when released.
  • D .toggleOnTrue() — but it only works for exactly two presses. The third press has no effect.
💪 Practice Prompt

Full Trigger Suite

  1. Migrate your RobotContainer from XboxController + JoystickButton to CommandXboxController. Verify all existing bindings still work. Note how much cleaner the named method access is: m_operator.a() vs new JoystickButton(m_operator, XboxController.Button.kA.value).
  2. Add a toggleOnTrue binding 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.
  3. 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.
  4. 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.
  5. 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.