498
Control Lab FRC Programming Curriculum
Robot Code · L01 of 1

The WPILib robot structure

Prereqs: fundamentals-06
Objectives 0 / 3

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

Key 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;
    }
}
Note

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.

⚠ Heads up

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:

Code Tracer
01Robot.robotInit()// runs once
02new RobotContainer()// wiring begins
03new ArmSubsystem()// hardware created
04configureBindings()// buttons wired
05teleopEnabled// match starts
06robotPeriodic() ×50/sec// loop ticking
07driver presses A button// command triggered
08ArmToPositionCommand.execute()// runs each tick
State
phaseboot
subsystemsnone
commandsnone
scheduleroff

Step through to see values update.

Initial state

Try it yourself

⚡ Check your understanding

Where should you create a CANSparkMax motor object?

⚡ Check your understanding

Two commands both call addRequirements(armSubsystem). What happens if they run at the same time?

Key takeaways

  • Robot.java is the entry point. It creates RobotContainer and keeps the scheduler running.
  • RobotContainer wires 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.