Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/saquer916/odyssey/llms.txt

Use this file to discover all available pages before exploring further.

The Follower is the runtime loop that ties every Odyssey subsystem together. On each call to update(), it queries the localizer for the robot’s current pose, projects that pose onto the path to find the closest point, reads the target velocity from the VelocityProfile, runs two PID controllers to correct translational and heading error, and packages the result into a DriveSignal that MecanumDrive converts into wheel powers.

Follower constructor

Follower follower = new Follower(
    path,
    localizer,
    velocityProfile,
    new PIDController(0, kP_trans, 0, kD_trans),   // translational PID
    new PIDController(0, kP_head, 0, kD_head),     // heading PID
    50.0,   // minDriveSpeed mm/s  — floor so the robot does not stall
    30.0    // floorCutoff mm      — distance at which minDriveSpeed is removed
);

Parameter reference

ParameterTypePurpose
pathPathThe pre-built multi-segment Bézier path to follow.
localizerLocalizerProvides the current Pose2d (X mm, Y mm, heading rad).
velocityProfileVelocityProfilePre-computed speed table indexed by arc-length distance.
pidTranslationalPIDControllerCorrects the lateral offset of the robot from the nearest path point. Setpoint is 0; input is −offset.getMagnitude().
pidHeadingPIDControllerCorrects the angle error between the reference heading and actual heading. Setpoint is 0; input is −normalizeAngle(referenceHeading − poseHeading).
minDriveSpeeddoubleA lower speed floor (mm/s) applied when the profile velocity is very low but the robot is still far from the end. Prevents the robot from stalling on carpet.
floorCutoffdoubleDistance (mm) from the path end within which minDriveSpeed is no longer applied, allowing the robot to decelerate to a true stop.

Update loop

The follower must be called every loop iteration from inside a LinearOpMode. Always call localizer.update() first so the follower sees a fresh pose.
ElapsedTime timer = new ElapsedTime();

while (opModeIsActive() && follower.getDistanceRemaining() > 20.0) {
    localizer.update();
    DriveSignal signal = follower.update(timer.seconds());
    drive.drive(signal);
    telemetry.addData("dist remaining", follower.getDistanceRemaining());
    telemetry.update();
}

// Explicitly stop the robot after the path completes
drive.drive(new DriveSignal(0, 0, 0, 0, 0));
getDistanceRemaining() returns the cached value from the most recent update() call — it does not trigger an additional getDistanceOnPath() query, so calling it in the loop condition is free.

DriveSignal decomposition

DriveSignal carries five fields that MecanumDrive.drive() unpacks into per-wheel power commands:
// Field velocities (robot frame, mm/s)
signal.getForwardVelocity()       // longitudinal component
signal.getStrafeVelocity()        // lateral component
signal.getTurn()                  // heading correction (rad/s equivalent)

// Field accelerations (robot frame, mm/s²) — used for feedforward
signal.getForwardAcceleration()   // tangential + centripetal forward
signal.getStrafeAcceleration()    // tangential + centripetal strafe
Inside MecanumDrive.drive(), the mecanum kinematics mixing is:
double v_FL = f - s - k * w;
double v_FR = f + s + k * w;
double v_BL = f + s - k * w;
double v_BR = f - s + k * w;
where k = lX + lY is the sum of the half-wheelbase and half-track width, and f, s, w are the forward velocity, strafe velocity, and turn from the signal.

Voltage compensation

MecanumDrive scales every wheel power by a voltage compensation factor so the robot behaves consistently as the battery drains:
double voltageComp = 12.0 / voltageSensor.getVoltage();

double p_FL = (kS * Math.signum(v_FL) + kV * v_FL + kA * a_FL) * voltageComp;
If the battery is at 13 V, voltageComp ≈ 0.92, reducing power slightly. At 11 V it rises to ≈ 1.09, boosting power to compensate for the sag. The final powers are then normalised so the maximum absolute value does not exceed 1.0.

MecanumDrive constructor

Wire up the drive before constructing the Follower:
MecanumDrive drive = new MecanumDrive(
    kS, kV, kA,
    lX,          // half-track width (mm) — lateral wheel offset from center
    lY,          // half-wheelbase (mm) — longitudinal wheel offset from center
    hardwareMap.get(DcMotorEx.class, "leftfront"),
    hardwareMap.get(DcMotorEx.class, "rightfront"),
    hardwareMap.get(DcMotorEx.class, "leftback"),
    hardwareMap.get(DcMotorEx.class, "rightback"),
    hardwareMap.get(VoltageSensor.class, "Control Hub")
);
MecanumDrive automatically sets all four motors to RUN_WITHOUT_ENCODER mode and BRAKE zero-power behaviour, and reverses the right-side motors to match the standard symmetric FTC chassis wiring.
The translational PID setpoint is always 0 — the target is zero offset from the path. The input passed to getOutput() is the negative magnitude of the position error vector: pidTranslational.getOutput(currentTime, -offset.getMagnitude()). This means a positive PID output pushes the robot toward the path, and the direction is applied separately via the normalised offset vector.
Call localizer.update() before follower.update() on every loop iteration. PinpointLocalizer.update() polls the GoBILDA Pinpoint hardware; if you skip it, follower.update() will compute corrections against a stale pose and accumulate error over time.

Build docs developers (and LLMs) love