Autonomous Routines
Hook
Fifteen seconds. No driver input. The robot has to score on its own.
Most teams write their first autonomous routine as a single command with a giant execute() method full of Thread.sleep() calls. It works in the shop, then fails at the event because the battery was at 91% instead of 100% and the timing was off by 0.3 seconds.
Reliable autonomous routines are composed from small, reusable commands. They finish based on sensors, not clocks — and when a sensor fails, they fall back gracefully. This lesson covers the structure.
Core concept
Autonomous routines are sequences of Commands. SequentialCommandGroup runs them one at a time, each finishing before the next starts. Wire multiple routines to a SendableChooser so drivers can pick one on SmartDashboard. Prefer sensor-based completion over time-based whenever possible.
The autonomous period
When the FMS (Field Management System) signals autonomous mode, WPILib calls:
Robot.autonomousInit()— runs onceRobot.autonomousPeriodic()— runs every 20 msRobot.autonomousExit()— runs once when auto ends
In a command-based project, autonomousInit() schedules the selected autonomous command. CommandScheduler.getInstance().run() in robotPeriodic() keeps executing it:
// Robot.java
private Command autoCommand;
@Override
public void autonomousInit() {
autoCommand = container.getAutonomousCommand();
if (autoCommand != null) {
autoCommand.schedule();
}
}
@Override
public void autonomousExit() {
if (autoCommand != null) {
autoCommand.cancel();
}
}
Cancel the auto command in autonomousExit(). Otherwise, if the auto command is still running (e.g., a drive command), it keeps running into teleop, fighting the driver.
SequentialCommandGroup
edu.wpi.first.wpilibj2.command.SequentialCommandGroup runs a list of commands one after another. Each command runs until its isFinished() returns true, then the next begins.
import edu.wpi.first.wpilibj2.command.SequentialCommandGroup;
public class DriveAndShootAuto extends SequentialCommandGroup {
public DriveAndShootAuto(DriveSubsystem drive, ShooterSubsystem shooter) {
addCommands(
new DriveDistanceCommand(drive, 2.0), // drive 2 meters
new SpinUpShooterCommand(shooter), // spin up to speed
new FireCommand(shooter, 1.5) // fire for 1.5 seconds
);
}
}
The scheduler runs DriveDistanceCommand until it reports finished, then immediately starts SpinUpShooterCommand, then FireCommand. No manual timing required.
Other command group types
| Class | Behavior |
|---|---|
SequentialCommandGroup | One at a time, in order |
ParallelCommandGroup | All at once; finishes when ALL finish |
ParallelRaceGroup | All at once; finishes when ANY ONE finishes, cancels others |
ParallelDeadlineGroup | All at once; finishes when the FIRST (deadline) command finishes |
These compose freely:
addCommands(
// Drive while spinning up the shooter simultaneously
new ParallelDeadlineGroup(
new DriveDistanceCommand(drive, 3.0), // deadline
new SpinUpShooterCommand(shooter) // runs alongside
),
// Then fire once in position
new FireCommand(shooter, 1.5)
);
SendableChooser
edu.wpi.first.wpilibj.smartdashboard.SendableChooser lets drivers choose an autonomous routine from SmartDashboard or Shuffleboard before the match:
// In RobotContainer
private final SendableChooser<Command> autoChooser = new SendableChooser<>();
public RobotContainer() {
configureAutoChooser();
configureBindings();
}
private void configureAutoChooser() {
autoChooser.setDefaultOption("Do Nothing",
Commands.none());
autoChooser.addOption("Drive Forward 2m",
new DriveDistanceCommand(drive, 2.0));
autoChooser.addOption("Drive and Shoot",
new DriveAndShootAuto(drive, shooter));
autoChooser.addOption("Two-Piece Auto",
new TwoPieceAuto(drive, intake, shooter));
SmartDashboard.putData("Auto Chooser", autoChooser);
}
public Command getAutonomousCommand() {
return autoChooser.getSelected();
}
Call autoChooser.getSelected() in autonomousInit(), not at construction time. The driver makes their selection during the pre-match period; reading it too early returns the default option regardless of what the driver chose.
Time-based vs sensor-based completion
Time-based
public class DriveForTimeCommand extends CommandBase {
private final DriveSubsystem drive;
private final double seconds;
private final Timer timer = new Timer();
public DriveForTimeCommand(DriveSubsystem drive, double seconds) {
this.drive = drive;
this.seconds = seconds;
addRequirements(drive);
}
@Override public void initialize() { timer.restart(); }
@Override public void execute() { drive.setSpeed(0.5, 0.5); }
@Override public boolean isFinished() { return timer.hasElapsed(seconds); }
@Override public void end(boolean interrupted) { drive.stop(); }
}
Pros: Simple. Always terminates. Cons: Distance depends on battery voltage, carpet friction, mechanism load. Consistent only in controlled conditions.
Sensor-based
public class DriveDistanceCommand extends CommandBase {
private final DriveSubsystem drive;
private final double targetMeters;
public DriveDistanceCommand(DriveSubsystem drive, double meters) {
this.drive = drive;
this.targetMeters = meters;
addRequirements(drive);
}
@Override public void initialize() { drive.resetEncoders(); }
@Override public void execute() { drive.setSpeed(0.5, 0.5); }
@Override public boolean isFinished() {
return drive.getAverageDistanceMeters() >= targetMeters;
}
@Override public void end(boolean interrupted) { drive.stop(); }
}
Pros: Repeatable. Doesn’t drift with battery voltage. Cons: Encoder failure or wheel slip can cause it to run indefinitely. Always add a timeout fallback.
Best practice: sensor-based with timeout
WPILib provides Command.withTimeout(seconds) as a decorator:
// Will finish when encoders reach 2m OR after 4 seconds — whichever comes first
new DriveDistanceCommand(drive, 2.0).withTimeout(4.0)
This is a ParallelRaceGroup under the hood — the timeout wins if the sensor command takes too long.
A complete auto example
public class DriveAndShootAuto extends SequentialCommandGroup {
public DriveAndShootAuto(DriveSubsystem drive,
ShooterSubsystem shooter,
IntakeSubsystem intake) {
addCommands(
// 1. Drive out of the starting zone (sensor-based, 4s timeout)
new DriveDistanceCommand(drive, 3.0).withTimeout(4.0),
// 2. Spin up shooter while retracting intake simultaneously
new ParallelDeadlineGroup(
new WaitUntilCommand(shooter::isAtTargetRpm), // deadline
new SpinUpShooterCommand(shooter),
new RetractIntakeCommand(intake)
).withTimeout(3.0),
// 3. Fire for 1.5 seconds
new FireCommand(shooter).withTimeout(1.5),
// 4. Stop shooter
Commands.runOnce(shooter::stop, shooter)
);
}
}
PathPlanner intro
For teams that need precise trajectories (curves, specific positions), PathPlanner is the de-facto standard in FRC.
PathPlanner (by team 3015 / mjansen4857) provides:
- A GUI to draw paths on the field diagram
- Auto-generation of
PathPlannerAutocommands that follow the path using a holonomic or differential drive controller - Integration with WPILib’s
SendableChooserviaAutoBuilder
// With PathPlanner installed, register named commands then build chooser:
NamedCommands.registerCommand("shoot", new FireCommand(shooter));
NamedCommands.registerCommand("intake", new IntakeCommand(intake));
// AutoBuilder reads paths from deploy/pathplanner/autos/
autoChooser = AutoBuilder.buildAutoChooser();
SmartDashboard.putData("Auto Chooser", autoChooser);
PathPlanner is a separate library — add it to build.gradle. Full documentation at pathplanner.me.
Interactive demo
autonomousInit()// auto period startsautoCommand.schedule()// group queuedDriveDistanceCommand.initialize()// encoders resetDriveDistanceCommand.execute() ×N// 20ms loopisFinished() → true// 2m reachedDriveDistanceCommand.end(false)// motors stoppedSpinUpShooterCommand.initialize()// next command startsSpinUpShooterCommand.execute() ×N// waiting for RPMisFinished() → true (at speed)FireCommand.initialize()// feedingFireCommand.end() → group done// auto completeStep through to see values update.
When should you call autoChooser.getSelected() to retrieve the driver's auto selection?
What does command.withTimeout(3.0) produce?
Key takeaways
autonomousInit()schedules the auto command;autonomousExit()cancels it so it doesn’t bleed into teleop.SequentialCommandGroupruns commands one at a time, each finishing before the next starts.ParallelDeadlineGroup,ParallelRaceGroup, andParallelCommandGrouphandle concurrent steps.SendableChooserwired toSmartDashboardlets drivers pick an auto at the field.- Sensor-based completion (
DriveDistanceCommand) is more repeatable than time-based; always add.withTimeout()as a safety net. - PathPlanner handles complex trajectories — use it once you’re comfortable with basic sequential autos.
Common confusions
“My auto command keeps running into teleop.” You forgot to call autoCommand.cancel() in autonomousExit(). Add it there.
“The chooser always shows the default option even after I select something.” You’re calling autoChooser.getSelected() too early — probably at construction. Move that call to autonomousInit().
“My SequentialCommandGroup skips a step.” Check that the skipped command’s isFinished() doesn’t return true immediately. Common cause: initialize() resets something that makes isFinished() true before execute() runs.
“The robot overshoots the distance target.” Open-loop speed commands with encoder-based completion still have motor latency. Consider using a PID drive command, or add a brief WaitCommand(0.1) after stopping to let the robot settle.
Challenge
Trace through this SequentialCommandGroup in your head, then confirm by reading the code trace. Each command’s finish condition is shown as a comment. Determine the order in which lines print.
Stuck? Show hint
Call runCommand('DriveForward', 2), then runCommand('SpinUp', 3), then runCommand('Fire', 1). Sequential means one finishes before the next begins.
What’s next
In Lesson 10: Telemetry and Logging, we instrument the robot — putting data on SmartDashboard, organizing dashboards with Shuffleboard, recording data for post-match review with DataLog, and understanding what’s worth logging versus what creates noise.