πŸ’» Programming Β· Complete Guide Β· Beginner β†’ Advanced

LemLib Complete Guide

From an empty project to a tuned, odometry-driven autonomous: install LemLib, wire up tracking wheels, configure the chassis, calibrate, tune the PIDs, and write motions that drive to coordinates instead of guesses. Work top to bottom the first time; after that, use it as a reference.

Source & version. LemLib is open-source (full source) from github.com/LemLib/LemLib, and targets PROS kernel 4.2.1. It's a third-party library β€” if you use it, cite it in your notebook. Commands and a couple of API signatures change between releases, so where exact syntax matters this guide links the official docs at lemlib.readthedocs.io β€” check them against the version you install.
On this page
  1. Is LemLib right for you?
  2. What you need first
  3. Install LemLib
  4. Wire up the hardware
  5. Configure the chassis
  6. Calibrate & first test
  7. Tune the PID controllers
  8. Write motions
  9. Async control
  10. Path following (pure pursuit)
  11. A complete example autonomous
  12. Common errors & debugging
  13. Function & params reference
πŸ—ΊοΈ Flowchart β€” LemLib motion picker

Once it's set up, LemLib coding is mostly picking the right motion call. This tree maps "what I want" to the call.

flowchart TD
    Start([What do you
want the bot to do?]) --> Q1{Drive or turn?} Q1 -->|"Drive to a spot
(heading free)"| MP[chassis.moveToPoint
x, y, timeout] Q1 -->|"Drive to a spot
AND face a way"| MPose[chassis.moveToPose
x, y, theta, timeout] Q1 -->|"Turn to a heading"| TH[chassis.turnToHeading
theta, timeout] Q1 -->|"Turn to face a point"| TP[chassis.turnToPoint
x, y, timeout] Q1 -->|"Follow a whole path"| FL[chassis.follow
path, lookahead, timeout] MP --> Wait[Then: chassis.waitUntilDone
blocks until the motion ends] MPose --> Wait TH --> Wait TP --> Wait FL --> Wait Wait --> Mid{Act mid-move?} Mid -->|"Yes"| WU[chassis.waitUntil dist
fire intake / lift, keep driving] Mid -->|"No"| Next([Next step]) WU --> Next style Start fill:#1e293b,stroke:#22d3ee,stroke-width:2px,color:#e2e8f0 style Q1 fill:#fbbf24,color:#0f172a,stroke:#fbbf24 style Mid fill:#fbbf24,color:#0f172a,stroke:#fbbf24
πŸ€” 0 Β· Is LemLib right for you?
Pick LemLib when you want accurate odometry and smooth, coordinate-based motion β€” pure-pursuit path following, "drive to this pose," and async motions you can overlap with mechanisms. It's the most capable of the common PROS libraries.
Be honest about the cost: LemLib is odometry-first β€” it expects real tracking wheels, and it targets PROS kernel 4.2.1. If you have no tracking hardware, or you mostly want a quick driver + simple PID auton, a template like EZ-Template gets you there with less setup. LemLib rewards teams that will actually tune it.
πŸ“¦ 1 Β· What you need first
Software
VS Code + the PROS extension, and a working PROS project (kernel 4.2.1). New to this? Start with VS Code + PROS Setup.
Hardware
A tank/arcade drivetrain, an IMU (inertial sensor), and at least one tracking wheel on a Rotation Sensor. Two vertical + two horizontal is the ideal. Build one: Odom Pod Build.
💡
You can start LemLib with fewer tracking wheels (even just the drive motors for a rough estimate), but the whole point is accuracy β€” plan to add at least one vertical and one horizontal pod. See the pod designs on Odom Pod Types (the leaf-spring design holds the wheel to the floor most consistently).
⬇ 2 Β· Install LemLib
Install commands change between LemLib releases. The steps below are the usual flow β€” confirm the exact commands on the official install page for the version you're adding.
1
Have a PROS project on kernel 4.2.1
Use an existing project, or make one: in the PROS terminal, pros conduct new-project. Confirm the kernel β€” LemLib targets 4.2.1.
2
Add the LemLib template
Grab the latest release template from github.com/LemLib/LemLib (the .zip), then fetch and apply it:
# from your project folder, in the PROS terminal
pros c fetch LemLib@0.5.0.zip
pros c apply LemLib@0.5.0
(Some versions support a depot install β€” pros c add-depot then pros c apply LemLib. Use whichever the install page lists.)
3
Include the API
At the top of the files where you'll use it:
// main.cpp (and any file that calls chassis)
#include "lemlib/api.hpp"
4
Build to confirm
Run pros build. A clean build means LemLib is installed against your kernel. A "template not compatible" error usually means a kernel mismatch β€” see Common errors.
πŸ›  3 Β· Wire up the hardware

Odometry works by adding up how far two unpowered tracking wheels roll: a vertical wheel (rolls as you drive forward/back) and a horizontal wheel (rolls as you slide sideways during turns and arcs). The IMU supplies heading. Each wheel's offset is its perpendicular distance from the robot's tracking center β€” and getting the sign and size right is what makes the pose accurate.

ROBOT (top-down) Β· front = up tracking center vertical offset horizontal offset +Y forward
A vertical wheel offset to the left and a horizontal wheel offset to the back. Offsets are measured from the tracking center; a wheel left of center or behind center is a negative offset in LemLib's convention.
📏
Measure, don't eyeball. Use calipers for wheel diameter and a ruler for each offset (perpendicular distance from the wheel's center line to the tracking center). A 0.25" offset error rotates your whole field frame over a long auton.
πŸŽ› 4 Β· Configure the chassis

Configuration is four objects: the drivetrain, the odom sensors, two PID controllers (one for driving straight, one for turning), and the chassis that ties them together.

a Β· Drivetrain
pros::MotorGroup leftMotors({-1, -2, -3});   // negative port = reversed
pros::MotorGroup rightMotors({4, 5, 6});

lemlib::Drivetrain drivetrain(
  &leftMotors, &rightMotors,
  11.5,                         // track width: left-to-right wheel distance (in)
  lemlib::Omniwheel::NEW_325,     // wheel diameter (3.25")
  450,                          // drive RPM (motor RPM x gear ratio)
  2                             // horizontal drift: 2 = traction wheels, ~8 = all-omni
);
ParamWhat it isHow to get it
track widthDistance between left & right wheelsMeasure center-to-center; fine-tune in Β§5
wheel diameterDrive wheel sizeUse the Omniwheel constant (NEW_275/325/4)
drive RPMFree speed of the driveCartridge RPM Γ— external gear ratio
horizontal driftHow much the bot slides sideways2 if you run traction wheels; higher for all-omni
b Β· Odometry sensors
pros::Rotation vertRot(11);
pros::Rotation horiRot(12);
pros::Imu imu(10);

// TrackingWheel(sensor, wheel diameter, offset from tracking center)
lemlib::TrackingWheel vertical(&vertRot, lemlib::Omniwheel::NEW_2, -2.5);
lemlib::TrackingWheel horizontal(&horiRot, lemlib::Omniwheel::NEW_2, -1.0);

// OdomSensors(vertical1, vertical2, horizontal1, horizontal2, imu)
// pass nullptr for any wheel you don't have
lemlib::OdomSensors sensors(&vertical, nullptr, &horizontal, nullptr, &imu);
c Β· PID controllers (lateral + angular)
// lateral = straight-line driving
lemlib::ControllerSettings lateral(
  10, 0, 3,     // kP, kI, kD
  3,            // anti-windup range
  1, 100,       // small error (in), small timeout (ms)
  3, 500,       // large error (in), large timeout (ms)
  20            // slew (max accel; 0 = off)
);
// angular = turning
lemlib::ControllerSettings angular(2, 0, 10, 3, 1, 100, 3, 500, 0);
The two error/timeout pairs are the exit conditions: the motion is "done" once it's within the small error for the small timeout, or within the large error for the large timeout (a safety net so it can't hang forever). You tune the gains in Β§6.
d Β· The chassis + calibrate
lemlib::Chassis chassis(drivetrain, lateral, angular, sensors);

void initialize() {
  chassis.calibrate();   // calibrates IMU + starts odometry β€” keep the robot still!
}
πŸ§ͺ 5 Β· Calibrate & first test
1
Confirm odometry is alive
After calibrate(), print the pose to the screen in a loop and push the robot by hand. x, y, and theta should change sensibly. If pose stays (0,0), see Common errors.
while (true) {
  auto p = chassis.getPose();
  pros::lcd::print(0, "x %.1f  y %.1f  h %.1f", p.x, p.y, p.theta);
  pros::delay(20);
}
2
Calibrate track width (turn test)
Command a 10-rotation in-place turn and compare the robot's real heading to what odometry reports. If it under/over-rotates, adjust the track width in the Drivetrain until reported and real headings match.
3
Sanity-check distance
Drive a known distance (e.g. 48"). If reported y is off, re-check your tracking-wheel diameter and that the wheel actually spins freely on the floor.
πŸ”¬ 6 Β· Tune the PID controllers

Tune lateral (driving) and angular (turning) separately. The method is the same for both:

1
Start with kP only (kI = kD = 0)
Raise kP until the robot reaches the target briskly and just barely overshoots / oscillates. Too low = sluggish and stops short; too high = wild oscillation.
2
Add kD to damp the overshoot
Raise kD until the oscillation settles into one clean approach with little or no bounce. kD resists sudden change β€” it's the brake.
3
Only add kI if it consistently stops short
If the bot reliably settles just shy of the target, a tiny kI (with the anti-windup range) nudges it the rest of the way. Most setups leave kI at 0.
4
Set exit conditions + slew
Tighten the small error/timeout so it doesn't quit early, but keep the large error/timeout as a safety net. Add slew (lateral) if the bot jerks at the start of a move.
Worked example β€” how the Β§4 numbers were found

The lateral gains in Β§4 (kP 10, kD 3) didn't come from nowhere. Here's the session that produced them β€” a ~14 lb bot on 3.25" wheels, driving 24". kI stays 0 the whole time.

TrykPkDWhat the robot didDecision
140Crept in slowly, stopped ~1.5" shortkP too low β€” raise it
2100Reached target, overshot ~1" and rocked back twiceGood speed; add kD to damp
3102One small overshoot, then settledClose β€” a touch more kD
4103Drove in and stopped, no bounce✅ keep

The angular controller gets the same four-step treatment on an in-place 90° turn, landing on kP 2, kD 10 β€” turns settle quickly, so it tolerates a higher kD-to-kP ratio. Those are exactly the Β§4 constants.

These are a starting point for a similar robot, not magic values. A heavier bot, different wheels, or worn tiles will want different gains β€” run the same four-step process on your robot.
📝
Tune at competition battery voltage, on the competition tiles, with the real robot weight. Constants tuned on a fresh battery will overshoot on a tired one. More on diagnosing PID behavior: PID Diagnostics.
πŸš— 7 Β· Write motions
Set the starting pose
// where the robot starts: x, y (in), heading (deg)
chassis.setPose(0, 0, 0);
Move to a point (heading free)
chassis.moveToPoint(24, 24, 2000);
chassis.waitUntilDone();
Move to a pose (boomerang)
// arrive at x,y FACING theta β€” curves in
chassis.moveToPose(24, 24, 90, 3000);
chassis.waitUntilDone();
Drive backward / cap speed (params)
chassis.moveToPoint(0, -24, 2000,
  {.forwards = false, .maxSpeed = 80});
Turn to a heading / a point
chassis.turnToHeading(90, 1000);
chassis.turnToPoint(24, 48, 1000);
Read the current pose
auto pose = chassis.getPose();
// pose.x, pose.y, pose.theta
🎯
moveToPoint vs moveToPose. Use moveToPoint when you only care where you end up. Use moveToPose (the boomerang controller) when the final heading matters β€” it arcs so it arrives lined up. The lead param (0–1) controls how wide that arc is.
⏱ 8 Β· Async control β€” LemLib's superpower

Motions run on a background task by default, so your code keeps going. Block when you need to with waitUntilDone(), or act partway through a move with waitUntil(distance) β€” that's how you fire an intake or lift without stopping the drive.

chassis.moveToPoint(0, 40, 2500);
chassis.waitUntil(20);     // once 20" into the move...
intake.move(127);          // ...start intake, drive keeps going
chassis.waitUntilDone();    // now wait for the drive to finish

chassis.cancelMotion();     // abort current motion (e.g. on a line/sensor trigger)
chassis.cancelAllMotions();
πŸ›€ 9 Β· Path following (pure pursuit)
1
Draw the path
Build the path in path.jerryio.com (the standard LemLib path tool), then export it as a .txt file and drop it in your project's static/ folder.
2
Embed and follow it
Use the ASSET() macro to embed the file, then follow it with a lookahead distance and a timeout:
ASSET(path_txt);   // embeds static/path.txt

// follow(path, lookahead(in), timeout(ms), forwards)
chassis.follow(path_txt, 15, 4000);
chassis.waitUntilDone();
📏
Lookahead is how far down the path the bot "aims." Bigger = smoother but cuts corners; smaller = follows tightly but can wobble. Start around 10–15" and adjust.
Worked example β€” an S-curve around an obstacle

You need to get from the start (0, 0) to a scoring spot at (36, 48), but a stack sits at about (18, 24) right in the lane. A straight moveToPoint would plow into it β€” a drawn path can S-curve around it.

1
Lay the waypoints in path.jerryio
Drop control points that bow the path out and back β€” roughly (0,0) → (4,18) → (28,30) → (36,48). The curve clears the stack on the left, then sweeps back to the goal. Export as scurve.txt into static/.
2
Embed and follow it β€” forwards
ASSET(scurve_txt);

chassis.setPose(0, 0, 0);
chassis.follow(scurve_txt, 12, 4000);   // lookahead 12in, 4s timeout
chassis.waitUntilDone();
A 12" lookahead tracks this curve tightly. Bump it toward 18" if the bot wobbles on the tight part; drop it toward 8" if it cuts the corner into the stack.
3
Variant β€” back the same path in
To reverse the route β€” say, retreat along the curve you drove in on β€” pass forwards = false. LemLib follows the same path driving backward:
chassis.follow(scurve_txt, 12, 4000, false);  // forwards = false
chassis.waitUntilDone();
💡
Point vs path. If there's a clear lane, a single moveToPoint / moveToPose is simpler and self-corrects with odometry. Reach for follow when the route matters β€” dodging an obstacle, hugging a wall, or running a specific lane you practiced.
🏁 10 · A complete example autonomous
void autonomous() {
  chassis.setPose(0, 0, 0);          // start at origin, facing up-field

  // 1) drive to the first scoring spot
  chassis.moveToPoint(0, 30, 2000);
  chassis.waitUntil(22);                // part-way in...
  intake.move(127);                     // ...spin intake, keep driving
  chassis.waitUntilDone();
  intake.move(0);

  // 2) face the goal and approach lined up
  chassis.moveToPose(24, 36, 90, 3000);
  chassis.waitUntilDone();
  scorer.move(127);
  pros::delay(600);
  scorer.move(0);

  // 3) back out and turn for the next cycle
  chassis.moveToPoint(24, 12, 2000, {.forwards = false});
  chassis.waitUntilDone();
  chassis.turnToHeading(180, 1000);
  chassis.waitUntilDone();
}
📝
EN4 β€” make it yours. This skeleton shows the shape of a LemLib auton. Your real routine, your coordinates, and your notebook write-up must be your own work, in your own words β€” never copied or generated. Document why you chose each call and each constant.
⚑ 11 · Common errors & debugging
❌ Pose is stuck at (0,0) and never updates
Fix: You didn't call chassis.calibrate() in initialize(), or you moved the robot during IMU calibration. Call it once and keep the robot still until it finishes.
❌ Odometry drifts or reads totally wrong
Fix: Wrong tracking-wheel diameter or offset. Re-measure with calipers/ruler. A flipped offset sign (left vs right, front vs back) mirrors or rotates your field β€” double-check the convention against the docs.
❌ A tracking wheel reads nothing
Fix: Wrong Rotation Sensor port, or the wheel isn't touching the floor / is binding. Confirm the port in the constructor and that the pod presses to the tile (a leaf-spring pod helps here).
❌ Robot overshoots or oscillates on turns
Fix: Tune the angular controller β€” usually kP too high or kD too low. Lateral and angular are tuned independently (Β§6).
❌ A motion never ends β€” the bot sits or stalls forever
Fix: Always pass a timeout and follow each motion with waitUntilDone(). Loosen the large-error exit condition so an unreachable target still times out.
❌ Project won't build after adding LemLib
Fix: LemLib targets PROS kernel 4.2.1 β€” a mismatched kernel won't compile. Check your project kernel and the LemLib release notes before upgrading.
❌ Robot turns the wrong way
Fix: A reversed motor group, or the IMU mounted in an unexpected orientation. Verify positive/negative ports on the MotorGroups and that a positive heading matches the direction you expect.
πŸ“‹ 12 Β· Function & params reference
FunctionWhat it doesNotes
chassis.calibrate()Calibrate IMU + start odometryOnce, in initialize(); keep still
setPose(x, y, ΞΈ)Tell the robot where it startsinches + degrees
getPose()Read current (x, y, ΞΈ)returns a Pose
moveToPoint(x, y, t)Drive to a coordinateheading is free
moveToPose(x, y, ΞΈ, t)Drive to a full poseboomerang controller
turnToHeading(ΞΈ, t)Turn to an absolute headingdegrees
turnToPoint(x, y, t)Turn to face a pointuses current pose
follow(path, look, t)Pure-pursuit path followpath from path.jerryio + ASSET()
waitUntilDone()Block until the motion finishesafter each motion
waitUntil(dist)Continue once dist is reachedmid-move triggers
cancelMotion()Abort the current motionasync control
Motion params (the {...} struct)
ParamMeaning
forwardsfalse = drive backward into the target
maxSpeedcap on speed (0–127)
minSpeedfloor on speed β€” avoids stalling near the end
earlyExitRangefinish the motion once this close (smoother chaining)
leadmoveToPose only β€” how wide the boomerang arc is (0–1)
⚙ STEM Highlight Computer Science: Asynchronous Programming & Concurrency
LemLib runs each drive motion on its own background task, so your main code keeps executing while the robot moves β€” that's asynchronous (non-blocking) programming. waitUntil() and waitUntilDone() are synchronization primitives: they let two things happening at once (driving, and running an intake) coordinate cleanly. Same pattern as web servers, game loops, and robotics middleware.
🎤 Interview line: “Because LemLib motions are asynchronous, we use waitUntil() to start our intake 20 inches into a drive instead of stopping, repositioning, then driving again. Understanding blocking vs. non-blocking control let us overlap actions and cut about a second off our autonomous.”
You want to start your intake when the robot is 10 inches into a moveToPoint, without stopping the drive. Which call is correct?
⬛ chassis.waitUntilDone()
⬛ chassis.waitUntil(10)
⬛ pros::delay(500)
📝
Notebook entry tip: Build & Program β€” Orange slide β€” Document your odometry setup (tracking-wheel sizes and offsets), your tuned lateral/angular constants, and why you chose moveToPose vs moveToPoint for each step. Citing the specific LemLib calls your autonomous uses β€” in your own words β€” shows judges you understood the tool, not just imported it.
← ALL GUIDES