Unit 5 · Lesson 8
⭐ Unit 5 Capstone

Basic Autonomous

Autonomous is where everything in Unit 5 converges: lifecycle methods set the stage, sensors provide position feedback, motor controllers execute movement, state machines sequence the steps, and SmartDashboard lets the drive team select the strategy. This lesson builds a complete, sensor-verified autonomous routine from scratch.

By the end of this lesson, you will:

  • Implement a time-based autonomous that sequences multiple actions using a Timer
  • Upgrade to encoder-based autonomous that uses measured distance rather than elapsed time
  • Add gyroscope heading correction to keep a straight-line drive from drifting
  • Chain autonomous states into a multi-step routine with drive, turn, and score actions
  • Integrate SendableChooser from Lesson 6 to let the drive team select the auto from the dashboard

Autonomous Is Just a State Machine With a Timer

Teleop reacts to live input — the drive team tells the robot what to do every 20ms. Autonomous has no driver; it must sequence its own behavior. The state machine pattern from Lesson 7 applies directly: each state represents one phase of the autonomous routine, and transitions fire when the phase is complete (timer expired, distance reached, sensor triggered).

The key difference from teleop state machines: autonomous transitions are driven by time and sensors, not by button presses.

Autonomous Sequence Tracer

Step through a five-state autonomous routine below. Each step shows the active state, the code that runs this cycle, and the condition that will advance to the next state. Notice how autonomousInit() primes the state and resets the hardware; autonomousPeriodic() advances it each cycle.

Auto Sequence Tracer — Drive 1.5m, Turn 90°, Drive 0.5m, Score
⚙️INIT
➡️DRIVE_FWD
↩️TURN_90
➡️DRIVE_FWD2
🎯SCORE
Code running this cycle
click Next to begin
Sensor / transition values
step through to see live sensor values
cycle 0

The Three Autonomous Approaches

The simplest approach: use Timer.getMatchTime() or a Timer object. Drive for N seconds, then stop. Fast to implement, but inaccurate — battery voltage, friction, and floor surface all affect how far the robot travels in a given time.

private final Timer autoTimer = new Timer(); @Override public void autonomousInit() { autoTimer.restart(); autoState = AutoState.DRIVE_FORWARD; gyro.reset(); } @Override public void autonomousPeriodic() { switch (autoState) { case DRIVE_FORWARD: drive.arcadeDrive(0.4, 0.0); if (autoTimer.hasElapsed(2.0)) { // 2 seconds at 0.4 power ≈ 1.5m (varies) drive.arcadeDrive(0.0, 0.0); autoState = AutoState.DONE; } break; case DONE: drive.arcadeDrive(0.0, 0.0); break; } }

Encoder-based autonomous uses the distance the wheel has actually traveled to determine when to transition — much more reliable than time. Reset encoder positions in autonomousInit() and read them each cycle.

@Override public void autonomousInit() { leftMotor.setPosition(0.0); // reset TalonFX integrated encoder rightMotor.setPosition(0.0); gyro.reset(); autoState = AutoState.DRIVE_FORWARD; } private double getDistanceM() { double rotations = leftMotor.getPosition().getValueAsDouble(); return (rotations / Constants.GEAR_RATIO) * Math.PI * Constants.WHEEL_DIAMETER_M; } @Override public void autonomousPeriodic() { switch (autoState) { case DRIVE_FORWARD: drive.arcadeDrive(0.4, 0.0); SmartDashboard.putNumber("Auto Dist m", getDistanceM()); if (getDistanceM() >= 1.5) { // stops at EXACTLY 1.5m regardless of time drive.arcadeDrive(0.0, 0.0); autoState = AutoState.DONE; } break; case DONE: drive.arcadeDrive(0.0, 0.0); break; } }

Even encoder-based autonomous drifts sideways if the left and right wheels have different friction. A gyroscope heading correction applies a small rotational correction proportional to the heading error, keeping the robot on a straight bearing while still using encoder distance for state transitions.

private static final double HEADING_KP = 0.03; // tune this value @Override public void autonomousPeriodic() { switch (autoState) { case DRIVE_FORWARD: double headingError = gyro.getYaw().getValueAsDouble() - 0.0; // target: 0° (straight) double correction = headingError * HEADING_KP; // Positive heading error = drifting left → apply rightward correction drive.arcadeDrive(0.4, -correction); SmartDashboard.putNumber("Heading Err", headingError); SmartDashboard.putNumber("Correction", correction); if (getDistanceM() >= 1.5) { drive.arcadeDrive(0.0, 0.0); leftMotor.setPosition(0.0); // reset for next segment rightMotor.setPosition(0.0); autoState = AutoState.TURN_90; } break; case TURN_90: double turnError = gyro.getYaw().getValueAsDouble() - 90.0; // target: 90° left turn drive.arcadeDrive(0.0, 0.3 * Math.signum(turnError)); // rotate until at target if (Math.abs(turnError) < 2.0) { // within 2° — close enough drive.arcadeDrive(0.0, 0.0); autoState = AutoState.DONE; } break; } }

Auto Strategy Selector

Competition robots need multiple autonomous options for different starting positions, game piece availability, and alliance strategy. The SendableChooser from Lesson 6 feeds autonomousInit(). Select a strategy below to see the state sequence and required sensors.

Auto Strategy Scenarios click to see the design
01 Do Nothing — robot stays put, safe fallback
02 Drive Forward 1.5m — clear the starting zone
03 Drive + Score — drive to scoring zone and score preloaded game piece
04 Two-Piece — score preloaded, collect second, score again
📈 State sequence

🔌 Required sensors & notes

The Complete Auto + Chooser Integration

// Everything from Unit 5 in one place public class Robot extends TimedRobot { // Hardware (Lessons 3 + 4) private final TalonFX leftMotor = new TalonFX(Constants.DRIVE_LEFT_ID); private final TalonFX rightMotor = new TalonFX(Constants.DRIVE_RIGHT_ID); private final Pigeon2 gyro = new Pigeon2(Constants.GYRO_ID); private final DifferentialDrive drive; // Controller (Lesson 2) private final XboxController controller = new XboxController(0); // Auto strategy chooser (Lesson 6) private final SendableChooser<String> autoChooser = new SendableChooser<>(); // State machine (Lesson 7) private AutoState autoState = AutoState.IDLE; public Robot() { drive = new DifferentialDrive(leftMotor, rightMotor); } // ── Lesson 1: lifecycle ──────────────────────────────────────── @Override public void robotInit() { configureMotors(); autoChooser.setDefaultOption("Do Nothing", "none"); autoChooser.addOption("Drive 1.5m", "drive"); autoChooser.addOption("Drive + Score", "score"); SmartDashboard.putData("Auto", autoChooser); } @Override public void robotPeriodic() { // Lesson 6: always-on dashboard SmartDashboard.putNumber("Battery V", RobotController.getBatteryVoltage()); SmartDashboard.putNumber("Left Amps", leftMotor.getStatorCurrent().getValueAsDouble()); SmartDashboard.putNumber("Distance m", getDistanceM()); SmartDashboard.putNumber("Yaw Deg", gyro.getYaw().getValueAsDouble()); SmartDashboard.putString("Auto State", autoState.name()); } @Override public void autonomousInit() { // Lesson 1: init once leftMotor.setPosition(0.0); rightMotor.setPosition(0.0); gyro.reset(); String selected = autoChooser.getSelected(); autoState = selected.equals("drive") ? AutoState.DRIVE_FORWARD : selected.equals("score") ? AutoState.DRIVE_FORWARD : AutoState.IDLE; } @Override public void autonomousPeriodic() { // Lesson 7: state machine double headingErr = gyro.getYaw().getValueAsDouble(); switch (autoState) { case DRIVE_FORWARD: drive.arcadeDrive(0.4, -headingErr * HEADING_KP); if (getDistanceM() >= 1.5) { leftMotor.setPosition(0.0); rightMotor.setPosition(0.0); autoState = AutoState.TURN; } break; case TURN: double err = gyro.getYaw().getValueAsDouble() - 90.0; drive.arcadeDrive(0.0, 0.3 * Math.signum(err)); if (Math.abs(err) < 2.0) { autoState = AutoState.SCORE; } break; case SCORE: intake.score(); autoState = AutoState.IDLE; break; case IDLE: drive.arcadeDrive(0.0, 0.0); break; } } @Override // Lesson 5: drive public void teleopPeriodic() { double scale = controller.getRightBumper() ? 1.0 : 0.6; drive.arcadeDrive( MathUtil.applyDeadband(-controller.getLeftY(), 0.05) * scale, MathUtil.applyDeadband( controller.getRightX(), 0.05) * scale); } private double getDistanceM() { return (leftMotor.getPosition().getValueAsDouble() / Constants.GEAR_RATIO) * Math.PI * Constants.WHEEL_DIAMETER_M; } }
🔍 LRI Observation

The autonomous routine that works every single time at competition is the one your team ran twenty times at home before the event. Not ten times, not five. Twenty. I watch teams arrive at their first event with an autonomous they "tested once and it worked." It doesn't work at the event — different floor material, different battery state, slightly different starting position. The teams that score in auto consistently are the teams that iterated. They have a drive distance constant in Constants.java that's been adjusted to four decimal places because they tuned it on a tape measure. They don't wonder if auto will work. They know.

⚙️ 🔌 Unit 5 Capstone System Check
  • Reset encoders and gyro in autonomousInit(), not robotInit(). You want distance measured from the start of autonomous, not from power-on. The gyro heading should be 0° at the moment auto enables, not at boot time.
  • Use encoder distance, not time, as your primary transition condition. Time is unreliable — battery state, floor friction, and motor configuration all affect it. The encoder measures what actually happened.
  • Add a safety timeout to every auto state. If an encoder fails, the robot will never reach the target distance and will be stuck in that state for the whole auto. A secondary time-based fallback catches this: if (autoTimer.hasElapsed(5.0)) autoState = AutoState.DONE;
  • Always call drive.arcadeDrive(0, 0) in the DONE/IDLE state. Without an explicit stop command, DifferentialDrive's motor safety timer will stop motors after 100ms — but the robot will coast during that window. Explicit zero is better.
  • Publish distance, heading error, and state to SmartDashboard. During auto testing, watch these values update in real time. If the heading error is growing, your correction gain is too low. If the robot overshoots, the gain or speed is too high.
  • Test every strategy in the chooser before competition. All three options (Do Nothing, Drive, Drive+Score) should be validated. The "Do Nothing" strategy should literally do nothing — if it accidentally drives the robot, something is wrong with your autoInit() logic.

Knowledge Check

A time-based autonomous drives for 2 seconds at 0.4 output. At practice on a carpeted gym floor, the robot travels 1.5m. At competition on the official field tiles, the same routine only travels 1.2m. What is the most direct fix?
  • 1Increase the timer to 2.5 seconds
  • 2Switch to encoder-based distance — use the wheel encoder to transition at exactly 1.5m regardless of surface friction or motor output; the encoder measures actual wheel travel, making the routine immune to surface and battery variation
  • 3Increase motor output to 0.5
  • 4Use a gyroscope to correct the distance
An autonomous routine gets stuck in the DRIVE_FORWARD state. The robot is stationary. The encoder distance never reaches the target. The routine never advances. What is the most likely cause and correct fix?
  • 1The motor output is too low — increase drive speed
  • 2The encoder position was never reset in autonomousInit(), so it's reading a non-zero starting position; or the encoder cable is disconnected and reads 0 forever; add a safety timeout so the routine advances after a maximum elapsed time regardless of encoder state
  • 3The switch statement is missing a break
  • 4The gyro heading is too far off to allow driving
Which of the following is the correct location for resetting the encoder positions used in autonomous distance calculation?
  • 1robotInit() — reset once at program start
  • 2autonomousPeriodic() — reset every cycle to keep the reading fresh
  • 3autonomousInit() — reset exactly once when autonomous begins so distance is measured from the starting position; resetting in robotInit() gives a distance from power-on; resetting in periodic overwrites the accumulating measurement every cycle
  • 4teleopInit() — to prepare for the next match
💪 Unit 5 Capstone Practice

Build a Complete, Competition-Ready Robot Program

This prompt integrates everything from Unit 5. By the end, you will have a TimedRobot program that can drive, respond to driver input, read sensors, publish diagnostics, select an autonomous strategy, and execute a multi-step autonomous routine.

  1. Lifecycle (L1): Structure Robot.java correctly. All hardware in robotInit(). Dashboard in robotPeriodic(). Mode-specific logic in the appropriate init() and periodic() methods. No blocking calls anywhere.
  2. Drive (L2 + L5): Implement arcade drive with deadband, negated Y-axis, and a 0.6 default speed scale (right bumper for full speed). Verify direction before proceeding.
  3. Sensors (L3 + L4): Add a beam break sensor. Display its raw and debounced values on SmartDashboard. Implement a basic intake state machine (L7): IDLE → INTAKING → HOLDING → EJECTING with correct transitions.
  4. Dashboard (L6): Add a SendableChooser with three autonomous options. Organize Shuffleboard into Drive and Diagnostics tabs. Confirm all values update while disabled.
  5. Autonomous (L8): Implement at minimum two of your three auto options: "Do Nothing" (literally nothing) and "Drive Forward 1.5m" using encoder distance with gyro heading correction. Add a 4-second safety timeout to every state. Test both options by selecting from the dashboard and confirming correct behavior.
  6. Full Integration: Run a simulated match cycle: DS disabled → auto enabled (2 minutes) → DS disables → teleop enabled → DS disables. Confirm every transition is clean, motors stop in disabledInit(), and the dashboard shows correct state throughout.
  7. Capstone Reflection: Write a paragraph for each Unit 5 lesson (1–8) identifying a specific line or pattern in your final code that demonstrates that concept. This is your roadmap to Unit 6, where every one of these patterns gets promoted into the Command-Based architecture.