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 finalconstants (motor ports, PID gains, speeds).Subsystems/andCommands/— 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.runOnceusage) — 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
Commandsubclass — 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
Constantsclass. 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), andSubsystems/Commandsdirectories. Robot.robotPeriodic()must callCommandScheduler.getInstance().run()— nothing works without this line.- Subsystems stay
privateinRobotContainer; 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)