Project 3 — Refactor into a Command-Based Drive Subsystem
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.
Key takeaways
Section titled “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
This lesson was adapted from learnfrc.com.
