Unit 6 · Lesson 6

Operator Interface & RobotContainer

RobotContainer is the single file where the human meets the machine. Every subsystem is born here. Every controller is declared here. Every button-to-command binding is written here. This lesson turns it from a place you initialize things into a deliberate, documented wiring diagram your entire team can read.

By the end of this lesson, you will:

  • Explain the full anatomy of a production-ready RobotContainer
  • Split driver and operator controllers with separate port assignments and documented responsibilities
  • Build a SendableChooser<Command> that lets the drive team select autonomous routines live from the dashboard
  • Write a control map comment block documenting every button binding
  • Apply the 2910 convention: one public method, getAutonomousCommand(), is the only thing Robot.java calls on RobotContainer

RobotContainer's Three Responsibilities

A well-designed RobotContainer does exactly three things and nothing else:

  1. Instantiate subsystems. One instance per mechanism, declared as private final fields. These are shared references — every command that uses a subsystem receives it via constructor injection from here.
  2. Declare controllers. One XboxController (or equivalent) per human operator, on their USB port. Driver is port 0. Operator is port 1. No logic happens here — just declarations.
  3. Wire input to commands. All button, trigger, and axis bindings in one method: configureButtonBindings(). Every line in this method is a trigger attached to a command. If a binding changes, this is the only file touched.

If you find yourself writing control logic in RobotContainer — deciding what to do based on robot state, reading sensor values — that logic belongs in a command or subsystem. RobotContainer is a wiring diagram, not a decision-maker.

The Complete RobotContainer

RobotContainer.java — full production template
public class RobotContainer {

  // ─── Subsystems ──────────────────────────────────
  private final DriveSubsystem m_drive = new DriveSubsystem();
  private final IntakeSubsystem m_intake = new IntakeSubsystem();
  private final ShooterSubsystem m_shooter = new ShooterSubsystem();

  // ─── Controllers ─────────────────────────────────
  private final XboxController m_driver = new XboxController(0);
  private final XboxController m_operator = new XboxController(1);

  // ─── Autonomous chooser ───────────────────────────
  private final SendableChooser<Command> m_autoChooser =
      new SendableChooser<>();

  public RobotContainer() {
    configureButtonBindings();
    configureAutonomous();
  }

  private void configureButtonBindings() {
    /*
     * DRIVER CONTROLLER (port 0)
     * Left Stick Y : Forward/back speed (arcade drive)
     * Right Stick X: Rotation (arcade drive)
     * Start button : Zero gyro heading
     *
     * OPERATOR CONTROLLER (port 1)
     * A (hold) : IntakeCommand
     * B (press) : EjectCommand
     * Y (press) : ShootCommand
     */

    // Default: drive with joysticks
    m_drive.setDefaultCommand(new DriveWithJoysticksCommand(m_drive, m_driver));

    // Driver: zero gyro on Start
    new JoystickButton(m_driver, XboxController.Button.kStart.value)
      .onTrue(Commands.runOnce(() -> m_gyro.reset(), m_gyro));

    // Operator: A hold = intake
    new JoystickButton(m_operator, XboxController.Button.kA.value)
      .whileTrue(new IntakeCommand(m_intake).withTimeout(3.0));

    // Operator: B = eject once
    new JoystickButton(m_operator, XboxController.Button.kB.value)
      .onTrue(new EjectCommand(m_intake));
  }

  private void configureAutonomous() {
    m_autoChooser.setDefaultOption("Drive Forward", new DriveDistanceCommand(m_drive, 72));
    m_autoChooser.addOption("Score + Drive", new ScoreAndDriveAuto(m_drive, m_shooter));
    m_autoChooser.addOption("Do Nothing", Commands.none());
    SmartDashboard.putData("Auto Chooser", m_autoChooser);
  }

  // Only public method — called by Robot.java
  public Command getAutonomousCommand() {
    return m_autoChooser.getSelected();
  }
}
← click a highlighted token

The Control Map: Your Most Important Comment

Notice the block comment inside configureButtonBindings() listing every binding. This is the control map — a human-readable summary of the entire operator interface. On 2910, this comment is required and kept in sync with the actual bindings throughout the season.

Here's why: at competition, the drive coach is standing at the field explaining to a new driver what every button does. They should be able to open RobotContainer.java and read it directly. If the control map comment doesn't match reality, someone made a change without updating it — that's a code review failure.

💡 Driver vs. Operator: the physical split

At competition, two people hold controllers. The driver (port 0) controls locomotion: forward, back, turn, field orientation reset. Their hands must stay on the drivetrain controls — everything else is the operator's job. The operator (port 1) controls all mechanisms: intake, shooter, arm, climber. This split is physical and deliberate. A driver who also has to think about shooting will miss driving opportunities. Design your control map around this division before writing a single binding.

Interactive Control Map Explorer

Explore a sample competition control map. Click a button to see the bound command and its behavior mode.

Competition Control Map — Sample Robot
Left Y + Right X axes
Arcade drive (default)
LB (hold)
Slow/precision mode
Start (press)
Zero gyro heading
A (hold)
Auto-aim assist
A (hold)
Intake game piece
B (press)
Eject game piece
RB (hold)
Spin up shooter
Y (press)
Fire game piece
← click a control to see the bound command and binding mode
🔍 LRI Perspective: "I test the control map before inspection"

My pre-match checklist includes asking the operator to show me every binding by pressing each button while I watch Shuffleboard. If the Scheduler widget shows the command I expect, the wiring is correct. If it shows something different — or nothing — the binding was changed without updating the comment. This 2-minute check has caught real bugs at regionals. The control map comment is not just documentation; it's a test specification.

🔌 System Check — RobotContainer Verification

Before qualifying:

  • Open Driver Station USB Devices tab. Verify driver controller is port 0, operator is port 1. If they are swapped, the control map is mirrored in hardware. Fix the physical USB slot or update the port numbers in RobotContainer — do not fix it in mid-match.
  • SmartDashboard.putData("Auto Chooser", m_autoChooser) must be called in the constructor, not in a periodic method. If it's called in periodic, the chooser widget resets to the default on every loop iteration, making the drive team's selection volatile.
  • Verify getAutonomousCommand() is the only public method on RobotContainer that Robot.java calls. If Robot.java is reaching into RobotContainer for anything else (subsystem references, controller objects), that's a design smell.

Knowledge Check

1. A teammate puts this in RobotContainer.configureButtonBindings(): if (m_intake.hasGamePiece()) { ... }. What rule does this violate?

  • A RobotContainer cannot call subsystem methods — only commands can.
  • B RobotContainer is a wiring diagram — it binds triggers to commands but does not make runtime decisions. Sensor-driven logic belongs in a command's isFinished() or in an .onlyIf() decorator on the binding, not as a raw conditional in configureButtonBindings().
  • C configureButtonBindings() only runs once; the condition can't update dynamically.
  • D This is valid — conditional bindings are the correct way to gate command execution.

2. The drive team selects "Score + Drive" in the auto chooser on Thursday night. On Friday morning the Driver Station laptop is rebooted and Shuffleboard is reopened. What auto will run if the drive team doesn't reselect?

  • A "Score + Drive" — Shuffleboard saves the last selection persistently.
  • B The default option — whichever was passed to setDefaultOption() in the code. After a reboot, RobotContainer's constructor re-publishes the chooser with its default. The previous session's selection is gone.
  • C Nothing — getSelected() returns null until an option is explicitly chosen.
  • D The first option added with addOption(), regardless of the default.

3. You have a ShootCommand that should only fire if the shooter flywheel is at target RPM. The cleanest way to implement this gate in RobotContainer is:

  • A Add an if (m_shooter.atTargetRPM()) check inside ShootCommand.execute().
  • B Use .onlyIf(() -> m_shooter.atTargetRPM()) as a decorator on the binding in configureButtonBindings(). The gate lives at the scheduling layer — the command only starts if the condition is true.
  • C Move the atTargetRPM() check into ShootCommand.initialize() and return early if false.
  • D Check the RPM in RobotContainer.configureButtonBindings() using a conditional before creating the JoystickButton binding.
💪 Practice Prompt

Build the Full RobotContainer

  1. Write the complete control map comment block at the top of configureButtonBindings() before writing any binding code. List every planned input for both controllers. Then implement the bindings to match it exactly. If you find yourself writing a binding that's not in the comment, add it to the comment first.
  2. Add a SendableChooser<Command> with at least three autonomous options. Configure it in a private configureAutonomous() method called from the constructor. Publish it via SmartDashboard.putData(). Open Shuffleboard and confirm the dropdown appears and each option is selectable.
  3. Add an .onlyIf() guard to the intake binding so it only fires when the robot doesn't already have a game piece. Block the beam break by hand and confirm the binding is skipped. Unblock it and confirm it fires again.
  4. Add SmartDashboard.putData("Scheduler", CommandScheduler.getInstance()) and run through every binding. Screenshot the Scheduler widget showing each command name as you press each button. This is your binding verification record — keep it as a comment or attached doc for the team.
  5. Stretch goal: Create an AutonomousChooser helper class that encapsulates all the SendableChooser setup and registration, returning the chooser from a getChooser() method. RobotContainer calls this helper instead of managing the chooser inline. This is the pattern elite teams use when they have 6–8 autonomous options — it keeps RobotContainer lean.