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
DriveForTimeCommandusing WPILib'sTimerclass 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.
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(); } }
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:
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:
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. }
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.
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.
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.
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.
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.
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.
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.
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. |
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.
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
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?
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?
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?
Build, Calibrate, and Break a Timed Routine
- Implement
DriveForTimeCommandandTurnForTimeCommandexactly as shown in this lesson. InRobotContainer, 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. - 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?
- 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.
- 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? - Bonus: Add a
SendableChooseroption to yourgetAutonomousCommand()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.