Lab 7: Timed Autonomous on the WCD#
Why it matters#
The first 15 seconds of every FRC match are autonomous: the robot runs entirely on its own code, with no driver input. A robot that does something useful in auto starts every match ahead.
In this lab you'll make the West Coast Drive drive a pattern by itself, and let the drive team pick which pattern from a menu on the dashboard. You'll use only time to decide how far to go, and you'll find out why that's not good enough. That sets up Lab 8, where the robot uses a gyro to know how far it has really turned.
Before you start: finish Lab 2. You'll add to the same WestCoastDrive project. Lesson 11 covers autonomous in more depth.
Read this#
Walkthrough#
Autonomous safety
In auto, nobody is steering. The robot drives the moment it's enabled.
- Clear a big space and keep everyone out of the robot's path.
- One person's only job is the disable key (Enter) and the emergency stop (Space).
- Test each new routine on blocks first.
- Keep
MaxOutputat 0.5 from Lab 2.
How autonomous runs#
You don't have to write any of this, but you should know it's there. In Robot.java, the template already does:
- When auto starts,
autonomousInit()asksRobotContainer.getAutonomousCommand()for a command and schedules it. - The scheduler runs it every 20 ms until it ends.
- When teleop starts,
teleopInit()cancels it, in case it's still running.
So your whole job is to make getAutonomousCommand() return the right command. While an auto command is using the drivetrain, the joystick default command pauses, then comes back when auto ends.
Step 1: A command that drives for a set time#
Open DriveSubsystem.java and add:
/** Drives each side at a fixed speed for a set time, then stops. */
public Command driveForSecondsCommand(double leftSpeed, double rightSpeed, double seconds) {
return run(() -> m_drive.tankDrive(leftSpeed, rightSpeed, false)) // false = don't square
.withTimeout(seconds)
.finallyDo(() -> m_drive.stopMotor())
.withName("Drive " + seconds + " s");
}
| Piece | Why |
|---|---|
double leftSpeed, double rightSpeed, double seconds |
Three inputs, so one method can make any timed drive: forward, backward, or spinning |
run(...) |
Every loop, send the speeds. DifferentialDrive needs a fresh speed every loop, or its safety feature stops the motors. |
tankDrive(left, right, false) |
The false turns off squaring, so 0.4 means 40%, not 16% |
withTimeout(seconds) |
Ends the command after that many seconds |
finallyDo(...) |
Runs when the command ends for any reason (finished, timed out, or canceled), so the motors always stop |
With this one command you can drive forward (both sides the same), backward (both negative), or spin in place (sides opposite). Remember the skid steer levers from Lab 2.
Step 2: Build routines from pieces#
Routines are just commands chained together. Add these methods to RobotContainer.java:
/** Drive straight forward for 2 seconds. */
private Command driveForwardAuto() {
return m_driveSubsystem.driveForSecondsCommand(0.4, 0.4, 2.0)
.withName("Drive Forward");
}
/** One side of a square: drive forward, pause, spin left about 90 degrees, pause. */
private Command squareSide() {
return Commands.sequence(
m_driveSubsystem.driveForSecondsCommand(0.4, 0.4, 1.0), // forward
m_driveSubsystem.driveForSecondsCommand(0.0, 0.0, 0.3), // pause
m_driveSubsystem.driveForSecondsCommand(-0.4, 0.4, 0.5), // spin left (guess the time!)
m_driveSubsystem.driveForSecondsCommand(0.0, 0.0, 0.3)); // pause
}
/** Try to drive a square and end where it started. */
private Command squareAuto() {
return Commands.sequence(squareSide(), squareSide(), squareSide(), squareSide())
.withName("Square");
}
Import edu.wpi.first.wpilibj2.command.Commands.
Piece by piece:
| Code | What it does |
|---|---|
private Command driveForwardAuto() |
A method that builds a routine and hands it back. Nothing moves until the routine is scheduled. |
driveForSecondsCommand(0.4, 0.4, 2.0) |
Both sides at 40% for 2 seconds: straight forward |
Commands.sequence(a, b, c, d) |
One command that runs a, then b, then c, then d. Each starts when the one before it ends. |
driveForSecondsCommand(-0.4, 0.4, 0.5) |
Left side backward, right side forward: spin left in place, for a guessed 0.5 seconds |
Commands.sequence(squareSide(), squareSide(), ...) |
A sequence made of sequences: four sides make a square |
.withName("Square") |
The name the dashboard shows while it runs |
Why pause by driving at 0 instead of Commands.waitSeconds?
DifferentialDrive has a safety feature: if nothing sends it a speed for about a tenth of a second, it stops the motors and prints an "Output not updated often enough" warning. Driving at 0 for the pause keeps it fed, so the console stays quiet. Each squareSide() call builds a new set of commands, which matters: one command object can't be used twice in the same routine.
The spin time is a guess
Nobody knows how long a 90° spin takes. Start with 0.5 seconds and adjust it until one turn looks like 90°. That guessing is the point of this lab.
Step 3: Let the drive team pick#
A Selectable puts a dropdown menu in Elastic, sent over NetworkTables like everything else. It's part of the course's telemetry folder you copied in Lab 2, and it's the same as WPILib 2027's Selectable (Lesson 6.1).
Add a field to RobotContainer:
private final Selectable<Command> m_autoChooser = new Selectable<>();
In the constructor, after configureBindings(), add the options and put the menu on the dashboard:
m_autoChooser.addDefault("Do nothing", Commands.none());
m_autoChooser.add("Drive forward", driveForwardAuto());
m_autoChooser.add("Square", squareAuto());
Tunables.publish("Auto", m_autoChooser); // shows up as /Tunables/Auto
And make getAutonomousCommand() return the choice:
public Command getAutonomousCommand() {
return m_autoChooser.getSelected();
}
Import frc.robot.telemetry.Selectable and frc.robot.telemetry.Tunables.
| Code | What it does |
|---|---|
Selectable<Command> |
A menu where each option is a Command. The <Command> tells Java what kind of thing is in the menu. |
new Selectable<>() |
Makes an empty menu. The <> means "the same type as the field." |
addDefault("Do nothing", Commands.none()) |
Adds an option and makes it the default. Commands.none() is a command that does nothing and ends right away. |
add("Square", squareAuto()) |
Adds another option. squareAuto() runs now, when the menu is built, but only builds the routine; nothing moves yet. |
Tunables.publish("Auto", m_autoChooser) |
Puts the menu on the dashboard |
getSelected() |
Returns the command for whatever the drive team picked |
Why not SendableChooser?
You'll see SendableChooser in older code and examples. It only reaches the dashboard through SmartDashboard.putData, which is going away in WPILib 2027. Selectable is its 2027 replacement, and the same lines of code work in 2027 (Lesson 6.1).
Why \"Do nothing\" is the default
If the drive team forgets to pick, a robot that sits still is far better than one that runs a routine meant for a different starting spot and hits someone else's robot.
Step 4: Run it#
- Deploy. In Elastic, find Tunables → Auto, drag it onto your tab (it shows up as a ComboBox Chooser), and pick Drive forward.
- On blocks first: in the Driver Station, select Autonomous, say "Enabling!", and enable. The wheels run for 2 seconds and stop.
- On the floor: put tape on the floor at the robot's front edge, and run it again. Mark where it stops.
- Run Square on blocks, then on the floor. Adjust the spin time in
squareSide()until it looks close to a square.
Step 5: How repeatable is it?#
Run Square five times without changing anything. Each time, measure how far the robot ends up from where it started.
| Run | Distance from start | Ended facing which way? |
|---|---|---|
| 1 | ||
| 2 | ||
| 3 | ||
| 4 | ||
| 5 |
Then swap in a less charged battery and run it twice more.
What you'll see: The square doesn't close, and it's different every time. Turns that were 90° become 80° or 100°, and the errors add up corner by corner. A weaker battery makes every move shorter.
The problem is the same one from Lab 6's timed attempt: the robot doesn't measure how far it turned. It only knows how long it tried. In Lab 8 you'll add a gyro so it does.
Step 6: Commit#
git add .
git commit -m "Lab 7: timed autonomous"
Finished code
Stuck, or missed a lab? lab07-timed-auto on GitHub is the complete project as it looks at the end of this lab. It's a full WPILib 2026 project you can open in VS Code, build, and deploy. Try to finish the lab yourself first.
Try it#
- Run Drive forward and Square from the chooser
- Tune the spin time until a single turn is close to 90°
- Fill in the repeatability table, including the less charged battery
- Add a Drive backward option to the chooser
- Write a "there and back" routine: forward 1.5 seconds, spin 180°, forward 1.5 seconds. Does it end where it started?
- Make the spin a gentle curve instead of turning in place (hint: both sides forward, one faster)
Check yourself#
What decides which command runs in autonomous?
Whatever getAutonomousCommand() returns. Robot.java schedules it when auto starts and cancels it when teleop starts.
What does Commands.sequence(a, b, c) do?
Runs a, then b after a ends, then c after b ends.
Why does driveForSecondsCommand use finallyDo to stop the motors?
So the motors stop however the command ends: timed out, finished, or canceled when teleop starts.
Why should \"Do nothing\" be the default auto?
If nobody picks one, the robot stays still instead of running a routine meant for a different starting spot.
Why doesn't the timed square end where it started?
Time isn't distance or angle. Battery charge, carpet, and wheel wear change how far each timed move goes, and the errors add up at every corner.