RobotContainer & Bindings

beginner25 min

Overview

RobotContainer is the central hub of command-based programming. It creates subsystems, configures default commands, and binds controller inputs to commands.

RobotContainer Responsibilities

This class:

  • Creates Subsystems: Instantiate all subsystem objects.
  • Sets Default Commands: Define what happens when no command is running.
  • Binds Controls: Connect buttons/triggers to commands.
  • Provides Auto Commands: Return autonomous routines.

Everything here is configuration. No loop logic belongs in RobotContainer — behaviour lives in commands.

Basic RobotContainer

package frc.robot;

import edu.wpi.first.wpilibj2.command.button.CommandXboxController;
import frc.robot.subsystems.intake.Intake;

public class RobotContainer {
    Intake intake = new Intake();
    private final CommandXboxController controller = new CommandXboxController(0);

    public RobotContainer() {
        configureButtonBindings();
    }

    private void configureButtonBindings() {
        intake.setDefaultCommand(intake.setIntakeCommand(0));

        controller.a().whileTrue(intake.setIntakeCommand(0.6));
    }
}

Bindings call the subsystem’s own command factories. intake.setIntakeCommand(0.6) already requires the intake, so there is nothing to declare and nothing to forget.

Button Bindings

CommandXboxController — and CommandPS4Controller / CommandPS5Controller for other hardware — expose each button as a Trigger:

Trigger methodRuns when
whileTrue(cmd)While held; cancelled on release
onTrue(cmd)Once on press; runs to completion
onFalse(cmd)Once on release
toggleOnTrue(cmd)Press starts it, press again cancels
// Run intake while A is held
controller.a().whileTrue(intake.setIntakeCommand(0.6));

// Eject while B is held
controller.b().whileTrue(intake.setIntakeCommand(-0.4));

// Toggle with X
controller.x().toggleOnTrue(intake.setIntakeCommand(0.6));

// D-pad
controller.povDown().whileTrue(intake.intakeDeployCommand());
controller.povUp().whileTrue(intake.intakeHomeCommand());

Composing Triggers

Triggers combine with and, or, and negate, which is how one button becomes a modifier for another:

// R2 intakes — but only when R1 and triangle are not held
controller.R2().and(controller.R1().negate()).and(controller.triangle().negate())
    .whileTrue(intake.deployAndIntakeCommand());

R2 alone intakes; R2 with R1 held does something else entirely. No if statement in any periodic method.

You can also bind several commands to one trigger, each requiring different subsystems:

controller.R1().whileTrue(new SmartShootCommand(drive, shooter, transfer, conveyor))
    .whileTrue(DriveCommands.aimAtTargetCommand(drive, shooter,
        () -> -controller.getLeftY(),
        () -> -controller.getLeftX()));

A Trigger is not limited to buttons — any BooleanSupplier works, so sensor conditions bind the same way:

new Trigger(intake::hasGamePiece).onTrue(leds.flashGreenCommand());

Default Commands

Default commands run when no other command requires the subsystem. Set them with subsystem.setDefaultCommand(command).

// Stop when nothing else is running
intake.setDefaultCommand(intake.setIntakeCommand(0));
shooter.setDefaultCommand(shooter.setDutyCycleSetpointCommand(0));

// The drivetrain's "resting state" is driver control
drive.setDefaultCommand(
    DriveCommands.joystickDrive(
        drive,
        () -> -controller.getLeftY(),
        () -> -controller.getLeftX(),
        () -> controller.getRightX()));

Default commands are how mechanisms stop

Release the trigger, the command is unscheduled, and the scheduler immediately starts the default command — which commands zero.

Without one, the last value stays applied and the roller keeps spinning. Every mechanism that can be left running needs a default command.

Note the joystick axes are passed as suppliers, not values. A value read once at binding time would be frozen forever; the supplier is read fresh every loop. The negations are because joysticks report forward as negative.

Constructing Subsystems

Subsystems that use an IO layer are constructed differently depending on the runtime mode, and RobotContainer is the only place that makes that choice:

switch (Constants.currentMode) {
    case REAL:
        drive = new Drive(new GyroIOPigeon2(), new ModuleIOTalonFX(/* ... */));
        break;
    case SIM:
        drive = new Drive(new GyroIO() {}, new ModuleIOSim(/* ... */));
        break;
    default: // REPLAY
        drive = new Drive(new GyroIO() {}, new ModuleIO() {});
        break;
}

Everything else — subsystems that talk to hardware directly — is constructed once, unconditionally.

The Auto Chooser

private final LoggedDashboardChooser<Command> autoChooser;

public RobotContainer() {
    registerNamedCommands();
    autoChooser = new LoggedDashboardChooser<>("Auto Choices", AutoBuilder.buildAutoChooser());

    autoChooser.addOption(
        "Drive Wheel Radius Characterization", DriveCommands.wheelRadiusCharacterization(drive));

    configureButtonBindings();
}

public Command getAutonomousCommand() {
    return autoChooser.get();
}

LoggedDashboardChooser is AdvantageKit’s drop-in replacement for SendableChooser. It behaves identically on the dashboard but records the selection into the log, so a replay picks the same auto the robot ran. With a plain SendableChooser, that choice is invisible afterwards.

Adding characterization routines as chooser options is a convenient way to run them from the dashboard without a separate code path. See Tunable Constants and Characterization.

Common mistakes

  • Reading a joystick axis into a variable instead of passing a supplier.
  • Runtime logic in RobotContainer. It configures; it does not decide.
  • A missing default command, so a mechanism never stops.
  • Building the auto chooser before registering named commands. PathPlanner resolves the names at build time.
  • Two bindings whose commands require the same subsystem, so pressing one silently cancels the other.

Resources