498
Control Lab FRC Programming Curriculum
Robot Code · L07 of 12

Triggers and Bindings

Prereqs: robot-code-06
Objectives 0 / 4

Hook

You bind a command to a button. Simple. But then the operator asks: “Can the intake only run when we’re not at full extension?” Now you need a button press AND a sensor condition. With raw JoystickButton, that logic bleeds into your command. With Trigger, you compose it in one line.

Trigger is WPILib’s answer to the question: “what, precisely, causes a command to start or stop?” Once you understand it, controller bindings become a small, readable table in RobotContainer instead of a maze of if-statements scattered across commands.

Core concept

Key Concept

A Trigger is any boolean condition — a button press, a sensor reading, a calculated state — that can start, stop, or toggle commands. CommandXboxController exposes every button and axis as a ready-made Trigger, and you can build your own from any BooleanSupplier. Triggers are composed and wired in RobotContainer; they never live inside a subsystem or command.

The Trigger class

edu.wpi.first.wpilibj2.command.button.Trigger wraps a single BooleanSupplier. The scheduler polls it every loop iteration (every 20 ms) and fires edge-detection events:

MethodFires when
onTrue(cmd)condition transitions false → true
onFalse(cmd)condition transitions true → false
whileTrue(cmd)schedules while condition is true; cancels on false
whileFalse(cmd)schedules while condition is false; cancels on true
toggleOnTrue(cmd)alternates schedule/cancel each false→true edge
toggleOnFalse(cmd)alternates schedule/cancel each true→false edge

CommandXboxController

edu.wpi.first.wpilibj2.command.button.CommandXboxController is the command-based wrapper around a standard Xbox controller. Every button is already a Trigger:

private final CommandXboxController driver = new CommandXboxController(0);

// Each of these returns a Trigger object:
driver.a()             // A button
driver.b()             // B button
driver.x()             // X button
driver.y()             // Y button
driver.leftBumper()    // Left bumper
driver.rightBumper()   // Right bumper
driver.leftTrigger()   // Left trigger axis > 0.5 (default threshold)
driver.rightTrigger(0.3) // Right trigger axis > 0.3 (custom threshold)
driver.leftStick()     // Left stick click
driver.povUp()         // D-pad up
Note

CommandXboxController is not the same as XboxController. The older XboxController returns raw doubles and booleans. CommandXboxController returns Trigger objects — use this one in command-based projects. We cover axes (for driving) in Lesson 08.

Binding examples

All bindings live in RobotContainer.configureBindings():

private void configureBindings() {
    // Run intake while A is held, stop when released
    driver.a().whileTrue(new IntakeCommand(intake));

    // Deploy arm once when B is pressed
    driver.b().onTrue(new ArmToPosition(arm, ArmConstants.kDeployAngle));

    // Retract arm once when B is released
    driver.b().onFalse(new ArmToPosition(arm, ArmConstants.kStowAngle));

    // Toggle a climber lock on/off with Y
    driver.y().toggleOnTrue(new LockClimberCommand(climber));

    // Fire shooter only when right trigger is pulled past 30%
    driver.rightTrigger(0.3).whileTrue(new ShootCommand(shooter));
}
⚠ Heads up

onTrue schedules a command on the rising edge only — one press, one schedule. If you hold the button, the command is not re-scheduled unless it finishes and the button is pressed again. Use whileTrue if you want the command to restart when interrupted.

Solenoid toggle with toggleOnTrue

A common pattern is toggling a pneumatic solenoid (e.g., a gripper):

// In RobotContainer
private final GripperSubsystem gripper = new GripperSubsystem();

private void configureBindings() {
    // Each press of left bumper flips the gripper state
    driver.leftBumper().toggleOnTrue(
        Commands.startEnd(
            gripper::close,   // runs on schedule
            gripper::open,    // runs on cancel
            gripper
        )
    );
}
// GripperSubsystem.java
public class GripperSubsystem extends SubsystemBase {
    private final DoubleSolenoid solenoid =
        new DoubleSolenoid(PneumaticsModuleType.CTREPCM, 0, 1);

    public void close() {
        solenoid.set(DoubleSolenoid.Value.kForward);
    }

    public void open() {
        solenoid.set(DoubleSolenoid.Value.kReverse);
    }
}

Custom Triggers from BooleanSupplier

Any BooleanSupplier can become a Trigger. This is powerful: you can trigger commands from sensor readings, calculated state, or any boolean expression.

// Trigger that fires when the proximity sensor detects a game piece
Trigger hasPiece = new Trigger(intakeSubsystem::hasPiece);
hasPiece.onTrue(new RumbleCommand(driver, 0.5, 0.25));  // haptic feedback

// Trigger from a lambda — fires when arm is above 60 degrees
Trigger armHigh = new Trigger(() -> arm.getAngle() > 60.0);
armHigh.whileTrue(new ExtendClimber(climber));

// Trigger from a NetworkTables entry (e.g., vision target locked)
Trigger targetLocked = new Trigger(() ->
    SmartDashboard.getBoolean("Vision/targetLocked", false));
targetLocked.whileTrue(new AutoAim(shooter, vision));

Composing Triggers

Triggers support boolean composition via and(), or(), and negate(). Each returns a new Trigger.

// Button AND sensor condition
Trigger canShoot = driver.rightBumper().and(shooter::isUpToSpeed);
canShoot.whileTrue(new FeedCommand(feeder));

// Either left OR right bumper runs the intake
Trigger eitherBumper = driver.leftBumper().or(driver.rightBumper());
eitherBumper.whileTrue(new IntakeCommand(intake));

// Run reverse intake when B is held but A is NOT also held
Trigger reverseOnly = driver.b().and(driver.a().negate());
reverseOnly.whileTrue(new ReverseIntakeCommand(intake));
Note

Composition creates a new Trigger at construction time — no runtime overhead. The scheduler polls the composed supplier just like any other trigger.

Full RobotContainer example

public class RobotContainer {
    private final DriveSubsystem drive = new DriveSubsystem();
    private final IntakeSubsystem intake = new IntakeSubsystem();
    private final ShooterSubsystem shooter = new ShooterSubsystem();

    private final CommandXboxController driver   = new CommandXboxController(0);
    private final CommandXboxController operator = new CommandXboxController(1);

    public RobotContainer() {
        drive.setDefaultCommand(
            new ArcadeDriveCommand(drive,
                () -> -driver.getLeftY(),
                () ->  driver.getRightX())
        );
        configureBindings();
    }

    private void configureBindings() {
        // Intake runs while right trigger pulled; reverse on left bumper
        driver.rightTrigger(0.2).whileTrue(new IntakeCommand(intake));
        driver.leftBumper().whileTrue(new ReverseIntakeCommand(intake));

        // Shoot only when both operator holds A AND shooter is at speed
        Trigger readyToShoot = operator.a().and(shooter::isAtTargetRpm);
        readyToShoot.whileTrue(new FeedShooterCommand(intake, shooter));

        // Spin up shooter whenever operator holds right trigger
        operator.rightTrigger(0.1).whileTrue(new SpinUpShooterCommand(shooter));

        // Haptic feedback when game piece detected
        new Trigger(intake::hasPiece)
            .onTrue(new RumbleCommand(driver, 0.6, 0.3));
    }
}

Interactive demo

Code Tracer
01driver.a() polled → false// no change
02driver.a() polled → false// still nothing
03driver presses A// edge detected!
04onTrue → schedule IntakeCommand// command starts
05driver.a() polled → true (held)// no new edge
06driver releases A// falling edge
07whileTrue cancel → IntakeCommand// command stops
State
buttonreleased
triggerEdgenone
commandStateidle

Step through to see values update.

Initial state
⚡ Check your understanding

You want a command to run continuously while a button is held and stop immediately when released. Which binding do you use?

⚡ Check your understanding

What does new Trigger(() -> arm.getAngle() > 60.0) do?

Key takeaways

  • Trigger wraps any BooleanSupplier and fires edge-based events each scheduler loop.
  • CommandXboxController exposes every button as a ready-made Trigger — prefer it over raw XboxController in command-based code.
  • Choose the binding method by behavior: onTrue (one shot on press), whileTrue (hold), toggleOnTrue (flip-flop), onFalse (on release).
  • Build custom triggers from sensor readings or lambdas — no subclassing needed.
  • Compose triggers with and(), or(), negate() to express multi-condition logic cleanly in RobotContainer.

Common confusions

“I used onTrue but the command runs forever.” onTrue schedules the command once and does not cancel it. If your command’s isFinished() never returns true, it runs indefinitely. Either fix isFinished(), or switch to whileTrue if you want the button release to cancel it.

“toggleOnTrue isn’t toggling — it just stays on.” Make sure the command you’re toggling actually declares a requirement with addRequirements(). Without a requirement, the scheduler can’t track it as “already running” and can’t cancel it on the next toggle.

“My composed trigger fires unexpectedly.” Trigger composition is evaluated once per scheduler loop. If shooter::isAtTargetRpm has jitter (bounces around the threshold), the composed trigger may fire multiple times. Add hysteresis in your subsystem’s isAtTargetRpm() method — e.g., use a debounce or a 5 RPM dead-band around the target.

“Can I bind the same button to two commands?” Yes — each call to .onTrue(), .whileTrue(), etc. on the same trigger adds an independent binding. Both commands will be scheduled when the condition fires. Just make sure they don’t require the same subsystem, or the second will cancel the first.

Challenge

⚡ Try it yourself

Implement a Trigger that fires when both joystick axes (left Y and right X) are within ±0.05 of center simultaneously — a “hands off” trigger you might use to switch to a default idle command.

Code EditorJavaCtrl+Enter to run
Stuck? Show hint

Math.abs(value) <= threshold checks whether a value is within ±threshold of zero. AND both conditions together.

What’s next

In Lesson 08: Driver Input, we go beyond buttons and look at axis values — how to read joystick positions, apply deadbands, shape the response curve, and structure a full arcade or tank drive command.