The flat Robot.java works, but it does not scale. The moment you add a second mechanism you will be fighting tangled state. WPILib strongly recommends command-based for new teams. Let us refactor.
A command-based project has four root files: Robot (mostly empty), RobotContainer (declares subsystems + bindings), Constants, and Main (do not touch). Subsystems live in subsystems/, commands in commands/.
DriveSubsystem.java:
package frc.robot.subsystems;
import com.revrobotics.spark.SparkMax;
import com.revrobotics.spark.SparkLowLevel.MotorType;
import edu.wpi.first.wpilibj.drive.DifferentialDrive;
import edu.wpi.first.wpilibj2.command.SubsystemBase;
public class DriveSubsystem extends SubsystemBase {
private final SparkMax m_left = new SparkMax(1, MotorType.kBrushless);
private final SparkMax m_right = new SparkMax(2, MotorType.kBrushless);
private final DifferentialDrive m_drive =
new DifferentialDrive(m_left::set, m_right::set);
public void arcade(double fwd, double rot) {
m_drive.arcadeDrive(fwd, rot);
}
}
Note the CAN IDs 1 and 2 match the IDs you persisted in Project 1. We now use the CAN SparkMax class (from the REVLib 2025+ com.revrobotics.spark package) instead of PWM, and pass its ::set method to DifferentialDrive.
RobotContainer.java wires a default command so the drivetrain is always listening to the sticks:
public class RobotContainer {
private final DriveSubsystem m_drive = new DriveSubsystem();
private final CommandXboxController m_ctrl =
new CommandXboxController(0);
public RobotContainer() { configureBindings(); }
private void configureBindings() {
m_drive.setDefaultCommand(
m_drive.run(() ->
m_drive.arcade(-m_ctrl.getLeftY(), -m_ctrl.getRightX())));
// hold B for a quick 'spin in place' demo
m_ctrl.b().whileTrue(m_drive.run(() -> m_drive.arcade(0, 0.5)));
}
}
Robot.java just runs the scheduler:
@Override public void robotPeriodic() {
CommandScheduler.getInstance().run();
}
Why this matters: the CommandScheduler runs each loop (the default TimedRobot period is 20ms / 50Hz) and, each loop, first runs every registered subsystem's periodic() method, then polls triggers, schedules any newly-triggered commands, runs already-scheduled commands, and removes any that have finished. The single most common command-based bug is forgetting CommandScheduler.getInstance().run() in robotPeriodic() — without it, nothing happens and the robot looks dead even though code is deployed. The setDefaultCommand pattern guarantees the drivetrain always has something to do when no other command requires it.
Key takeaways
- Command-based splits code into Subsystems, Commands, and bindings in RobotContainer — it scales where flat TimedRobot does not
- Forgetting CommandScheduler.getInstance().run() in robotPeriodic() makes the whole robot look dead
- setDefaultCommand keeps a subsystem (like drive) responsive whenever no other command requires it
Go deeper
Lesson quiz
RequiredAnswer all 3 questions correctly to complete this lesson.
01.When refactoring the drivetrain into a command-based subsystem, which base class does DriveSubsystem extend?
02.Why is a default command set on the drive subsystem with setDefaultCommand?
03.What happens if you forget to call CommandScheduler.getInstance().run() in robotPeriodic()?
Answer every question to submit.
All 28 lessons in Getting Started with FRC
- Not started:Project 1 — Make a NEO Spin with the REV Hardware Client
- Not started:Project 2 — Deploy a Real Arcade-Drive Program
- Not started:Project 3 — Refactor into a Command-Based Drive Subsystem
- Not started:Project 4 — Build a Fuel Launcher for REBUILT
- Not started:Project 5 — A One-Button Autonomous Routine
- Not started:The Connection Chain: When the Driver Station Won't Connect
- Not started:Brownouts: Why the Robot Goes Limp Mid-Match
- Not started:CAN Bus Gremlins: Missing and Conflicting Devices
- Not started:Software Gotchas: Inverted Drives, Scheduler Stalls, and Reading the RioLog
- Not started:Inspection-Day Failures: Bumpers, Size, and Weight
- Not started:Closed-Loop Control: PID + Feedforward for a Consistent Shot
- Not started:Swerve Drive: Omnidirectional Movement with YAGSL
- Not started:AprilTag Vision: Knowing Where You Are with PhotonVision
- Not started:Data-Driven Strategy: Scouting, EPA/OPR, and Alliance Selection
- Not started:Choosing Your Hardware Ecosystem: REV vs CTRE