Unit 9 · Lesson 6

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.

Trajectory PathPlannerTrajectory / Trajectory

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.

Pose2d edu.wpi.first.math.geometry.Pose2d

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.

ChassisSpeeds edu.wpi.first.math.kinematics.ChassisSpeeds

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.

Holonomic Controller PPHolonomicDriveController

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.

Odometry SwerveDrivePoseEstimator

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.

Event Markers NamedCommands (PathPlanner)

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.

Path following control loop — simulated swerve drive stopped
Max pose error
Path complete
Simplified physics — illustrative only

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.

1
Sample the trajectory at current time

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.

2
Read the current robot pose from odometry

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.

3
Compute pose error and generate ChassisSpeeds

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.

4
Convert ChassisSpeeds to swerve module states

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.

5
Check trajectory completion

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.

🔍 Why the feedforward velocity matters as much as the PID

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.

Prerequisites — what must be working before the first path test
// 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.
🔍 Odometry accuracy is the constraint that everything else runs against

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.

💡 Path following is not magic — it's fast, accurate sensor-based control

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 SwerveDrivePoseEstimator updating every loop in periodic()
  • A resetPose(Pose2d) method on your drivetrain subsystem
  • A getChassisSpeeds() method returning the current measured speeds
  • A drive(ChassisSpeeds) method that accepts a ChassisSpeeds and 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

⚙️ Before You Attempt Path Following

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?

  • A The robot continues along its now-offset path and arrives 0.3 meters from the intended endpoint
  • B The path-following command detects the disturbance and immediately restarts the trajectory from the beginning
  • C The holonomic controller detects the pose error introduced by the bump on the next 20 ms loop and computes a corrective ChassisSpeeds that steers the robot back toward the planned trajectory, arriving close to the intended endpoint
  • D The robot stops and waits for the pose error to fall below tolerance before continuing the path

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?

  • A The translation PID kP is too high, causing overcorrection to the right
  • B The robot's starting pose wasn't reset before autonomous — odometry thinks the robot starts 0.8 meters to the right of its actual position, so the controller corrects toward that phantom position throughout the path
  • C The trajectory was drawn on the wrong side of the field in the PathPlanner GUI
  • D One of the swerve modules has an inverted drive motor

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?

  • A Holonomic controllers are faster to compute, which is important for the 20 ms loop
  • B Ramsete controllers require encoder ticks instead of meters, which swerve modules don't provide
  • C A swerve drive can independently control x translation, y translation, and rotation — a holonomic controller generates all three components of ChassisSpeeds simultaneously. A Ramsete controller is designed for differential drives where translation and rotation are always coupled — it can't generate independent y-velocity commands that swerve requires.
  • D Ramsete controllers only work with WPILib trajectories, not PathPlanner trajectories
💪 Practice Prompt

Prepare Your Drivetrain for Path Following

  1. Verify that your drivetrain subsystem exposes all four prerequisites: resetPose(Pose2d), getPose(), getChassisSpeeds(), and drive(ChassisSpeeds). If any are missing, implement them. Add SmartDashboard output in periodic() that publishes the current pose as x, y, and heading in degrees.
  2. 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.
  3. Write a standalone test method in your subsystem called testChassisSpeedsInput() that commands drive(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.
  4. Add m_drive.resetPose(AutoConstants.STARTING_POSE_DEFAULT) to autonomousInit() in Robot.java. Create AutoConstants.STARTING_POSE_DEFAULT as 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.
  5. Bonus: Read the PathPlanner docs at pathplanner.dev for the AutoBuilder.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.