Unit 9 · Lesson 2

Timed Movements Approach (and Why It Fails)

Every FRC programmer writes timed autonomous first. It works in the shop. It fails at competition. This lesson teaches you both how to do it correctly and exactly why it breaks — so you know what you're leaving behind when Lesson 3 introduces sensor-based control.

By the end of this lesson, you will:

  • Implement a correct DriveForTimeCommand using WPILib's Timer class and all four command lifecycle methods
  • Assemble a multi-step timed autonomous routine using Commands.sequence()
  • Explain the three physical variables — battery voltage, carpet friction, and external contact — that cause timed routines to drift at competition
  • Quantify drift: given a specific battery voltage change, estimate how far a timed drive command will overshoot or undershoot
  • Describe the two legitimate uses for timed commands in a competition codebase
  • Recognize which WPILib built-in commands can replace manual timed commands in most situations

What Timed Movement Is

A timed autonomous movement is exactly what it sounds like: you command a motor to run at a fixed duty cycle for a fixed number of seconds, then stop. No encoder. No gyro. No closed-loop feedback of any kind. The robot is blind — it has no way to know how far it actually traveled, whether it got bumped off course, or whether the battery voltage changed between your shop test and the competition match.

The code pattern is built around WPILib's Timer class. You start the timer in initialize(), apply a motor output in execute(), and check elapsed time in isFinished(). When the timer passes your threshold, the command ends.

Before looking at code, understand the conceptual model clearly: the robot is making a bet. It bets that "if I drive at 50% output for 1.5 seconds, I'll have traveled approximately 1.2 meters." That bet is correct when the battery is fully charged, the carpet is typical, and nobody bumps the robot. Every deviation from those assumed conditions costs distance accuracy.

Building a Correct Timed Command

A common mistake in timed commands is putting the motor command in initialize() and leaving execute() empty. This appears to work but creates a subtle problem: if the command is interrupted and restarted, initialize() runs again and resets the timer, but the motor was still running from the previous run. Structuring all repeating logic in execute() and timer management in initialize() avoids this.

DriveForTimeCommand.java
import edu.wpi.first.wpilibj.Timer;
import edu.wpi.first.wpilibj2.command.Command;

public class DriveForTimeCommand extends Command {

    private final DriveSubsystem m_drive;
    private final double m_speed;      // duty cycle, -1.0 to +1.0
    private final double m_duration;   // seconds
    private final Timer  m_timer = new Timer();

    public DriveForTimeCommand(DriveSubsystem drive, double speed, double seconds) {
        m_drive    = drive;
        m_speed    = speed;
        m_duration = seconds;
        addRequirements(drive);
    }

    @Override
    public void initialize() {
        m_timer.reset();
        m_timer.start();
        // Reset and start here — not in the constructor.
        // The constructor runs once when RobotContainer is built,
        // which may be minutes before autonomous actually runs.
    }

    @Override
    public void execute() {
        m_drive.arcadeDrive(m_speed, 0);
        // Apply output every loop. Even though the output
        // doesn't change, motor controllers may timeout
        // without regular update commands on some configurations.
    }

    @Override
    public boolean isFinished() {
        return m_timer.hasElapsed(m_duration);
    }

    @Override
    public void end(boolean interrupted) {
        m_drive.stop();
        m_timer.stop();
    }
}
💡 Reset the timer in initialize(), not the constructor

RobotContainer's constructor runs during robotInit() — at boot time, which might be several minutes before autonomous starts. If you call m_timer.reset() and m_timer.start() in the constructor, the timer is already counting by the time autonomous begins. The first time isFinished() runs, the elapsed time will be much larger than you expect. Always reset and start the timer in initialize(), which is called immediately before the command starts running.

A Turn Command

The same pattern applies to any mechanism action. For a timed turn, you pass a rotation speed instead of a forward speed:

TurnForTimeCommand.java — simplified version
public class TurnForTimeCommand extends Command {

    private final DriveSubsystem m_drive;
    private final double m_rotation;   // positive = counterclockwise
    private final double m_duration;
    private final Timer  m_timer = new Timer();

    public TurnForTimeCommand(DriveSubsystem drive, double rotation, double seconds) {
        m_drive    = drive;
        m_rotation = rotation;
        m_duration = seconds;
        addRequirements(drive);
    }

    @Override
    public void initialize() { m_timer.reset(); m_timer.start(); }

    @Override
    public void execute() { m_drive.arcadeDrive(0, m_rotation); }

    @Override
    public boolean isFinished() { return m_timer.hasElapsed(m_duration); }

    @Override
    public void end(boolean interrupted) { m_drive.stop(); m_timer.stop(); }
}

Assembling a Timed Autonomous Routine

These commands become useful when assembled into a sequence in RobotContainer. The readable structure below is the primary educational value of timed commands — it demonstrates command sequencing clearly without the added complexity of sensor math:

RobotContainer.java — timed auto sequence
public Command getAutonomousCommand() {
    return Commands.sequence(
        // Drive forward at 40% for 1.2 seconds
        new DriveForTimeCommand(m_drive, 0.4, 1.2),

        // Pause 0.1 s to let the robot settle after stopping
        Commands.waitSeconds(0.1),

        // Turn left at 30% for 0.85 seconds (tuned by observation)
        new TurnForTimeCommand(m_drive, 0.3, 0.85),

        // Drive forward again at 40% for 0.8 seconds
        new DriveForTimeCommand(m_drive, 0.4, 0.8)
    );
    // Total budget: about 3 seconds of the 15-second auto period.
    // Notice how the durations are tuned magic numbers — not derived
    // from any measurement of the robot's actual physical travel.
}
💡 WPILib already has Commands.waitSeconds()

You don't need to write a custom "wait" command for pauses between timed actions. Commands.waitSeconds(seconds) is a built-in factory method that creates a timed command with no motor output — it simply waits for the specified duration and finishes. Use it freely in sequences to give mechanisms time to settle between steps. Similarly, Commands.runOnce(() -> subsystem.doSomething()) handles one-shot actions without a full command class.

Why Timed Routines Fail at Competition

A timed routine is a model of the real world that assumes specific physical conditions. When those conditions change — and they always do — the model is wrong, and the robot goes somewhere it wasn't supposed to. There are three primary failure modes, each grounded in physics your robot has no way to detect or compensate for.

🔋 Battery voltage variation

A duty-cycle command (like arcadeDrive(0.5, 0)) commands 50% of whatever the current bus voltage is. At 12.6V (fresh battery), that's about 6.3V to the motors. By the end of a match cycle, the battery may be at 11.2V, delivering only 5.6V. The robot travels noticeably less distance in the same time — easily 10–15% less over a 1.5-second drive segment. A routine tuned at 12.6V will consistently undershoot at 11.2V.

🟫 Carpet friction variation

FRC carpet has a surface texture that varies between rolls, event locations, and wear patterns. A robot tuned on the team's shop carpet may encounter different rolling resistance at a competition venue. The difference is small — typically 2–5% — but over a 2-meter drive segment, that's 4–10 centimeters of position error. Enough to miss a scoring zone entirely if the tolerance is tight.

💥 External contact (defense)

Another robot can legally make contact with yours during the autonomous period in many FRC games. A 150-lb robot nudging yours sideways by 6 inches is impossible for a timed routine to detect or correct. The timer keeps running, the motors keep running, and your robot arrives at entirely the wrong position with no indication anything went wrong.

📍 Starting position variation

If the alliance station personnel place your robot 2 inches away from your intended starting position, every subsequent timed movement inherits that error. Sensor-based routines can reset their reference point mid-routine (e.g., from a vision target or AprilTag). Timed routines cannot — they have no reference point at all, so any initial error compounds through every step.

🌡️ Temperature effects

Motors run hotter as a match day progresses. Thermal resistance in the windings changes the motor's effective torque constant, and brushed motor contact resistance increases with temperature. The result is slightly less torque per volt at higher temperatures — a subtle effect that makes early-day routines marginally more aggressive than late-day ones. Small, but real.

⏱️ Error accumulation

Each timed segment in a sequence is independent — it doesn't know whether the previous segment arrived at the right place. A 3% undershoot on the first drive segment means the turn starts from the wrong position, which means the second drive segment ends even further from the intended destination. Errors don't average out across a sequence; they add.

The Drift Simulator: See It Happen

Adjust the sliders below to simulate real-world conditions and run the "same" timed routine under each scenario. The routine is: drive forward 40% for 1.5 seconds. In ideal shop conditions it travels exactly 1.0 m. Watch how each variable shifts the actual landing position.

Timed routine drift simulator 1.5 s at 40% duty cycle — target: 1.0 m
Environmental conditions
Battery voltage 12.6 V
Carpet friction (μ) 0.85
Defense push (cm) 0 cm
Robot parameters
Duty cycle 40%
Duration (s) 1.5 s
Robot mass (kg) 45 kg
Simplified physics model — illustrative, not exact
Field view (top-down) — target zone shown in green
Target 1.00 m
Actual distance
Error
Lateral drift

When Timed Commands Are Acceptable

Timed movement isn't zero-value — it has two legitimate roles in a competition codebase. Outside of these, sensor-based control should always be preferred.

Situation Timed OK? Rationale
Mechanism without a position sensor ✓ Yes If there is genuinely no encoder or other sensor on a mechanism (e.g., a simple intake roller), a timed run is the only option. Keep the duration conservative to avoid stalling.
Fallback when a sensor fails at an event ✓ Yes A working timed routine is better than no autonomous at all. Keep a commented-out or chooser-selectable timed fallback for sensor failure scenarios.
Early build-season prototyping ✓ Yes Timed commands let you test command structure and sequencing before encoders are installed or configured. Replace before competition.
Waiting for a mechanism to settle ✓ Yes Commands.waitSeconds() between motion commands gives mechanisms time to physically stop oscillating before the next command begins. Common and valid.
Competition drivetrain movement ✗ No Battery sag and field variation make distance accuracy unreliable. Use encoder-based or gyro-based movement for any drivetrain action that must be precise.
Any action that must reach a specific position ✗ No If missing by even a few centimeters matters (scoring zone, alliance wall, obstacle clearance), timed movement is not reliable enough. Use sensor-based control.
Multi-step routines where errors accumulate ✗ No Error from each step adds to the next. A three-step timed sequence can arrive at the final position with 20–30 cm of accumulated error even if each step was only 5–10% off.
🔍 The fallback argument is real — but plan for it deliberately

At competition, when an encoder fails or a CAN device browns out and stops reporting, having a timed fallback auto that at least drives the robot out of the starting zone is genuinely better than sitting still. Teams that plan this deliberately — a SendableChooser option labeled "Timed Fallback" that they keep up to date — handle sensor failures gracefully. Teams that only have sensor-based code and no timed fallback are stranded when hardware fails. This is not an argument to use timed routines as your primary strategy. It's an argument to keep a simple timed routine ready alongside your main autonomous at every event.

WPILib's Built-In Timed Commands

Before writing a custom timed command, check whether WPILib already provides what you need. These factory methods on the Commands class and subsystem fluent API cover the most common patterns:

  • Commands.waitSeconds(double seconds) — waits the specified time with no motor output. Use between commands to let mechanisms settle.
  • command.withTimeout(double seconds) — decorates any existing command with a timeout. If the command hasn't finished by the deadline, it's interrupted. Excellent for sensor-based commands that might get stuck: new DriveDistanceCommand(...).withTimeout(3.0).
  • Commands.runOnce(() -> subsystem.method()) — runs a lambda once and immediately finishes. Use for single-shot actions like resetting encoders or setting a solenoid state.
  • Commands.run(() -> subsystem.method(), subsystem).withTimeout(seconds) — the inline equivalent of a full timed command class. Useful for very simple timed actions that don't warrant their own file.
💡 .withTimeout() is often better than a custom timed command

For sensor-based commands that could theoretically get stuck — say, an intake that waits for a beam break that never trips because a game piece is jammed — .withTimeout() provides a safety net without requiring a separate timed command class. The command tries to reach its sensor goal, and if it takes more than the timeout, it moves on. This combines the reliability of sensor-based logic with the safety of a time ceiling. It's a pattern used extensively in championship code.

🔌 System Check

⚙️ Before Running a Timed Routine on a Real Robot

Timed routines are the simplest autonomous approach but still require physical precautions before enabling:

  • Battery is fully charged before calibrating durations. Tune your timed durations with the battery you'll use at competition — ideally a fully charged competition battery. A duration tuned at 11.8V will overshoot at 12.6V. Record the battery voltage when you calibrate.
  • Test on competition carpet, not shop flooring. Most shop floors are harder and smoother than FRC carpet. The friction coefficient difference can produce 5–8% distance error. If you can't get competition carpet, note this as a known margin of error.
  • Clear a minimum 2× expected travel distance before enabling. Timed routines can't stop early if something is in the way. Make sure the robot has room to complete the full routine plus margin.
  • Have someone on the disable button the entire time. Timed routines have no way to detect obstacles or unexpected situations. A human observer ready to disable is the only safety layer available.
  • Run the routine at least 5 times and measure the actual landing position. One successful run doesn't mean the routine is reliable. Run it at least five times on the same battery at the same charge level and measure the variance. If you're seeing more than ±3 cm variance on a 1-meter drive, investigate before calling it done.

Knowledge Check

1. A team tunes their timed forward drive to land exactly on a scoring marker during shop testing with a fully charged battery at 12.6V. At competition, they run four matches with a battery that averages 11.4V. Which of the following best describes what will happen to the routine's performance?

  • A The routine will perform identically because the duty cycle command is voltage-independent
  • B The routine will overshoot because the motors receive more voltage at competition
  • C The routine will consistently undershoot because a lower bus voltage produces less torque and speed at the same duty cycle, so the robot travels less distance in the same time
  • D The routine will sometimes overshoot and sometimes undershoot due to random noise, not battery voltage

2. A team has a sensor-based DriveDistanceCommand that stops when the encoder reads 2.0 meters. During testing they notice that on slippery carpet, the command sometimes never finishes because the wheels spin without the encoder advancing. What is the best way to handle this without removing the sensor-based logic?

  • A Replace the sensor-based command with a timed command that drives for 2.5 seconds
  • B Add .withTimeout(4.0) to the sensor-based command so it terminates after 4 seconds even if the encoder never reaches 2.0 m, keeping sensor-based logic as the primary and timed as the safety net
  • C Increase the encoder distance target to 2.5 m to account for wheel slippage
  • D Add a second encoder and average the two readings

3. A new programmer puts m_timer.reset() and m_timer.start() in the command's constructor instead of initialize(). RobotContainer is built during robotInit(), which runs 45 seconds before the match begins. What happens when autonomous starts?

  • A isFinished() returns true immediately on the first loop because the timer already shows 45+ seconds of elapsed time — far longer than the configured duration — so the command ends without moving the robot
  • B The robot drives for exactly the configured duration because WPILib resets all timers at autonomous enable
  • C The command runs indefinitely because the timer was already stopped before autonomous began
  • D The code produces a compile error because Timer cannot be called in a constructor
💪 Practice Prompt

Build, Calibrate, and Break a Timed Routine

  1. Implement DriveForTimeCommand and TurnForTimeCommand exactly as shown in this lesson. In RobotContainer, assemble a sequence: drive forward 1 second → wait 0.1 s → turn left 0.5 seconds → drive forward 0.8 seconds. Deploy and run with the robot elevated so the wheels spin freely.
  2. If you have a robot on carpet: run the sequence three times. After each run, mark the robot's landing position with a piece of tape. Measure the variance in landing position across the three runs. Write down the measured distance and variance. Is it the same every time?
  3. Change the battery to one that is significantly less charged (if available) and run the same sequence. Does the robot land in the same place? Record the difference in centimeters.
  4. Replace the forward drive steps with Commands.run(() -> m_drive.arcadeDrive(0.4, 0), m_drive).withTimeout(1.0) instead of a custom command class. Verify the behavior is identical. Discuss in a comment: when would you use the inline form vs. a named command class?
  5. Bonus: Add a SendableChooser option to your getAutonomousCommand() called "Timed Fallback" that returns the timed sequence, alongside your main sensor-based auto. Deploy and confirm you can switch between them on SmartDashboard without redeploying code. This is the emergency fallback pattern you should carry into every competition.