Gray Matter
WorkshopAutonomous
WPILib 2027 is still in alpha: these pages change as the APIs settle.
LESSON 23

Autonomous

One autonomous routine is one class holding one command. The class puts a name on the driver station and owns the mode boundary. The command does the driving, and the one you build here leaves the starting line and stops.

About 30 minutes
You’ll need
  • A swerve robot you can drive, with a pose you trust, from Swerve Calibration.
  • A route plan and a starting pose from PathPlanner.
  • Command.sequence and .withTimeout, from Command Composition.
  • Three meters of clear floor and one person on the disable switch.

Nothing here is new syntax. Command Composition gave you Command.sequence. Finish Conditions gave you the rule that every step needs an ending. This lesson puts both inside an OpMode and drives a real robot with them.

The routine is a timed drive, and it is crude on purpose. A timed step tells you whether the mode list, the scheduler, and the drivetrain agree with each other. It tells you very little about where the robot ended up.

Two layers

The class is the part you cannot test without a driver station. Keep everything else out of it. A command that reads a controller, checks the match clock, or names a mode has taken on the class's job.

Lifecycle
The @Autonomous class
The name on the driver station, the Robot handed to its constructor, and the schedule and cancel calls at the mode boundary.
Behavior
The routine command
Which way to drive, for how long, and how it stops. The same command can run from a button or from another routine unchanged.

A command built on the drivetrain requires the drivetrain, so a second drivetrain command cannot run beside it. Arm and flywheel commands can. That is the same resource rule the scheduler has enforced since Workshop 2.

Build the routine

The PathPlanner plan does not become code yet. Its published Java examples target Commands v2, and pasting them into this project will not compile. What carries over is the geometry: where the robot starts, which way the first segment runs, and roughly how far.

Everything gets built in the constructor, which runs the moment somebody picks the mode. The routine lives in a field because end() needs a reference to the command it cancels. Building a command sends no output, so the constructor is safe to run while the robot is still disabled.

LeaveStartAuto.java: lifecycle and behavior together
package frc.robot.opmodes;
 
import static org.wpilib.units.Units.Seconds;
 
import com.ctre.phoenix6.swerve.SwerveRequest;
import frc.robot.Robot;
import org.wpilib.command3.Command;
import org.wpilib.command3.Scheduler;
import org.wpilib.opmode.Autonomous;
import org.wpilib.opmode.PeriodicOpMode;
 
@Autonomous(name = "Leave Start")
public class LeaveStartAuto extends PeriodicOpMode {
private final Command routine;
 
public LeaveStartAuto(Robot robot) {
Command drive =
robot.drivetrain
.applyRequest(
() ->
new SwerveRequest.RobotCentric()
.withVelocityX(1.0)
.withVelocityY(0.0)
.withRotationalRate(0.0))
.withTimeout(Seconds.of(1.5));
 
Command stop =
robot.drivetrain
.applyRequest(() -> new SwerveRequest.RobotCentric())
.withTimeout(Seconds.of(0.1));
 
routine = Command.sequence(drive, stop).named("Leave Start");
}
 
@Override
public void start() {
Scheduler.getDefault().schedule(routine);
}
 
@Override
public void end() {
Scheduler.getDefault().cancel(routine);
}
}

A timeout is the only finish line available here. DriveMechanism reports its pose, but nothing on it answers am I there yet the way arm.isAtTarget() did on Finish Conditions. Workshop 4 adds a command that measures against a field pose and finishes when it arrives.

Two names go into this file and they do different jobs. The one in the annotation is what the driver station lists, so it is the one a driver reads under pressure. The one in .named(...) is what the command is called in the log.

The second step is the one people leave out. setControl latches a request: the drivetrain keeps applying it until something sends a different one. When a routine ends, nothing does. The mechanism falls back to idle(), which sends nothing at all, and the wheels carry on at the last speed they were given.

In teleop the joystick default covers that. It is set in TeleopOpMode, and bindings belong to the mode that made them, so no such default exists in this class. A zero-speed step is what stops the robot.

The robot's field position is never set in this project, so the starting pose you wrote down in PathPlanner appears nowhere in the code. Drivetrain/Pose starts wherever odometry left off. Restart the robot code before a measured run and it reads near zero, which makes the distance easy to read straight off the log.

Four test passes

Each pass answers one question, and each one can fail on its own. Run them in order. A routine that fails the second pass has nothing to prove in the third.

Don't

Nobody in front of the robot

An autonomous routine drives with nobody holding a stick. Give one person the robot to watch and one person the driver station, with a thumb near disable. Keep the first three meters clear of anything you care about. Enable last.

  1. On blocks. Deploy, pick Leave Start off the mode list, and enable. All four modules drive forward together for about a second and a half. Then they stop, and they stay stopped while the mode runs.
  2. On the floor, once. Put the robot on its tape mark with clear floor ahead of it and run the same routine. It leaves in the direction its front bumper points, because RobotCentricX is the robot's forward and not the field's. The starting heading sets the direction.
  3. Measured, three times. Tape the floor at the front edge before and after each run, starting from the same mark every time. The taped distance and the end of Drivetrain/Pose in the log should agree within a few centimeters. The three runs should land inside about ten.
  4. Disabled partway. Hit disable about a second into the drive. The wheels stop at once. Re-select the mode from the list before running again: picking a mode builds the OpMode fresh, and a fresh routine with it.

A second and a half is a small slice of an autonomous period. A robot that sits still for the rest of it has not failed. That is the stop step doing its job.

Three failure shapes

A routine fails quietly. Nothing throws, nothing logs a complaint, and the robot does something you did not ask for. Almost all of it looks like one of these three.

Nothing moves
Stuck on a step
Selected, enabled, sitting still. A step with no ending holds the sequence there forever. Look for a request with no .withTimeout(...) on it.
Never stops
A latched request
The timeout expires and the robot keeps rolling. Nothing zeroes the drivetrain, so the last request stays applied. The zero-speed step is the fix.
Wrong place
Heading or voltage
It moves, and not where you drew it. Robot-centric X follows the starting heading, and a tired battery shortens a timed step by a surprising amount.

A mode missing from the driver station list is a different problem. Take it back to OpModes: a class that is not public, an annotation with no name, or a constructor that does not take Robot.

Read the log before guessing. Drivetrain/Pose at the end of a run separates a robot that went the wrong way from one that never went anywhere.

Check your work

Run the routine three times from the same tape mark, on the floor, with logging on. You are done when the three runs land on top of each other.

Check

You should see

  • Leave Start on the mode list, and the drive beginning the moment you enable.
  • The robot leaving in the direction its front bumper was pointing.
  • A full stop that stays stopped, with no creep after the timeout.
  • Three end poses in Drivetrain/Pose within about ten centimeters of each other.

Write down the distance the tape measured, the end pose out of the log, and the timeout that produced them. Workshop 4 replaces that timeout with a field pose the command can steer to. These three numbers are what you will hold the new routine against.

A second routine is a second file. Copy this one, change the annotation name and the numbers, and it turns up on the list beside the first. Nothing registers it, and nothing in Robot.java chooses between the two.