the video version
The same lesson as a narrated video. Watch it on YouTube
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, polls triggers, schedules new commands, runs scheduled commands, removes finished ones, then runs subsystem periodic() methods. 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.
the part worth keeping
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
Getting Started with FRCWorked Examples & Mini-Projectslesson 3 of 5
Keep going
Take the quiz+10 XP with an accountMore in Worked Examples & Mini-Projects
where this came from
Sources and corrections
This lesson is AI-assisted: drafted from primary sources, then reviewed and edited by hand. Errors still get through. When one is reported we fix it and write down what changed, in public, in the corrections log.
sources and further reading
- docs.wpilib.orgStructuring a Command-Based Robot Project
- docs.wpilib.orgWhat Is Command-Based Programming?
- docs.revrobotics.comMigrating to REVLib 2025 (REV)
clipped to this lesson
Articles that go further on this
The lesson gets you through the topic. These go wider on it, and they read in one sitting.
- 9 min readFRC Command-Based Programming: Subsystems, Commands, and the SchedulerA beginner-friendly guide to WPILib command-based programming in Java: subsystems, commands, the CommandScheduler, triggers, and composing commands./blogread it
- 13 min readFRC Code Structure Best Practices: Command-Based Project ArchitectureHow to structure an FRC command-based robot project the right way — subsystems, commands, RobotContainer, and Constants — verified against official WPILib docs./blogread it
answer sheet
Lesson quiz
All 3 right completes the lesson. Miss one and only that question comes back, anything you already answered correctly stays banked.
0 of 3 answered
01When refactoring the drivetrain into a command-based subsystem, which base class does DriveSubsystem extend?
02Why is a default command set on the drive subsystem with setDefaultCommand?
03What happens if you forget to call CommandScheduler.getInstance().run() in robotPeriodic()?
Answer every question to submit.
All 28 lessons in Getting Started with FRCopenclose
01 / what-first-and-frc-are
02 / the-season-and-the-game
03 / culture-teams-and-roles
04 / getting-started-your-first-steps
05 / worked-examples-mini-projects
- Not read yet:Project 1 — Make a NEO Spin with the REV Hardware Client
- Not read yet:Project 2 — Deploy a Real Arcade-Drive Program
- Not read yet:Project 3 — Refactor into a Command-Based Drive Subsystem
- Not read yet:Project 4 — Build a Fuel Launcher for REBUILT
- Not read yet:Project 5 — A One-Button Autonomous Routine
06 / common-mistakes-troubleshooting
- Not read yet:The Connection Chain: When the Driver Station Won't Connect
- Not read yet:Brownouts: Why the Robot Goes Limp Mid-Match
- Not read yet:CAN Bus Gremlins: Missing and Conflicting Devices
- Not read yet:Software Gotchas: Inverted Drives, Scheduler Stalls, and Reading the RioLog
- Not read yet:Inspection-Day Failures: Bumpers, Size, and Weight
07 / advanced-techniques-case-studies
- Not read yet:Closed-Loop Control: PID + Feedforward for a Consistent Shot
- Not read yet:Swerve Drive: Omnidirectional Movement with YAGSL
- Not read yet:AprilTag Vision: Knowing Where You Are with PhotonVision
- Not read yet:Data-Driven Strategy: Scouting, EPA/OPR, and Alliance Selection
- Not read yet:Choosing Your Hardware Ecosystem: REV vs CTRE