Skip to content
IncredibotsAcademy
Incredibots playbook

The FollowPathCommand

Writing a wrapper class to bridge FTCLib's Command Scheduler and PedroPathing.

20 minintermediate

By the end you can

  • Understand why a wrapper class is necessary.
  • Review the FollowPathCommand.java implementation from the team's repository.
  • Understand how isFinished() works with PedroPathing tolerances and stall detection.

PedroPathing is incredible at driving the robot, but it doesn't know anything about FTCLib Commands. If you want to use a SequentialCommandGroup to drive to a position and then run your LaunchBallsCommand, FTCLib needs a way to know when PedroPathing is finished driving!

To solve this, our team uses a Wrapper Class. A wrapper class is a standard FTCLib CommandBase that simply "wraps" around PedroPathing's functions.

Writing the Wrapper

Let's look at FollowPathCommand.java directly from the Unearthed-Alberta repository.

  1. Open your project in Android Studio.
  2. Navigate to TeamCode > src > main > java > org > firstinspires > ftc > teamcode > commands.
  3. Create a new Java Class named FollowPathCommand.
  4. Copy the code below.
package org.firstinspires.ftc.teamcode.commands;
 
import android.util.Log;
import com.arcrobotics.ftclib.command.CommandBase;
import com.pedropathing.follower.Follower;
import com.pedropathing.paths.PathChain;
import com.qualcomm.robotcore.util.ElapsedTime;
 
public class FollowPathCommand extends CommandBase {
    private final Follower follower;
    private final PathChain path;
    private final boolean holdEnd;
 
    // Stall detection variables
    private ElapsedTime stallTimer = new ElapsedTime();
    private final ElapsedTime initializationTimer = new ElapsedTime();
    private static final double STALL_VELOCITY_THRESHOLD = 0.5; // inches per second
    private static final double STALL_TIMEOUT = 500; // milliseconds before giving up
    private static final double END_TOLERANCE = 3.0; // Finish when 3 inches away
    private static final double MINIMUM_RUN_TIME = 500.0; // Ensure at least 500ms of run time
 
    private boolean initialized = false;
 
    public FollowPathCommand(Follower follower, PathChain path, boolean holdEnd) {
        this.follower = follower;
        this.path = path;
        this.holdEnd = holdEnd;
        this.initialized = false;
    }
 
    @Override
    public void initialize() {
        // 1. Tell Pedro to start driving!
        follower.followPath(path, holdEnd);
 
        // 2. Force an update immediately so the state locks in
        follower.update();
    }
 
    @Override
    public void execute() {
        if (!initialized) {
            initializationTimer.reset();
            stallTimer.reset();
            initialized = true;
        }
 
        double currentVelocity = follower.getVelocity().getMagnitude();
 
        // 3. Stall Detection: If we are moving faster than the threshold, reset the timer
        if (currentVelocity > STALL_VELOCITY_THRESHOLD) {
            stallTimer.reset();
        }
    }
 
    @Override
    public boolean isFinished() {
        // SAFETY: Do not allow the command to finish if it hasn't been running for 500ms.
        // This prevents race conditions where isBusy() is checked before the path latches.
        if (initializationTimer.milliseconds() < MINIMUM_RUN_TIME) {
            return false;
        }
 
        // 4. Finish Logic
        double distanceRemaining = follower.getCurrentPath().getDistanceRemaining();
        boolean closeEnough = distanceRemaining < END_TOLERANCE;
 
        // Finish if Pedro says done, OR we are close enough, OR we stalled (hit a wall)
        return !follower.isBusy() || closeEnough || stallTimer.milliseconds() > STALL_TIMEOUT;
    }
 
    @Override
    public void end(boolean interrupted) {
        if (stallTimer.milliseconds() > STALL_TIMEOUT) {
            Log.w("Follow Path Command", "Path Stalled! Moving to next command.");
            follower.breakFollowing(); // Stop the motors immediately
        }
        initialized = false;
    }
}

Breaking Down the Magic

Why is this file so complicated? Because the real world is messy!

  1. follower.followPath(): This is called inside initialize() to start the movement.
  2. The "Latch" Bug: Sometimes, if you ask PedroPathing "Are you busy?" a millisecond after telling it to follow a path, it says "No" because it hasn't loaded the path yet! We fix this by calling follower.update() immediately, and enforcing a MINIMUM_RUN_TIME of 500ms before it's allowed to finish.
  3. Stall Detection: If your robot drives into a wall or gets stuck on a game piece, PedroPathing will sit there forever trying to reach the end of the path, ruining your Autonomous. The stallTimer constantly checks your velocity. If your robot stops moving (< 0.5 inches/sec) for more than 500ms, it cancels the path and moves on to the next command!
  4. END_TOLERANCE: Just like we learned in the Advanced Control lesson, waiting for exact perfection takes too long. If the robot is within 3 inches of the target, we declare the path "finished" so the shooter can start spinning up early, saving precious time!

Check Your Understanding

Check yourself

Why does the FollowPathCommand implement stall detection using the robot's velocity?