498
Control Lab FRC Programming Curriculum
Robot Code · L09 of 12

Autonomous Routines

Prereqs: robot-code-06
Objectives 0 / 4

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

Key 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:

  1. Robot.autonomousInit() — runs once
  2. Robot.autonomousPeriodic() — runs every 20 ms
  3. Robot.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();
    }
}
Note

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

ClassBehavior
SequentialCommandGroupOne at a time, in order
ParallelCommandGroupAll at once; finishes when ALL finish
ParallelRaceGroupAll at once; finishes when ANY ONE finishes, cancels others
ParallelDeadlineGroupAll 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();
}
⚠ Heads up

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 PathPlannerAuto commands that follow the path using a holonomic or differential drive controller
  • Integration with WPILib’s SendableChooser via AutoBuilder
// 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

Code Tracer
01autonomousInit()// auto period starts
02autoCommand.schedule()// group queued
03DriveDistanceCommand.initialize()// encoders reset
04DriveDistanceCommand.execute() ×N// 20ms loop
05isFinished() → true// 2m reached
06DriveDistanceCommand.end(false)// motors stopped
07SpinUpShooterCommand.initialize()// next command starts
08SpinUpShooterCommand.execute() ×N// waiting for RPM
09isFinished() → true (at speed)
10FireCommand.initialize()// feeding
11FireCommand.end() → group done// auto complete
State
stepinit
runningnone
finished

Step through to see values update.

Initial state
⚡ Check your understanding

When should you call autoChooser.getSelected() to retrieve the driver's auto selection?

⚡ Check your understanding

What does command.withTimeout(3.0) produce?

Key takeaways

  • autonomousInit() schedules the auto command; autonomousExit() cancels it so it doesn’t bleed into teleop.
  • SequentialCommandGroup runs commands one at a time, each finishing before the next starts.
  • ParallelDeadlineGroup, ParallelRaceGroup, and ParallelCommandGroup handle concurrent steps.
  • SendableChooser wired to SmartDashboard lets 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

⚡ Try it yourself

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.

Code EditorJavaCtrl+Enter to run
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.