CLOCKWORK · VEX V5 · v1.6.0

CLOCKWORK

A small, focused library of motion, intake, and control helpers for the VEX V5, sitting on top of LemLib.

platform VEX V5 PROS kernel ^4.2.1 depends LemLib license MIT

Written for V5RC team 2360C. The idea is simple: install it once as a PROS template and call clean helpers from your autonomous and driver code, instead of copy-pasting the same motion code into every new project.

What you get

HelperWhat it does
Motion::driveFullThenSlowDrive straight fast, then ease down to a slow speed for a gentle arrival
Motion::driveDistanceDrive a set number of inches, slowing near the target, holding heading
Motion::driveTimedPush at a fixed power for a fixed time (ram / square up)
Motion::turnByTurn a relative number of degrees using the chassis's tuned turn PID
Motion::driveUntilStalledDrive until the robot hits something, detected from odometry
RollerReadable intake control: in / out / stop / spin / pulse / hold
Roller::antiJamClears intake jams on its own, without blocking your loop
PIDControllerA reusable PID for anything LemLib doesn't drive: arm, lift, flywheel
AutonSelectorPick an autonomous routine from the controller before the match
SlewRateLimiterRamp a value so a hard joystick shove can't wheelie or brown out
PneumaticsA clamp/wings piston with extend / retract / toggle and state
ToggleLatch a boolean on a single button press (one-button clamps, modes)
joystickCurveShape joystick input for fine low-speed control, still full at the ends
TrapezoidalProfileSpeed-up / cruise / slow-down motion profile for smooth moves
FlywheelControllerHold a flywheel at a target RPM with feedforward plus a P trim

Nothing here owns your hardware. Motion borrows a lemlib::Chassis*, Roller borrows a pros::MotorGroup*, and PIDController is standalone. You keep full control of your own devices.

Setup

Install

You need a PROS project on kernel ^4.2.1 with LemLib already installed (pros c install LemLib).

From the depot (recommended)
pros c add-depot clockwork https://raw.githubusercontent.com/AkshobyaRaoSWE/clockwork-lib/main/depot.json
pros c apply clockwork
From a release zip
pros c fetch clockwork@1.6.0.zip
pros c apply clockwork
Heads uppros c apply <path-to-zip> does not read a file path. It reads the argument as a name@version query. So always fetch the zip first, then apply clockwork.

To upgrade later, just run pros c apply clockwork again once the depot refreshes.

Setup

Quick start

Point the helpers at your own chassis and intake motor group, then call them from your routines.

#include "clockwork/clockwork.hpp"

// chassis and intake_motors are your own configured globals.
clockwork::Motion motion(&chassis);
clockwork::Roller intake(&intake_motors);

void autonomous() {
    chassis.setPose(0, 0, 0);

    intake.in();                          // start intaking
    motion.driveDistance(30, 110, 2000);  // 30 in, heading held
    motion.turnBy(90);                    // turn 90 deg clockwise
    motion.driveFullThenSlow(18, 6, 40, 1500); // fast, then gentle
    intake.pulse(-127, 300);              // eject for 300 ms

    if (motion.driveUntilStalled(70, 1200)) // ram the wall
        chassis.setPose(0, 0, 0);           // reset odom if we hit it
}

void opcontrol() {
    while (true) {
        intake.in();       // drive the intake
        intake.antiJam();  // clears jams by itself, never blocks
        pros::delay(20);
    }
}

More complete routines are on the EXAMPLES page.

Learn

Understanding gains (kP, kI, kD)

Every "Kp" in this library is really one question: how hard should the robot push to fix a mistake? You do not need any control-theory background for this. Read it once and the rest of the library makes sense.

The one idea behind all of it

A controller is a loop that runs many times a second and asks one thing:

The questionHow far am I from where I want to be?

That gap is the error. For a drive it might be "12 inches short." For an arm it might be "30 degrees too low." The controller turns that error into a motor power, waits a few milliseconds, checks again, and repeats until the error is basically zero. The gains decide how it turns error into power.

kP proportional · the main dial

Power = kP × error. The bigger the mistake, the harder it pushes, and it eases off on its own as it gets close. Picture a spring pulling the robot to the target, and kP is how stiff that spring is.

  • Too low: weak spring. It creeps in slowly, or stops short (an arm sags below where you wanted it).
  • Too high: violent spring. It races in, overshoots, then overshoots the other way. It shakes.
  • Just right: reaches the target quickly with only a tiny overshoot.

This is the one you tune first, and it does about 90% of the work. In this library, headingKp (default 2.0) and driveKp (default 8.0) are both pure kP dials.

kD derivative · the brake

A stiff kP overshoots because it keeps pushing hard right up until it arrives, so momentum carries it past. kD is the shock absorber. It watches how fast the error is shrinking and pushes back against fast approaches.

  • Adds a braking force that grows the faster you are closing in.
  • Lets you run a higher kP (fast) without the overshoot (sloppy).
  • Too high: it fights every little sensor wiggle and the mechanism buzzes.

Tune kD second, after kP, to kill the shake that kP left behind.

kI integral · the closer, use sparingly

Sometimes it settles near the target but never quite reaches it, a small steady offset. The classic case is an arm held up against gravity, where kP's push at a tiny error is not quite enough to hold it level. kI fixes that last sliver by adding up the leftover error over time until the push is finally strong enough.

  • Only cures a small, persistent offset. It does nothing for speed or overshoot.
  • Keep it tiny, often 10x to 100x smaller than kP.
  • Too high: it "winds up" and causes slow, lazy oscillation.
  • Plenty of mechanisms are perfect with kI = 0. Start there.

PIDController guards against wind-up for you: it caps the sum (integralCap) and clears it whenever the error crosses zero, so a big move can't leave a huge push waiting to overshoot.

The tuning recipe

  1. Set kI = 0 and kD = 0.
  2. Raise kP until it reaches the target quickly and overshoots just a little. If it shakes hard, you went too far, back off.
  3. Raise kD until that overshoot and shake smooth out. If it starts buzzing, back off.
  4. Only if it still stops slightly short, add a tiny kI (start around 1/50th of kP) until it closes the gap. Otherwise leave kI = 0.
Golden ruleP, then D, then maybe I. Change one gain at a time and watch what it does before you touch the next.

Symptom, then which gain

What you seeLikely causeDo this
Reaches target too slowly, or stops shortkP too lowRaise kP
Overshoots then oscillateskP too highLower kP, or add kD
Fast approach but shakes at the endNeeds dampingAdd or raise kD
Buzzes or jitters constantlykD too highLower kD
Settles just short of target foreverSteady offsetAdd a tiny kI
Slow lazy wobble that won't diekI wind-upLower kI, or set it to 0

One thing that matters: loop timing

A controller assumes it runs at a steady rhythm. Always put a fixed delay in your loop (pros::delay(10) is typical) and keep it the same. If you change the loop delay, your tuned gains change meaning and you have to re-tune. That is why every example here ends its loop with a pros::delay.

Reference

clockwork::Motion

clockwork::Motion motion(&chassis);

Every drive helper holds the heading it started at with a P controller (headingKp) and bypasses the driver curve. The distance-based ones measure from the pose at the moment you call them, so give the chassis a valid pose first with setPose. Pass negative speeds or distances to run in reverse.

MethodSignature
driveFullThenSlow(float fullDist, float slowDist, int slowSpeed, int timeoutMs, int fullSpeed = 127, float headingKp = 2.0f)
driveDistance(float dist, int maxSpeed = 127, int timeoutMs = 3000, float headingKp = 2.0f, float settleRange = 1.0f, float driveKp = 8.0f)
driveTimed(int ms, int speed, float headingKp = 2.0f)
turnBy(float degrees, int timeoutMs = 1500, int maxSpeed = 127)
turnToHeading(float heading, int timeoutMs = 1500, int maxSpeed = 127)
moveToPoint(float x, float y, int timeoutMs = 3000, int maxSpeed = 127, bool forwards = true)
driveUntilStalled(int power, int timeoutMs = 3000, float headingKp = 2.0f) → bool
  • driveFullThenSlow: full speed for fullDist inches, then slowSpeed for the next slowDist. Fast approach, soft arrival.
  • driveDistance: drive dist inches, easing off near the target, settling within settleRange or timing out. driveKp sets how hard it pushes per inch remaining (raise for snappier, lower if it overshoots).
  • driveTimed: apply speed for ms milliseconds, then stop.
  • turnBy: turn degrees relative to your current heading (positive is clockwise) using the chassis's own turn PID. Blocks until it settles.
  • driveUntilStalled: drive at power until the robot stops moving (a wall, an obstacle) or times out. Returns true if it actually stalled. Great for squaring on a wall before an odom reset.
Heading is cappedThe heading correction is internally clamped, so a hard bump can't produce a giant turn command that steals power from the throttle. Forward motion always keeps priority.

Reference

clockwork::Roller

clockwork::Roller intake(&intake_motors, 127); // second arg = default power
MethodDescription
in()spin inward at the default power
out()spin outward at the default power
stop()command 0
spin(int power)explicit signed power, -127 to 127
pulse(int power, int ms)spin for ms, then stop (this one blocks)
hold()set HOLD brake mode and brake in place
stalled(double vel = 5.0)true if velocity is under vel RPM
antiJam(...)non-blocking auto-reverse on a jam

How anti-jam works

antiJam(int reversePower = 127, int reverseMs = 200, int jamHoldMs = 150, double velThreshold = 5.0)

Call it once per loop, right after you command the intake. It is a little state machine that reads the last power you sent, so it always knows which way you meant to run and it never blocks.

  • Idle (you commanded 0): it does nothing and returns false.
  • Watching: you commanded movement, so it checks the real velocity. Moving fine resets the stall timer. Below velThreshold starts the timer.
  • Jam confirmed: once it has been stalled longer than jamHoldMs, it drives the group the opposite way at reversePower for a reverseMs window.
  • Clearing: later calls just let the burst run, then it resumes your original command on its own and returns to watching.
while (true) {
    intake.in();
    intake.antiJam();      // reverses a 200 ms burst on a jam, then resumes
    pros::delay(20);
}
NoteIt works off motor velocity, not current, so a motor that free-spins without intaking reads as "moving" and won't false-trigger. The flip side: a slipping jam where the motor still spins won't be caught. Keep calling it every loop, or a burst in progress never gets cancelled.

Reference

clockwork::PIDController

clockwork::PIDController pid(kP, kI, kD, integralCap = 0, outputCap = 0);

A standalone PID for anything LemLib doesn't drive: an arm, a lift, a flywheel, a wall-align. If the three numbers are new to you, read Understanding gains first.

MethodDescription
update(float error) → floatCall every loop with target - measured. Returns a motor power, clamped to outputCap if you set one.
reset()Forget history (the integral sum and last error). Call it at the start of a new move.
setGains(kP, kI, kD)Swap gains at runtime, for example loaded versus empty.
settled(tolerance, stillness = 1.0f)True when the error is within tolerance and barely changing, so it has actually arrived rather than flying through.

Two constructor extras: integralCap limits how much the integral can build up (anti-windup; 0 disables it), and outputCap limits the returned power. Pass 127 for outputCap so update() never hands a motor an out-of-range command.

// arm: kP=0.9, kI=0, kD=4, integral cap 50, output clamped to motor range
clockwork::PIDController armPid(0.9f, 0.0f, 4.0f, 50.0f, 127.0f);

void moveArmTo(float targetDeg) {
    armPid.reset();
    while (!armPid.settled(2.0f)) {
        float measured = armSensor.get_position() / 100.0f;
        arm.move(armPid.update(targetDeg - measured));
        pros::delay(10); // steady loop timing, see the gains guide
    }
    arm.brake();
}

To hold a position forever, like keeping an arm up under gravity, skip the settled() exit and just keep calling update() every loop.

Reference

clockwork::AutonSelector

clockwork::AutonSelector selector(&master); // pass your pros::Controller

A controller-driven autonomous selector. You register routines by name, the driver scrolls them with the D-pad before the match, the current pick shows on the controller and brain screens, and run() runs the choice in autonomous(). It uses only core PROS, so it works on any project, no liblvgl or LLEMU.

MethodDescription
add(name, routine)Register a void() routine (a function or a lambda) under a display name
poll()One iteration of the picker: reads the D-pad and redraws. Call it in a loop
run()Run the selected routine. Call it from autonomous()
next() / prev()Move the selection, wrapping around
select(int) / select(name)Choose a specific routine, for example a default
draw()Push the current selection to both screens
index() / count() / name()Read the current state
void initialize() {
    selector.add("Left rush",  left_rush);
    selector.add("Right safe", right_safe);
    selector.add("Do nothing", [] {});
    selector.draw();
}

void competition_initialize() {
    while (true) { selector.poll(); pros::delay(20); } // driver picks here
}

void autonomous() { selector.run(); }
ControlsRIGHT or DOWN on the D-pad moves to the next routine, LEFT or UP to the previous one. It uses new-press detection, so holding the D-pad won't run off the end of the list.

Reference

clockwork::SlewRateLimiter

clockwork::SlewRateLimiter slew(2.5f); // most it can move per call

Ramps a value toward a target by at most a fixed step each call, so the command eases in instead of jumping. Use it to smooth driver throttle, so a hard shove can't wheelie the robot or brown out the battery, or to bring a flywheel up to speed gently.

MethodDescription
calculate(float target)Step the output toward target and return the new value. Call once per loop
reset(float value = 0)Snap straight to a value with no ramp
value()The current output, without stepping it

The step is per call, so it depends on your loop rate. At a 10 ms loop, a step of about 2.5 ramps from 0 to full power (127) over roughly half a second. Keep the loop delay steady and the ramp stays consistent.

void opcontrol() {
    while (true) {
        int y = master.get_analog(pros::E_CONTROLLER_ANALOG_LEFT_Y);
        left_mg.move(slew.calculate(y)); // eased instead of instant
        pros::delay(10);
    }
}

Reference

clockwork::Pneumatics

clockwork::Pneumatics clamp('A'); // ADI port A (or a number 1-8)

Wraps a single pneumatic piston (an ADI digital-out solenoid) for a clamp, wings, or a tilt. It remembers whether it's extended, so you can toggle it and read its state instead of tracking a loose bool.

MethodDescription
extend() / retract()push the piston out / pull it in
set(bool out)extend when true, retract when false
toggle()flip to whichever it isn't right now
extended()true if it's currently out

Pass startExtended to the constructor if the piston should begin pushed out.

Reference

clockwork::Toggle

clockwork::Toggle clampToggle;

A latch that flips a boolean on the released-to-pressed edge of a button. Feed it the button state every loop and it handles the edge detection, so a single press toggles once instead of flickering while you hold it. This is what makes a one-button clamp or speed mode work.

MethodDescription
update(bool pressed)Call every loop with the button state. Flips on the press, returns the latched value
state()The latched value, without changing it
set(bool)Force the latch to a value, for example reset to open at match start
clamp.set(clampToggle.update(master.get_digital(pros::E_CONTROLLER_DIGITAL_L1)));

Reference

clockwork::joystickCurve

float shaped = clockwork::joystickCurve(input, 2.0f, 5);

Shapes a raw joystick reading so the middle of the stick is gentler than the ends. You get fine control for small nudges and still hit full power at the extremes, which is the trick that makes a drive feel precise instead of twitchy.

ArgumentMeaning
inputraw stick value, -127 to 127
curvehow much to bend it. 1.0 is linear; higher (2 or 3) gives more finesse near the center. Default 2.0
deadbandanything this small either way reads as 0, killing stick drift. Default 5
left_mg.move(clockwork::joystickCurve(y + x, 2.0f, 5));

Reference

clockwork::TrapezoidalProfile

clockwork::TrapezoidalProfile profile(24.0f, 40.0f, 80.0f); // distance, maxVel, maxAccel

A speed-up / cruise / slow-down motion profile, the smooth shape you want for a move instead of slamming to full speed and back. You give it how far to go, the top speed to allow, and how hard it may accelerate; it works out the timing and tells you the target velocity and position at any moment. If the distance is too short to reach top speed it hands you a triangle instead of a trapezoid, and a negative distance profiles a reverse move.

MethodDescription
totalTime()how long the whole move takes
velocityAt(float t)target velocity at time t (0 before the start and after the end)
positionAt(float t)distance covered by time t, clamped to the full distance
distance()the distance it covers

Units are yours to pick as long as they agree: inches and inches/sec means acceleration is inches/sec² and time is seconds.

float t = (pros::millis() - start) / 1000.0f;
int power = (int)(profile.velocityAt(t) * (127.0f / 40.0f)); // velocity -> power

Reference

clockwork::FlywheelController

clockwork::FlywheelController fw(0.21f, 0.05f); // kFF, kP  (outputMax defaults to 127)

A velocity controller for a flywheel. A position PID is the wrong tool for a spinning mass, so this uses the combo that works: output = kFF * targetRpm + kP * (targetRpm - measuredRpm). The feedforward term (kFF) guesses the power to hold the target speed and does the heavy lifting; the proportional term (kP) trims the error and helps it snap back after a shot drags the wheel down. Output is clamped to [0, outputMax] because a flywheel only spins one way.

MethodDescription
update(targetRpm, measuredRpm)Call every loop; returns a power in [0, outputMax]
setGains(kFF, kP)Swap gains at runtime (a different speed preset)
TuningSet kP to 0 and raise kFF until the wheel settles near the target on its own (start around outputMax / maxRpm, so ~0.21 for 600 RPM). Then add a little kP for faster recovery after a shot.
flywheel.move(fw.update(500, flywheel.get_actual_velocity()));

More

Examples

Straight rush and score
void rushAndScore() {
    clockwork::Motion motion(&chassis);
    chassis.setPose(0, 0, 0);
    intake.in();
    motion.driveFullThenSlow(36, 8, 45, 2500); // 36 in full, 8 in easing to 45
    intake.pulse(-127, 400);                   // eject
}
Wall-align and reset odometry
void alignToWall() {
    clockwork::Motion motion(&chassis);
    if (motion.driveUntilStalled(70, 1500)) {
        lemlib::Pose p = chassis.getPose();
        chassis.setPose(p.x, 0, 0); // squared up, re-zero the known axis
    }
}
Box pattern with relative turns
void box() {
    clockwork::Motion motion(&chassis);
    chassis.setPose(0, 0, 0);
    for (int i = 0; i < 4; i++) {
        motion.driveDistance(24, 110, 2000);
        motion.turnBy(90);
    }
}

The full set lives in EXAMPLES.md.

More

Testing

The pure-logic classes (PIDController and SlewRateLimiter) have no PROS or hardware dependency, so they come with tests you can run on your own computer, no V5 brain needed.

make test        # or: bash test/run.sh

That builds the tests with your system compiler and runs them. They check the proportional math, the output clamp, that a PID actually converges on a target, and that the slew limiter ramps and never overshoots.

CLOCKWORK host tests
747 checks, 0 failed
OK

The motion helpers and the selector talk to real hardware, so they are verified by building the template into a project and running it on a robot.

More

Building & releasing

You need the PROS toolchain (pros and arm-none-eabi) on your PATH.

pros make            # build the project + bin/clockwork.a
pros make template   # package clockwork@<version>.zip

To cut a release: bump VERSION in the Makefile, run pros make template, attach the zip to a GitHub release, and add an entry to depot.json. The template ships only the public headers and the compiled archive.

Versioning follows semver. See the changelog.