dev only

Structuring a Command-Based Robot Project

The Standard Project Shape

WPILib's own project template organizes a command-based robot around four root-level classes plus two subdirectories:

  • Robot — the entry point, kept intentionally thin (see below).
  • RobotContainer — where subsystems, commands, and trigger bindings are actually declared.
  • Constants — globally-accessible, public static final constants (motor ports, PID gains, speeds).
  • Subsystems/ and Commands/ — one file per user-defined subsystem or command class.

Robot: Deliberately Thin

Because command-based is declarative, Robot.java should contain almost nothing beyond a handful of required calls:

public Robot()
{
    robotContainer = new RobotContainer(); // performs all the actual setup
}

@Override
public void robotPeriodic()
{
    CommandScheduler.getInstance().run(); // THE single most important line in the whole project
}

@Override
public void autonomousInit()
{
    autonomousCommand = robotContainer.getAutonomousCommand();
    if (autonomousCommand != null)
    {
        CommandScheduler.getInstance().schedule(autonomousCommand);
    }
}

@Override
public void teleopInit()
{
    if (autonomousCommand != null)
    {
        autonomousCommand.cancel(); // stop auto once teleop begins
    }
}

Piling large amounts of imperative logic directly into Robot.java fights the entire declarative philosophy (Lesson 25.1) — real setup belongs in RobotContainer.

RobotContainer: Where Setup Actually Happens

Subsystems are declared as private fields here — deliberately not global variables:

private final Intake intake = new Intake();

private void configureBindings()
{
    driverController.b().whileTrue(intake.runIntakeCommand());
}

public Command getAutonomousCommand()
{
    return AutoRoutines.driveAndIntake(drivetrain, intake);
}

Keeping subsystems private and passing them explicitly to whatever needs them (dependency injection, first introduced in Lesson 25.2) is deliberate: if subsystems were globally accessible, any code anywhere could call subsystem methods directly, completely bypassing the scheduler's resource-management guarantees — exactly the two-commands-fighting-over-one-motor problem command-based exists to prevent.

Constants: One Place for Every Tunable Number

public static final class IntakeConstants
{
    public static final int kMotorId = 5;
    public static final double kIntakeSpeed = 0.8;
}

Every constant is public static final — globally reachable, and impossible to accidentally reassign (Ch.23's final). Grouping related constants into nested classes (one per subsystem, typically) keeps names shorter and organizes them logically.

Choosing How to Define a Reusable Command

For a command that's used in more than one place (a button binding, an autonomous routine, a self-test), a few options exist, each suited to a different case:

  • Instance factory method on a single subsystem (like Ch.20's Subsystem.runOnce usage) — best for a command tied to exactly one subsystem: intake.runIntakeCommand().
  • Static factory method (Ch.21) — best when a command needs to coordinate multiple subsystems at once, so it doesn't naturally belong to any single one of them.
  • A full Command subclass — best when a command needs its own genuine internal state (not subsystem state) or unusually complex logic.
public class Intake extends SubsystemBase
{
    public Command runIntakeCommand()
    {
        return this.startEnd(() -> this.set(1.0), () -> this.set(0.0)); // implicitly requires `this`
    }
}
public class AutoRoutines
{
    public static Command driveAndIntake(Drivetrain drivetrain, Intake intake)
    {
        return Commands.sequence(
            Commands.parallel(drivetrain.driveCommand(0.5, 0.5), intake.runIntakeCommand()).withTimeout(5.0),
            Commands.parallel(drivetrain.stopCommand(), intake.stopCommand())
        );
    }
}

Common Pitfalls

  • Declaring subsystems as global/static fields for easy access. This defeats the resource-management system's whole purpose — pass subsystems explicitly (dependency injection) instead.
  • Forgetting to cancel the autonomous command in teleopInit(). Without it, an unfinished autonomous command can keep running (and keep its subsystem requirements locked) into teleop.
  • Scattering constants across many files instead of one Constants class. Changing one tunable value (like an intake speed) should mean editing exactly one line, not hunting across the codebase.

Key Takeaways

  • A standard command-based project has Robot (thin, mostly boilerplate), RobotContainer (real setup, subsystems, bindings), Constants (all tunables), and Subsystems/Commands directories.
  • Robot.robotPeriodic() must call CommandScheduler.getInstance().run() — nothing works without this line.
  • Subsystems stay private in RobotContainer; other code reaches them only by dependency injection, never as globals — this is what preserves the scheduler's resource-management guarantees.
  • Choose how to define a reusable command based on its shape: instance factory (single subsystem), static factory (multiple subsystems), or a full class (real internal state or complex logic).

Derived from other-reference-repo: wpilib/command-based/04-subsystems.md (WPILIB 20.4), 06-structuring-command-based-project.md (WPILIB 20.6), 07-organizing-command-based.md (WPILIB 20.7)