The WPILib robot structure
Hook
Open a new WPILib project and you’re greeted with six files, two folders, and a build.gradle. Where do you even start?
Most beginners drop their code into Robot.java and it works — until it doesn’t. The motor that used to work stops responding. Two subsystems fight over the same hardware object. The autonomous routine runs half of itself, then stops.
The WPILib architecture isn’t bureaucracy. It exists to prevent exactly these failures. Understanding it is the difference between code that “works on my machine” and code that survives a competition.
Core concept
A WPILib robot program has four layers: Robot.java (the entry point), RobotContainer (the wiring), Subsystems (hardware + logic), and Commands (actions). Each layer has one job and talks to the others through a strict interface.
Walk-through
Layer 1: Robot.java
The entry point. WPILib calls its methods based on robot state:
public class Robot extends TimedRobot {
private RobotContainer container;
public void robotInit() {
container = new RobotContainer(); // runs once at startup
}
public void robotPeriodic() {
CommandScheduler.getInstance().run(); // runs every 20ms, always
}
public void teleopInit() { /* ... */ }
public void autonomousInit() { /* ... */ }
}
Almost nothing else belongs in Robot.java. Its only job is to create RobotContainer and keep the scheduler ticking.
Layer 2: RobotContainer
Where you wire the robot. Created once at startup, it:
- Creates every Subsystem
- Creates default Commands (what a subsystem does when nothing else is running)
- Binds buttons to Commands
public class RobotContainer {
private final ArmSubsystem arm = new ArmSubsystem();
private final XboxController controller = new XboxController(0);
public RobotContainer() {
configureBindings();
}
private void configureBindings() {
new JoystickButton(controller, XboxController.Button.kA.value)
.onTrue(new ArmToPositionCommand(arm, 90.0));
}
}
Layer 3: Subsystems
One class per mechanism. A Subsystem owns its hardware objects and exposes methods for commands to call.
public class ArmSubsystem extends SubsystemBase {
private final CANSparkMax motor = new CANSparkMax(1, MotorType.kBrushless);
private final Encoder encoder = new Encoder(0, 1);
public void setMotorPower(double power) {
motor.set(power);
}
public double getAngle() {
return encoder.getDistance();
}
@Override
public void periodic() {
SmartDashboard.putNumber("Arm/angle", getAngle());
}
}
A Subsystem can only be “owned” by one Command at a time — the scheduler enforces this. This prevents two Commands from fighting over the same motor.
Layer 4: Commands
Commands describe what the robot does, not how the hardware works. A Command uses a Subsystem’s public methods.
public class ArmToPositionCommand extends CommandBase {
private final ArmSubsystem arm;
private final double target;
public ArmToPositionCommand(ArmSubsystem arm, double target) {
this.arm = arm;
this.target = target;
addRequirements(arm); // claims exclusive ownership
}
@Override
public void execute() {
double error = target - arm.getAngle();
arm.setMotorPower(error > 0 ? 0.4 : -0.4);
}
@Override
public boolean isFinished() {
return Math.abs(target - arm.getAngle()) < 2.0;
}
}
addRequirements(arm) is what prevents conflicts. If another Command tries to use the arm while this one is running, the scheduler cancels the lower-priority Command first.
Never create hardware objects (motors, sensors) inside a Command. They belong in the Subsystem. If two Commands each created a CANSparkMax(1, ...), they’d both try to initialize the same CAN ID — undefined behavior.
Interactive demo
Trace the startup sequence from power-on to the driver pressing a button:
Robot.robotInit()// runs oncenew RobotContainer()// wiring beginsnew ArmSubsystem()// hardware createdconfigureBindings()// buttons wiredteleopEnabled// match startsrobotPeriodic() ×50/sec// loop tickingdriver presses A button// command triggeredArmToPositionCommand.execute()// runs each tickStep through to see values update.
Try it yourself
Where should you create a CANSparkMax motor object?
Two commands both call addRequirements(armSubsystem). What happens if they run at the same time?
Key takeaways
Robot.javais the entry point. It createsRobotContainerand keeps the scheduler running.RobotContainerwires Subsystems to Commands and Commands to buttons.- Subsystems own hardware. Commands describe actions using Subsystem methods.
addRequirements()prevents two Commands from controlling the same hardware simultaneously.- Hardware objects (motors, sensors) belong in Subsystems — never in Commands or
Robot.java.
Common confusions
“My subsystem’s periodic() isn’t running.” Subsystem periodic() only runs if the Subsystem is registered with the scheduler. SubsystemBase does this automatically in its constructor — make sure you’re extending SubsystemBase, not just Object.
“I have duplicate motor behavior — it’s being driven from two places.” You probably have a default command on the subsystem AND another command running. Check CommandScheduler.getInstance().printWatchdogEpochs() in the logs — it shows what’s running each tick.
What’s next
In Robot Code Lesson 02, we’ll go deeper into Subsystems — how to structure one cleanly, what belongs in periodic(), and how to expose the right interface for your Commands.