Introduction to Path Following
Sensor-based commands move the robot from point A to point B. Path following moves the robot through a continuous curve — at a precise speed, with a controlled heading, while other actions run simultaneously. This lesson explains the gap between those two approaches and the concepts that bridge it.
By the end of this lesson, you will:
- Explain what a trajectory is and how it differs from a sequence of sensor-based commands
- Describe the role of
Pose2d,ChassisSpeeds, and odometry in path following - Explain why a holonomic drive controller is required for swerve — and what makes it different from a differential drive controller
- Trace the path-following control loop: trajectory → desired pose → pose error →
ChassisSpeeds→ module states - Identify what makes path following more accurate and faster than sequential sensor-based commands
- Articulate what preconditions must be true on the robot before path following can work reliably
Why Sequential Commands Can't Do This
In Lessons 2–4 you built a solid foundation: sensor-based commands that drive a specific distance, turn to a specific angle, and sequence these into multi-step routines. This approach works well. For many FRC games and many teams, it's enough to be competitive.
But there's a fundamental limitation to the "drive, stop, turn, drive" pattern. Every time the robot stops to turn, it loses momentum. The sequence of motions isn't continuous — it's a series of discrete segments joined at hard stops. In a 15-second autonomous period where fractions of seconds matter, those stops add up. More importantly, they make the path longer than necessary: a smooth arc from position A to position B is almost always shorter than the drive-stop-turn-drive equivalent.
Path following solves this by treating the robot's motion as a continuous trajectory — a mathematically defined curve through space with a velocity and heading at every point along it. Instead of commanding "drive 2 meters, then turn 90°," you command "follow this arc from A to B, arriving with this heading and this velocity." The robot never stops. The closed-loop controller corrects for drift at every timestep. The result is motion that's faster, smoother, and more position-accurate than any sequence of discrete commands can achieve.
| Aspect | Sequential sensor-based commands | Path following (trajectory tracking) |
|---|---|---|
| Motion shape | Straight segments with full stops at turns | Continuous curves with no stops mid-path |
| Speed profile | Constant speed per segment, zero between | Velocity planned across entire trajectory with acceleration limits |
| Position feedback | Encoder distance or gyro angle, one at a time | Full 2D pose (x, y, heading) updated every 20 ms |
| Heading control | Separate turn commands | Continuous holonomic heading control, independent of translation |
| Parallel actions | Possible via command groups | Event markers trigger commands at specific path waypoints |
| Odometry required | No — works with local encoder resets | Yes — must know field-relative position at all times |
| Best for | Simple routines, early build season, fallback | Competition-ready multi-piece routines |
The Core Concepts
Path following introduces vocabulary that will appear throughout Lessons 7–10. These are not abstractions — each concept maps directly to a WPILib class or a physical robot behavior.
A time-stamped sequence of states describing the robot's planned position, heading, and velocity at every moment during a path. Generated offline (in the GUI) or at runtime from waypoints. The trajectory is the plan; the controller's job is to execute it.
A robot's complete 2D state: x position (meters), y position (meters), and heading (Rotation2d). The trajectory provides a desired Pose2d at each timestep; odometry provides the actual Pose2d. The difference between them is the pose error that the controller must correct.
Three-component velocity: vx (m/s forward), vy (m/s sideways), and ω (rad/s rotation). The path-following controller outputs a ChassisSpeeds object every loop that the drivetrain converts into individual swerve module states via inverse kinematics.
The feedback controller that computes ChassisSpeeds from the pose error. "Holonomic" means it can independently control translation (x, y) and rotation — which is only possible on a swerve drive. Differential drives cannot decouple translation and rotation and require a different controller.
Continuously tracks the robot's field-relative Pose2d by integrating encoder and gyro data every 20 ms. This is the "actual position" measurement the controller compares against the trajectory's desired position. Without accurate odometry, the controller corrects for the wrong error.
Named trigger points along the path that fire robot commands when the trajectory reaches that distance or time. Replace the parallel command group patterns from Lesson 4 — instead of "parallel(drive, intake)," you draw an intake marker at the right point on the path in the GUI.
The Control Loop in Action
The path-following control loop runs every 20 ms. At each timestep, the controller compares where the robot should be (trajectory desired pose) against where it actually is (odometry actual pose) and computes the ChassisSpeeds needed to correct the error. Toggle between open-loop and closed-loop below to see what happens when there's no feedback to correct position drift.
The Path-Following Control Loop Step by Step
Understanding what happens every 20 ms during path following demystifies why it requires accurate odometry, why the holonomic controller has PID gains, and why pose resets before autonomous matter so much.
The path-following command tracks how long it's been running and samples the trajectory at that elapsed time. This gives the desired state: the exact position, heading, and velocity the robot should have at this moment. The trajectory stores these states for every millisecond of the planned path — the sample is just a lookup by time.
The drivetrain subsystem's periodic() method updates odometry every loop from encoder and gyro data, producing the actual pose: the robot's current estimated position and heading on the field. This is the measurement the controller uses for feedback. If odometry drifts significantly, the controller corrects for the wrong error — which is why odometry accuracy is the single most important prerequisite for path following.
The holonomic drive controller receives the desired state (from step 1) and actual pose (from step 2) and computes the ChassisSpeeds needed to close the gap. It combines the trajectory's feedforward velocity (the planned speed at this point) with PID corrections for x error, y error, and heading error — three independent corrections running simultaneously. This is what "holonomic" means in practice: x, y, and θ are controlled independently in the same loop.
SwerveDriveKinematics.toSwerveModuleStates(chassisSpeeds) converts the three-component velocity vector into individual drive speed and steering angle targets for each of the four modules. Each module then uses its own PID (steering) and feedforward (driving) to achieve those targets. Path following doesn't command motors directly — it commands the kinematics layer, which commands modules, which command motor controllers.
The path-following command checks whether the elapsed time exceeds the trajectory's total duration. When it does, the command ends. Some implementations also check final pose error — ending only when the robot is within a tolerance of the last waypoint — to ensure the robot doesn't "finish" the path while still physically moving. PathPlanner handles this check automatically.
The holonomic controller's feedforward term uses the trajectory's planned velocity at each timestep. This means the controller doesn't wait for a position error to develop before commanding speed — it proactively commands the velocity the trajectory planned for this moment, and PID corrections handle residual error. Without feedforward, the robot lags behind the trajectory and accumulates position error throughout the path. With it, the robot tracks the trajectory closely even without highly tuned PID gains. This is the same feedforward + feedback pattern from Unit 8, applied at the drivetrain level to a 2D trajectory.
What the Robot Needs Before Path Following Works
Path following is more demanding than sensor-based commands. It requires several things to be true simultaneously on the physical robot. None of these are optional — each one is a hard dependency.
// 1. ACCURATE ODOMETRY // The robot must continuously track its field-relative Pose2d. // Test: place robot at known position, drive 1 meter forward. // Odometry should read approximately (1.0, 0.0) if starting from origin. // If it reads (0.7, 0.3), your encoder configuration or gyro heading is off. SwerveDrivePoseEstimator poseEstimator = new SwerveDrivePoseEstimator( kinematics, gyro.getRotation2d(), modulePositions, startingPose); // 2. POSE RESET AT AUTO START // Odometry must be zeroed to the robot's actual starting position on the field // before the first path command runs. The trajectory assumes the robot starts // at a specific Pose2d — if odometry shows a different pose, the controller // computes an immediate correction toward the wrong reference. m_drive.resetPose(new Pose2d(1.5, 4.0, Rotation2d.fromDegrees(0))); // 3. CHARACTERIZATION DATA FOR DRIVETRAIN // The holonomic controller uses kV to predict the voltage needed at each // velocity point in the trajectory. Without characterization, feedforward // is effectively zero and the robot relies entirely on PID — causing lag. // Run SysId on the drivetrain before tuning path-following gains. // 4. HOLONOMIC CONTROLLER WITH TUNED PID GAINS // Three independent PID controllers: x translation, y translation, rotation. // Start with kP = 1.0-5.0 for translation, kP = 1.0-3.0 for rotation. // Increase until you see oscillation, then back off 20%. PPHolonomicDriveController controller = new PPHolonomicDriveController( new PIDConstants(5.0, 0, 0), // translation PID new PIDConstants(2.0, 0, 0)); // rotation PID // 5. MODULE STATES APPLIED CORRECTLY // Each call to the path-following controller produces a ChassisSpeeds object. // Your drivetrain must convert this to module states and apply them every loop. // If your setModuleStates() method is missing, path following outputs will be // computed but not sent to any motor controller.
At competition I've seen teams deploy perfectly correct PathPlanner code and watch the robot drive off into a wall. The path looked right in the GUI. The controller gains were reasonable. The code compiled clean. The problem: their odometry hadn't been reset to the actual starting pose before auto, so the controller thought the robot started 1.5 meters left of where it actually was. Every correction it computed was wrong because it was correcting for a position error that didn't exist. The fix is always the same: reset odometry to the exact starting pose before the first path command, and verify that pose by watching it in AdvantageScope or SmartDashboard immediately before enabling. If odometry shows the wrong pose at enable, no path will track correctly.
Why Path Following Is Especially Powerful on Swerve
Path following exists for both differential and swerve drivetrains, but its advantages are compounded on swerve. A differential drive can follow a curved path, but it must rotate the robot to change direction — heading and translation are always coupled. A swerve drive can follow a curved path while maintaining any heading, independently. This decoupling is what "holonomic" means.
In autonomous, holonomic path following means the robot can:
- Drive in a curve while keeping the intake pointed at a game piece source
- Arrive at a scoring position facing the target without a dedicated turn command
- Travel from one scoring position to another via the most geometrically efficient path, regardless of the required headings at each end
- Correct for drift in two dimensions simultaneously rather than sequentially
The PathPlanner holonomic controller used in Lesson 7 exploits all of this. You define the robot's heading at each waypoint independently of its direction of travel, and the controller maintains that heading constraint throughout the path without any additional turn commands.
Everything in path following is built on concepts from earlier in this unit and Unit 8. The trajectory is a pre-planned motion profile (Unit 8, Lesson 11 — TrapezoidProfile). The controller is feedforward plus PID feedback (Unit 8, Lesson 10). Odometry is a Kalman filter (Unit 7). Event markers are command groups (Lesson 4). PathPlanner wraps all of these into a tool that handles the geometry and timing automatically. When something goes wrong with path following, the debugging starts with the same questions: is the sensor data correct? Are the controller gains reasonable? Did the pose reset happen? These are the questions you already know how to ask.
What Comes Next: PathPlanner
Lesson 7 puts everything from this lesson into practice with PathPlanner — the GUI path editor and Java library used by Team 2910 and most top FRC teams. You'll draw paths, configure constraints, register event markers, and generate the AutoBuilder chooser.
Before moving to Lesson 7, you should have the following working on your robot:
- A
SwerveDrivePoseEstimatorupdating every loop inperiodic() - A
resetPose(Pose2d)method on your drivetrain subsystem - A
getChassisSpeeds()method returning the current measured speeds - A
drive(ChassisSpeeds)method that accepts aChassisSpeedsand applies it to the modules - SysId characterization data for the drivetrain (kS, kV, kA) from Lesson 8.13
If any of these are missing, path following will either fail silently or produce incorrect motion. Verify each one independently before running the first PathPlanner path.
🔌 System Check
Path following depends on a working drivetrain stack. Verify these before running any trajectory:
- Odometry tracks correctly over a straight 1-meter drive. Push the robot forward exactly 1 meter from a known starting pose. SmartDashboard should show x ≈ 1.0, y ≈ 0.0, heading ≈ 0°. Errors above 5 cm suggest encoder scaling, gear ratio, or wheel diameter issues. Fix these before any path test — odometry error accumulates over longer paths.
- Gyro doesn't drift over a full 360° rotation. Rotate the robot slowly by hand through a complete revolution. Gyro should read within 2° of 0° when returned to starting position. Significant drift indicates mounting issues or missed initialization steps.
- The drivetrain's drive(ChassisSpeeds) method works correctly. Command a slow forward speed (
ChassisSpeeds(0.5, 0, 0)) and verify all four modules drive forward with correct speed and zero steering. If modules crab sideways or spin in wrong directions, the kinematics constants or module inversions need fixing. - Pose reset is wired into autonomousInit(). Confirm that
m_drive.resetPose(startingPose)is called before the first path command. Log the post-reset pose to SmartDashboard. Enable auto and verify the logged pose matches the robot's actual position on the field. - The robot is safe to run at path-following speeds. Path following will command higher speeds than manual sensor-based tests. Before the first full-speed path test, run at 50% of planned maximum velocity and verify the robot tracks the path roughly correctly before opening up to full speed.
Knowledge Check
1. A swerve robot running path following is commanded to follow an arc from (0,0) to (3,2) while maintaining a 0° heading throughout. Halfway through the path, another robot bumps it sideways by 0.3 meters. With closed-loop path following, what happens next?
2. A team deploys a PathPlanner routine. The robot drives in roughly the right direction but consistently curves 0.8 meters to the left of the planned path throughout the entire trajectory. Odometry appears correct. What is the most likely cause?
3. Why does path following require a holonomic controller specifically for a swerve drive, rather than the differential-drive Ramsete controller used in WPILib examples?
Prepare Your Drivetrain for Path Following
- Verify that your drivetrain subsystem exposes all four prerequisites:
resetPose(Pose2d),getPose(),getChassisSpeeds(), anddrive(ChassisSpeeds). If any are missing, implement them. Add SmartDashboard output inperiodic()that publishes the current pose as x, y, and heading in degrees. - Test odometry accuracy over a 1-meter straight drive: place the robot at (0,0), drive forward 1 meter at 40% speed using a timed command, stop, and read the SmartDashboard pose. Record the x, y, and heading values. Repeat three times. Is x within 5 cm of 1.0? Is y within 3 cm of 0.0? If not, recalibrate the encoder distance-per-rotation or wheel diameter.
- Write a standalone test method in your subsystem called
testChassisSpeedsInput()that commandsdrive(new ChassisSpeeds(0.5, 0, 0))for 1 second and then stops. Deploy and run with the robot on blocks. All four modules should spin forward at the same speed. If any module is wrong direction or different speed, fix it before proceeding. - Add
m_drive.resetPose(AutoConstants.STARTING_POSE_DEFAULT)toautonomousInit()inRobot.java. CreateAutoConstants.STARTING_POSE_DEFAULTas a constant representing your robot's typical center-field starting position. Log the pose on SmartDashboard and verify it reads correctly when you enable autonomous mode with the robot physically placed at that position. - Bonus: Read the PathPlanner docs at
pathplanner.devfor theAutoBuilder.configure()method. List the four parameters it requires and identify which drivetrain method in your subsystem provides each one. This is exactly what you'll implement in Lesson 7 — doing the mapping now means Lesson 7 is configuration, not investigation.