A self-contained autonomous pathing library for an FTC mecanum drivetrain. It offers four interchangeable path followers, a full Pedro-style Bezier path engine, two localizer options, and ready-to-run example and tuning OpModes. Everything is editable live through FTC Dashboard.
- Units: inches, seconds, radians.
- Field frame:
+xforward (away from driver),+yleft, heading CCW-positive. - Use
Pose.fromDegrees(x, y, deg)for human-friendly heading entry.
At the top level the system answers one question, 50+ times per second: "Given where the robot is and where the path wants it to be, what power should each wheel get?" That answer is produced by a pipeline of four stages. Everything else in the library is a piece of one of these stages.
sensors geometry control motors
┌──────────┐ pose ┌──────────┐ target ┌──────────┐ vector ┌──────────┐
│Localizer │ ────────▶ │ Path / │ ───────▶ │ Follower │ ───────▶ │ Mecanum │
│ │ │ PathChain│ │+controllers│ │ Drive │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
where am I? where should how hard, which spin the
I be heading? direction? wheels
Every OpMode does the same thing once per loop, inside Follower.update():
- Localize.
localizer.update()reads the odometry sensors and computes the robot's fieldPose(x, y, heading). - Project onto the path. The follower finds the parameter
t ∈ [0, 1]of the closest point on the activePathto the robot (path.getClosestT(pose)).tis "how far along the path" the robot currently is. - Compute a drive signal. The follower asks its controllers for a field-relative translation
Vectorplus a turn power — this is the only part that differs between the four followers. - Drive.
MecanumDriverotates the field vector into the robot's frame, mixes it with the turn into four wheel powers, normalizes, and writes them to the motors. - Advance. If the robot has reached the end of the current path (within tolerances), the follower steps to the next path in the chain, or finishes.
Because step 3 is the only difference between followers, swapping new PIDFollower(...) for
new PedroFollower(...) changes the behavior while everything else stays identical.
Vector is an immutable 2D field vector with the usual operations (add, scale, dot, rotate,
fromPolar). Pose is a position plus heading. Pose.normalizeAngle wraps angles to [-π, π] so
heading errors always take the shortest way around (turning from 170° to -170° is +20°, not -340°).
A path's shape is a Bezier curve defined by control points. The curve is evaluated with De Casteljau's algorithm — repeated linear interpolation between control points — which is numerically stable for any degree. From the curve we derive everything the followers need:
getPoint(t)— the point on the curve at parametert.getDerivative(t)— the tangent (direction of travel). Mathematically the derivative of a degree-nBezier is a degree-(n-1)Bezier on the control-point differences, so we reuse De Casteljau on those.getSecondDerivative(t)— used for curvature.getCurvature(t) = (x'·y'' − y'·x'') / |d'|³— signed; positive means the curve bends left. This feeds the Pedro follower's centripetal term.length()— arc length, estimated by sampling 100 points and summing the chords.getClosestT(target)— projects the robot onto the curve. It samples coarsely to find the nearest point, then runs a few Newton iterations on the squared-distance gradient to refinet.
BezierLine is a 2-point (straight) curve; BezierPoint is a degenerate single-point curve used for
hold-position behavior.
Translation and heading are decoupled — a mecanum robot can face any direction while moving any
direction. A HeadingInterpolator decides the goal heading at each t:
- constant — face one heading the whole path.
- linear — sweep smoothly from a start to an end heading (uses
normalizeAngleto take the short way). - tangent — face along the path tangent (optionally offset, or reversed to drive backward).
A Path bundles one curve + one heading interpolator + the end conditions that decide when it's
"done" (t-value threshold, translational/heading/velocity tolerances, and a settle timeout). Defaults
come from FollowerConstants so they're tunable in one place.
A PathChain is an ordered list of Paths run back-to-back. PathBuilder is the fluent API for
building one:
PathChain chain = new PathBuilder()
.addLine(new Vector(0, 0), new Vector(24, 0))
.setConstantHeadingInterpolation(0)
.addCurve(new Vector(24, 0), new Vector(40, 0), new Vector(48, 24))
.setLinearHeadingInterpolation(0, Math.toRadians(90))
.build();A Localizer reports the robot's field pose and velocity. update() is called once per loop; the
factory Localizers.create() builds the one chosen in LocalizerConstants.
Wraps a goBILDA Pinpoint computer, which fuses two odometry pods with an internal IMU and reports pose directly. The wrapper just maps the Pinpoint frame to the library convention (Pinpoint X → field x forward, Pinpoint Y → field y left, heading in radians) and converts to inches.
Computes pose itself from three encoders + the hub IMU:
- Two parallel pods measure forward travel (averaged); one perpendicular pod measures strafe.
- Heading comes from the IMU (more reliable than integrating the wheels, which drift on slip).
- The perpendicular pod's reading is corrected for the part caused purely by rotation about the
tracking center (
strafe − perpOffset·Δheading). - The robot-frame displacement is integrated with a pose-exponential (constant-curvature) model, so motion along a curve is tracked accurately rather than as a straight chord.
setPose lets you align odometry to the field at the start of autonomous (it stores a heading offset
so the IMU's absolute yaw maps to your chosen starting heading).
These are the building blocks the followers combine. All live in control/.
- PIDController — proportional-integral-derivative feedback. Hardened per the FRC controls book:
real
dtin seconds with a first-loop guard, integral anti-windup clamping, output saturation limits, and a tolerance band that zeroes tiny errors to stop jitter. - SquIDController — output is
sqrt(kP·error)·sign(error). It grows with the square root of error, so it pushes hard far away but eases in near the target without needing a D term. - Feedforward —
kS·sign(v) + kV·v + kV2·v·|v| + kA·a. This is open-loop: given a desired velocity and acceleration it predicts the power needed.kS/kV/kAare the FRC book §5.10 terms (static friction + velocity + acceleration).kV2·v·|v|is an optional quadratic-drag term (off by default, see below). - TrapezoidProfile — given a distance and velocity/acceleration limits, produces the commanded
position/velocity/acceleration at any time: accelerate, optionally cruise, decelerate to a stop.
From FRC book appendix H.4. Kept for reference; the
FeedforwardFollowernow uses the S-curve. - SCurveProfile — a jerk-limited version of the trapezoid: the acceleration itself ramps at a
bounded rate
maxJerk(seven phases) instead of stepping instantly. Smoother, less wheel slip. AsmaxJerk → ∞it collapses back to the trapezoid. - KinematicLimiter — an acceleration-aware speed cap for the followers that have no profile of
their own. Each loop it returns the fastest safe speed: brake via
v=√(2·a·d)into the target and ramp up via a slew-rate limitΔv ≤ a·dton launch.
Feedback vs feedforward: feedback (PID/SQuID) reacts to error after it happens; feedforward
predicts the effort before error appears. The best follower (FeedforwardFollower) uses
feedforward for the bulk of the effort and a small PID to clean up the residual — the standard
motion-profiled control structure.
This library accounts for acceleration in four distinct ways. They are not alternatives to each other; they operate at different points in the pipeline, and a follower may use more than one.
| Mechanism | What it is | Acts on | Used by | Tune |
|---|---|---|---|---|
Linear accel feedforward kA·a |
Predicts the extra power to change velocity at a commanded rate. Needs a profile to supply a. Voltage↔accel is linear on FTC drivetrains, so this term stays linear (not quadratic). |
the commanded acceleration setpoint | FeedforwardFollower | kA |
| S-curve / jerk limit | Shapes the profile so acceleration ramps at ±maxJerk instead of jumping. Trades a little time for smoothness; cuts wheel slip and shake. |
the motion profile | FeedforwardFollower | maxJerk |
| Kinematic velocity cap | Gives feedback followers an accel-aware speed limit: brake v=√(2·a·d), ramp Δv ≤ a·dt. Caps how hard they push so they don't demand impossible accelerations. |
the commanded speed | PID, SQuID, Pedro | maxAcceleration (Pedro braking also zeroPowerAccelerationMultiplier) |
Quadratic drag kV2·v·|v| |
A v²-resistance feedforward term (F_drag = ½ρ·Cd·A·v²) for fast robots. Off by default. Only meaningful where a velocity feedforward exists — it cannot attach to pure PID/SQuID feedback, which output power straight from position error with no velocity to apply it to. |
the feedforward output | FeedforwardFollower (when useQuadraticDrag) |
useQuadraticDrag, kV2 |
The short version:
kAis the acceleration feedforward — it's the term that literally costs power to accelerate, and it stays linear because motor voltage↔acceleration is linear.- The S-curve is a smoothness upgrade to profiling (it limits jerk, the rate of change of acceleration).
- The kinematic cap is how the non-profiled followers (PID/SQuID/Pedro) become acceleration-aware at all — it's a speed limit derived from kinematics, not a feedforward.
- Quadratic drag is velocity-squared resistance, not acceleration; it's optional polish for fast robots and rides on top of the feedforward, never on raw feedback.
The follower outputs a field-relative translation vector + a turn. The drive:
- Rotates the field vector into the robot frame by
−heading(so "drive toward field +x" works no matter which way the robot faces). - Mixes forward / strafe / turn into four wheel powers with the standard mecanum equations.
- Normalizes — if any wheel exceeds 1.0, all four are scaled down together so the direction of travel is preserved (you never lose the requested heading just because you asked for too much power).
- Applies the global
maxPowercap and writes the motors.
Motor names, per-motor reverse flags, brake/coast, and a strafe multiplier all come from
DriveConstants.
All extend Follower, which runs the shared loop above and the path-end logic. Each implements only
computeDriveSignal(path, t, pose).
Drives straight at the path's end point: a translational PID on the distance to the target produces
the vector magnitude, a heading PID produces the turn. Simple and predictable; ideal for lines and
point-to-point moves. (It aims at the end point, so it cuts corners on curves — use Pedro for those.)
A KinematicLimiter caps the speed so it ramps acceleration in on launch and brakes into the end
rather than slamming to full PID output.
Identical structure to PIDFollower (including the KinematicLimiter speed cap) but with
SquIDControllers. One gain per axis, gentle braking into the target, no derivative term to tune.
On each new path it builds an SCurveProfile (jerk-limited) over the path's arc length. Every loop
it asks the profile for the commanded position/velocity/acceleration at the current time, maps the
commanded position to a target point on the path, and outputs:
- a feedforward vector along the path tangent sized by
kS/kV/kA(plus optionalkV2drag) from the profile's velocity and acceleration (does most of the work), plus - a correction PID vector pulling the robot onto the profiled target point, plus
- a heading PID.
The path only completes once the profile has finished and the robot has settled at the end.
Tracks curved paths tightly by summing three field-relative vectors plus a heading correction:
- Drive vector — along the path tangent, scaled by an acceleration-limited velocity curve (a
KinematicLimiter): it ramps up on launch and brakes viav = sqrt(2·a·d)(wheredis the remaining distance) so it slows smoothly into the end. Braking uses the tunedzeroPowerAccelerationMultiplier; ramp-up uses the plainmaxAcceleration. - Centripetal vector — perpendicular to the tangent,
centripetalScaling · mass · v² · curvature, which counteracts the robot sliding to the outside of a curve. - Translational correction — a PID pulling the robot back onto the closest point of the path.
The drive normalizes the sum, so all three blend into one motion.
A path is "done" when any motion profile has finished, the robot is within t-value and
translational tolerance of the end, and either it has settled (velocity + heading within tolerance)
or a settle timeout elapses as a failsafe. holdPoint(pose) follows a single-point path and never
auto-completes, so the robot actively holds position.
All tunables live in constants/, annotated @Config so FTC Dashboard
(http://192.168.43.1:8080/dash) can edit them live without redeploying. Followers re-read their
gains every loop, so a Dashboard change takes effect immediately. util/Drawing pushes a field
overlay (robot + path) to the same dashboard so you can watch the robot track the path in real time.
| File | Holds |
|---|---|
DriveConstants |
motor names, reverse flags, brake/coast, strafe multiplier, max power |
FollowerConstants |
all PID/SQuID gains, kS/kV/kA, optional kV2 drag, profile limits (maxVelocity/maxAcceleration/maxJerk), Pedro params, path-end tolerances |
LocalizerConstants |
localizer choice + Pinpoint and 3-deadwheel configuration, IMU orientation |
Run the OpModes in the pathing-tuning group in order, editing constants live in Dashboard:
- Localization Test — verify pose tracks reality (fix direction flags in
LocalizerConstants). - Forward/Strafe Push Test — set distance scaling / ticks-per-inch.
- Translational PID — tune
FollowerConstants.translational*. - Heading PID — tune
FollowerConstants.heading*. - Feedforward (kS/kV/kA) — only for
FeedforwardFollower. - SQuID kP — only for
SquIDFollower.
Then polish the Pedro follower's centripetalScaling, mass, and zeroPowerAccelerationMultiplier
on real curves.
These all default to safe values; tune only if you see slip, overshoot, or velocity-tracking error.
maxAcceleration(in/s²) — the traction limit shared by the kinematic speed cap (PID/SQuID/ Pedro ramp-up and braking) and the profile. Raise until the wheels start to slip on launch/stop, then back off ~20%.maxJerk(in/s³,FeedforwardFolloweronly) — how fast acceleration is allowed to change. Start high (near-trapezoid); lower it until the start/stop transitions stop jerking and the wheels stop chirping. Lower = smoother but slightly slower.useQuadraticDrag+kV2(FeedforwardFolloweronly, fast robots) — leave off unless the robot tops out fast enough thatkV·vunder-predicts power at high speed (the profiled velocity lags the target at the top end). Then setuseQuadraticDrag = trueand raisekV2(power/(in/s)²) until high-speed velocity tracking flattens out. On slow robots this is negligible — leave it off.
- In
DriveConstants, set the four motor names and reverse flags for your robot. - In
LocalizerConstants, choose and configure your localizer. - Run Tuning 1: Localization Test and confirm the pose is correct.
- Run an OpMode from the
pathing-examplesgroup (start with Straight Line (PID)):
Follower follower = new PedroFollower(hardwareMap); // or PIDFollower / SquIDFollower / FeedforwardFollower
follower.setStartingPose(new Pose(0, 0, 0));
follower.followPath(chain);
while (opModeIsActive() && follower.isBusy()) {
follower.update();
}- Tune in the order above until paths run cleanly.
| OpMode | Shows |
|---|---|
| Straight Line (PID) | basic point-to-point with the PID follower |
| Bezier Curve (Pedro) | following a curve with the vector follower |
| Path Chain (Pedro) | multi-segment chain + all three heading modes |
| SQuID Follow | move + turn with the SQuID follower |
| Feedforward Profile (FRC) | motion-profiled move with kS/kV/kA |
| Hold Point | actively holding a pose; push it and it returns |
| Field-Centric TeleOp | driving the drive + localizer directly, no follower |
FTC Dashboard (com.acmerobotics.dashboard:dashboard:0.4.16) is added in
build.dependencies.gradle with the bundled RobotCore excluded so it uses the SDK's copy.