Unit 9 · Lesson 1

Autonomous Code Structure in Command-Based

Autonomous isn't a special mode — it's the same Command-Based architecture you already know, just without a driver. This lesson maps exactly how the framework receives control at match start, where your commands live, and what the robot is actually doing during those 15 seconds.

By the end of this lesson, you will:

  • Trace the exact call chain from the Driver Station enabling autonomous to the first command executing
  • Identify the role each file plays in the autonomous pipeline: Robot.java, RobotContainer.java, and command classes
  • Explain what getAutonomousCommand() returns and when it is called
  • Describe why the Command Scheduler — not a timer — drives the autonomous execution loop
  • Understand how command requirements and subsystem ownership work during autonomous
  • Recognize the two most common structural mistakes teams make when wiring up autonomous for the first time

What Autonomous Actually Is in Command-Based

If you think of autonomous as a completely different mode requiring different code, you've made a very common conceptual mistake. In Command-Based, the architecture doesn't change at all between teleop and autonomous. The same subsystems exist. The same commands exist. The same Command Scheduler runs every 20 milliseconds.

What changes in autonomous is simpler than that: the source of commands changes. During teleop, commands are triggered by driver button presses on controllers. During autonomous, a single pre-assembled command (or command group) is scheduled once at the start of the 15-second period, and the scheduler runs it to completion — or until the period ends, whichever comes first.

That's the entire difference. Everything else — subsystem requirements, the execute() loop, isFinished() checks, interruption handling — works identically to what you built in Unit 6. Autonomous is just Command-Based with a pre-scheduled command instead of a human driving.

💡 Your teleop commands already run during autonomous if you're not careful

Default commands are still active during autonomous unless your auto command holds the same subsystem requirement. If your drivetrain has a DriveWithJoystick default command, it will resume the moment your auto command ends — or if it never required the drivetrain. Always make sure your auto command explicitly requires every subsystem it touches. The scheduler will handle the rest automatically.

The Autonomous Call Chain: From Enable to First Motion

When the Driver Station enables autonomous, a specific sequence of calls flows through your code. Understanding this chain prevents the two most common autonomous wiring bugs. Click each step to see what's happening and why it matters.

Autonomous enable sequence click each step
DS enables Driver Station
autonomousInit() Robot.java
getAutonomous
Command()
RobotContainer
schedule(cmd) CommandScheduler
autonomous
Periodic()
Robot.java (loop)
Scheduler.run() every 20 ms
execute() /
isFinished()
your command
Driver Station The Driver Station sends an enable signal over the network

When the FMS (or a practice DS) enables autonomous, it sends a mode packet to the roboRIO. WPILib's communication layer receives this, recognizes the mode change, and calls autonomousInit() once. This is the only event that triggers the autonomous startup sequence — there is no other hook.

The Three Files and What Each One Owns

Every autonomous routine in a Command-Based project touches the same three files. Their responsibilities are strictly divided — blurring those responsibilities is the source of most structural bugs.

🤖 Robot.java
The mode manager — calls init once, runs the scheduler every loop Robot.java is responsible for two things in autonomous: calling getAutonomousCommand() and scheduling it in autonomousInit(), then calling CommandScheduler.getInstance().run() in autonomousPeriodic(). Nothing else. In a well-structured project, autonomousInit() is four to six lines long. If it's longer, something that belongs in RobotContainer or a command class has leaked into Robot.java.
📦 RobotContainer.java
The wiring file — assembles subsystems and returns the auto command RobotContainer holds all subsystem instances and exposes a single getAutonomousCommand() method that returns the assembled auto command or command group. This is where your autonomous strategy is expressed in code: which commands run, in what order, with what parameters. RobotContainer doesn't execute anything — it builds and returns. Execution is the scheduler's job.
commands/*.java
The behavior units — each one does exactly one thing Individual command classes (e.g., DriveDistanceCommand, IntakeUntilSensorCommand, ShootCommand) contain the actual robot behavior. In autonomous, these are the building blocks that RobotContainer assembles into a sequence. Each command should require only the subsystems it genuinely uses, should be small enough to test independently, and should never know it's running during auto versus teleop — the scheduler handles that context transparently.

Robot.java: The Minimal Autonomous Wiring

The modern WPILib template for Robot.java in a Command-Based project is intentionally minimal. Here is what a correctly wired autonomous looks like, annotated for every line that matters.

Robot.java — command-based template, autonomous highlighted
public class Robot extends TimedRobot {

    private RobotContainer m_robotContainer;

    @Override
    public void robotInit() {
        m_robotContainer = new RobotContainer();
        // RobotContainer constructor creates all subsystems and wires all bindings.
    }

    @Override
    public void robotPeriodic() {
        CommandScheduler.getInstance().run();
        // Runs every 20 ms regardless of mode — keeps the scheduler ticking.
    }

    @Override
    public void autonomousInit() {
        m_autoCommand = m_robotContainer.getAutonomousCommand();
        // Ask RobotContainer for the selected auto command.

        if (m_autoCommand != null) {
            m_autoCommand.schedule();
            // Hand the command to the scheduler. It runs in robotPeriodic() above.
        }
    }

    @Override
    public void autonomousPeriodic() {
        // Nothing here — robotPeriodic() already runs the scheduler every loop.
        // Putting motor commands directly in autonomousPeriodic() bypasses the
        // entire Command-Based framework and is one of the most common mistakes.
    }

    @Override
    public void teleopInit() {
        if (m_autoCommand != null) {
            m_autoCommand.cancel();
            // Cancel the auto command when teleop starts.
            // Without this, a still-running auto command will compete
            // with your teleop commands for subsystem ownership.
        }
    }

    private Command m_autoCommand;
}
🔍 The two wiring bugs I see every event

The first is putting motor commands directly inside autonomousPeriodic() — usually because a student copied code from a TimedRobot tutorial into a Command-Based project. The motors run, but they bypass the scheduler entirely. Requirements aren't checked, subsystem ownership isn't respected, and the commands you wrote in Unit 6 never run at all.

The second is forgetting to cancel the auto command in teleopInit(). The robot drives autonomously into the first 10 seconds of teleop because the auto SequentialCommandGroup is still executing. The driver is pressing buttons, the scheduler is ignoring them because the auto command holds the drivetrain requirement, and everyone is confused why the robot won't respond. One line — m_autoCommand.cancel() — prevents all of it.

RobotContainer.java: Building and Returning the Auto Command

The getAutonomousCommand() method in RobotContainer is where your autonomous strategy is actually written. At its simplest, it assembles a command group from your individual commands and returns it. At its most sophisticated, it returns whichever command a SendableChooser selected on the dashboard. Both patterns are valid — they share the same structural contract: build the command here, return it, and let the scheduler execute it.

RobotContainer.java — getAutonomousCommand() patterns
public class RobotContainer {

    // Subsystems — created once, shared with commands via constructor injection
    private final DriveSubsystem  m_drive  = new DriveSubsystem();
    private final IntakeSubsystem m_intake = new IntakeSubsystem();
    private final ArmSubsystem    m_arm    = new ArmSubsystem();

    // ── Pattern A: hardcoded single routine ────────────────────────────────
    // Fine for a robot with only one auto strategy. Simple and clear.
    public Command getAutonomousCommand() {
        return Commands.sequence(
            new DriveDistanceCommand(m_drive, 2.0),   // drive 2 meters forward
            new IntakeUntilSensorCommand(m_intake),    // grab game piece
            new MoveArmToAngleCommand(m_arm, 90),      // raise arm to score
            new ShootCommand(m_arm, m_intake)         // score
        );
    }

    // ── Pattern B: SendableChooser (Unit 9, Lesson 5 covers this fully) ────
    // Used when you have multiple strategies to select pre-match.
    // m_autoChooser is populated in the constructor with addOption() calls.
    public Command getAutonomousCommand() {
        return m_autoChooser.getSelected();
        // Returns whichever Command the drive team selected on SmartDashboard.
    }
}
💡 Commands.sequence() vs. new SequentialCommandGroup()

Both produce a sequential command group — the factory method Commands.sequence() is the modern WPILib idiom and is preferred for inline assembly in getAutonomousCommand(). Named SequentialCommandGroup subclasses are better when the group is complex enough to deserve its own file, needs constructor parameters, or is reused in multiple places. You'll use both across Unit 9; the choice is structural, not functional.

What the Scheduler Does With Your Auto Command

Once schedule() is called on the auto command in autonomousInit(), the Command Scheduler takes over. Click each lifecycle phase to understand what the scheduler is doing during those 15 seconds and what your command code is responsible for.

Command lifecycle during autonomous click a phase
initialize()
execute()
isFinished()
end(interrupted)
Sequence transitions
Period ends
Once, at command start initialize() runs once when the command is first scheduled

For a top-level auto command that's a SequentialCommandGroup, initialize() on the group runs once, then immediately starts the first child command. For each individual command in the sequence, initialize() runs once at the start of that command's execution.

This is where you reset encoders, reset odometry, set a new PID goal, or take any one-time setup action. Code here runs exactly once per command — don't put repeating logic in initialize(). If you want something to happen every loop, it belongs in execute().

Subsystem Requirements During Autonomous

Every command that uses a physical mechanism must declare that subsystem as a requirement by calling addRequirements(subsystem) in the constructor. This is how the scheduler prevents two commands from fighting over the same hardware simultaneously.

During autonomous this matters just as much as during teleop — possibly more, because command groups often run parallel branches. If a parallel group has two child commands that both require the drivetrain, the scheduler will throw a runtime error and cancel one of them. Understanding requirements prevents that class of bug before it happens at a match.

DriveDistanceCommand.java — requirement declaration
public class DriveDistanceCommand extends Command {

    private final DriveSubsystem m_drive;
    private final double m_distanceMeters;

    public DriveDistanceCommand(DriveSubsystem drive, double meters) {
        m_drive = drive;
        m_distanceMeters = meters;
        addRequirements(drive);
        // ↑ This line is mandatory. Without it:
        //   - Default commands (like DriveWithJoystick) can preempt this command mid-auto.
        //   - Parallel branches in command groups can run conflicting drive commands.
        //   - The scheduler has no way to enforce hardware ownership.
    }

    @Override
    public void initialize() {
        m_drive.resetEncoders();
    }

    @Override
    public void execute() {
        m_drive.arcadeDrive(0.5, 0);   // 50% forward, no rotation
    }

    @Override
    public boolean isFinished() {
        return m_drive.getAverageDistanceMeters() >= m_distanceMeters;
    }

    @Override
    public void end(boolean interrupted) {
        m_drive.stop();
        // Always stop motors in end() — whether the command finished
        // normally or was interrupted. If you skip this, the motors
        // keep running at whatever speed execute() last set them to.
    }
}
🔍 Always stop motors in end()

One of the most common bugs in autonomous: a command's isFinished() returns true, end() is called, but no stop() call exists inside it. The motor keeps spinning at whatever speed execute() last commanded — which is typically full speed. The robot drives past its target, and the next command starts while the robot is still moving. In a sequential group this corrupts every subsequent action. Make it a habit: every command that moves a mechanism calls stop() in end(), no exceptions.

How the 15-Second Period Actually Ends

You don't write any code to handle the end of the autonomous period. When the FMS transitions from autonomous to teleop, the Driver Station sends a mode change packet. WPILib receives it and calls teleopInit() in Robot.java. Your teleopInit() should call m_autoCommand.cancel(), which tells the scheduler to interrupt all running commands and call their end(true) methods.

From that point, the scheduler continues running — it just no longer has the auto command active. Teleop button bindings take over. Default commands resume on any subsystem that was being held by the auto command.

If your auto command finishes on its own before the 15 seconds are up (all commands in the sequence have completed), the scheduler simply has nothing more to run. The robot will coast or hold its last state until teleop begins — whichever motor commands were left active in the final command's end() method will remain in effect. This is why stopping motors in end() matters even when your auto finishes cleanly.

🔌 System Check

⚙️ Before You Test Any Autonomous Routine

The autonomous plumbing must be verified before you can trust any auto behavior. Work through this before your first run:

  • getAutonomousCommand() returns a non-null command. Add a temporary print in autonomousInit(): if m_autoCommand == null, log a warning and return early. A null auto command schedules nothing — the robot sits still, and you won't know why unless you check.
  • Every auto command declares its requirements. Open each command class used in your autonomous sequence and confirm addRequirements() is called in the constructor with every subsystem the command uses. A missing requirement means the default command can interrupt your auto mid-run.
  • end() stops motors on every command. Read through each auto command's end() method. If it's empty, add a stop call. This one omission causes more mid-auto runaway behavior than any other bug.
  • teleopInit() cancels the auto command. Confirm the cancel call exists. Without it, run the robot in auto mode in the shop, then flip to teleop on the Driver Station and verify the robot stops responding to autonomous immediately when teleop begins.
  • Test with the robot elevated off the ground first. Before running any autonomous that involves drivetrain movement, verify each individual command works correctly with the wheels in the air. isFinished() should return true at the expected encoder value. If it never finishes or finishes instantly, fix that before running on the ground.

Knowledge Check

1. A team has a working Command-Based robot. They add autonomous code by putting motor commands directly inside autonomousPeriodic() in Robot.java. The robot moves during auto, but their DriveDistanceCommand never runs. What is the most accurate explanation?

  • A DriveDistanceCommand must be registered with the scheduler in robotInit() before it can run in autonomous
  • B Code in autonomousPeriodic() bypasses the Command Scheduler entirely — DriveDistanceCommand is a command object that only runs when scheduled, and it was never scheduled
  • C Commands cannot require the drivetrain during autonomous because default commands hold it
  • D autonomousPeriodic() only runs once per autonomous period, not every 20 ms

2. A team's robot finishes its autonomous sequence after 8 seconds. The drive motors were last set to 50% in the final command's execute(). The end() method of that command is empty. What happens to the robot for the remaining 7 seconds of the autonomous period?

  • A The robot stops automatically because isFinished() returned true
  • B The scheduler restarts the auto command from the beginning
  • C The motors continue spinning at 50% — the motor controller holds its last commanded output until told otherwise — and the robot drives until the field boundary or until teleop begins
  • D The default DriveWithJoystick command immediately takes over and the robot responds to driver input

3. A team forgets to call m_autoCommand.cancel() in teleopInit(). Their autonomous command is a SequentialCommandGroup that requires both the drivetrain and the arm. What will the driver experience when teleop begins?

  • A Teleop commands work normally because the scheduler automatically clears all scheduled commands at mode boundaries
  • B Teleop commands that require the drivetrain or arm will be silently ignored by the scheduler, because the auto command still holds those requirements — the robot continues executing whatever step of the sequence it was in
  • C The auto command immediately stops because teleopInit() is called, even without an explicit cancel
  • D The scheduler throws an exception and the robot code crashes
💪 Practice Prompt

Wire Up and Verify the Autonomous Pipeline

  1. Open your current robot project (or create a new Command-Based template). Locate Robot.java and read every method from top to bottom. Identify: where is getAutonomousCommand() called? Where is the result scheduled? Is there a cancel call in teleopInit()? If any of these are missing, add them now — do not continue to the next step until the wiring is complete.
  2. In RobotContainer.java, write a getAutonomousCommand() method that returns a Commands.sequence() with at least two placeholder commands: one that prints "Auto started" with Commands.print(), waits 1 second with Commands.waitSeconds(1), and one more print. Deploy to a robot or run in simulation. Enable autonomous on the Driver Station and verify the sequence executes.
  3. Write a simple StopAndPrintCommand that requires your drivetrain subsystem, drives forward at 30% speed in execute(), finishes after 2 seconds using a Timer, and calls drive.stop() in end(). Add it to your auto sequence. Verify end() is called and motors stop by logging to SmartDashboard.
  4. Intentionally break the wiring in two ways — one at a time, fixing each before introducing the next: (a) remove the cancel() call from teleopInit() and observe what happens when you flip to teleop, then restore it; (b) remove addRequirements() from your drive command and confirm whether a default command interrupts it unexpectedly.
  5. Bonus: Open Team 2910's most recent robot GitHub repository (linked in Unit 0, Lesson 5). Find their Robot.java and RobotContainer.java. Identify exactly where getAutonomousCommand() is called and what it returns. Write two sentences describing how their structure matches — or differs from — the pattern in this lesson.