Controllers

Closed-loop and feedforward math that drives a measured value toward a target. Every controller shares one small interface, so subsystems can swap PID, feedforward, or a combination without changing their update loop.

A Controller is an abstract closed-loop controller. It holds a single target (setpoint) and exposes an update(current, dt) method that is called once per loop with the latest measurement and returns the control output to apply to a motor, servo, or other actuator. The controllers package lives in com.cdarobotics.cdalib.controllers.

abstractController
The base type: a target plus update(current, dt).
feedbackPIDController
Proportional + integral + derivative on error.
feedforwardFFController
Static + linear + cosine term from the measurement.
compositeCompoundController
Sums the outputs of several sub-controllers.
no-opNullController
Always returns 0.

The Controller base type

All controllers extend Controller, an abstract class with one field and three members you interact with:

MemberSignatureDescription
constructorController(double target)Stores the initial setpoint.
updateabstract double update(double current, double dt)Computes the control output for this loop from the current measured value and the elapsed time (in seconds) since the previous update. Returns the output to apply.
getTargetdouble getTarget()Returns the current setpoint.
setTargetvoid setTarget(double target)Changes the setpoint. Takes effect on the next update().
dt is in seconds. The dt argument is the loop time delta, used by the integral and derivative terms. Pass the actual measured time between loops (a fixed nominal value works too, but real loop time gives more stable behavior).

PIDController

A proportional-integral-derivative feedback controller. Each loop it computes the error target - current and returns the sum of three terms:

  • Proportional — error * kP.
  • Integral — the running integral of error times kI. The integral is accumulated using trapezoidal integration: each loop it adds (previousError + error) / 2 * dt. This is a plain accumulator with no anti-windup clamping, so hold the target reasonably close to where the mechanism can reach it.
  • Derivative — the rate of change of error, (error - previousError) / dt, times kD.

As implemented, the full output is:

error * kP + integral * kI + derivative * kD
ConstructorParameters
PIDController(double target, double kP, double kI, double kD) target initial setpoint · kP proportional gain · kI integral gain · kD derivative gain
Note on initialization. The controller seeds previousError = target at construction. The very first update() after construction therefore computes its derivative against that seed; this is normally harmless but worth knowing if your first loop matters.

FFController

A feedforward controller. Its output is computed purely from the current measurement (it does not use the target or accumulate any state). The exact formula as implemented is:

kS + kF * current + Math.cos(kCos * current)

This combines a static term, a term proportional to the measurement, and a cosine term — for example to compensate for gravity on a rotating arm, where the holding torque varies with the cosine of the arm angle.

ConstructorParameters
FFController(double target, double kS, double kF, double kCos) target initial setpoint (stored but unused by the formula) · kS static term · kF gain applied to the measurement · kCos scale applied to the measurement inside the cosine
The cosine term is unscaled. The formula adds Math.cos(kCos * current) directly (there is no separate gain multiplying the cosine output), so its magnitude is always in the range [-1, 1]. Tune kCos so that the units of current map to radians as you intend.

CompoundController

Combines several sub-controllers, summing their outputs each loop. The classic use is adding a feedforward term to a PID feedback term.

CompoundController(double target, Controller... controllers)

On each update(current, dt) it calls update(current, dt) on every sub-controller and returns the sum. The same current and dt are passed to all of them.

The target does not propagate. Each sub-controller keeps its own target. Calling setTarget(...) on the CompoundController only changes the compound controller's own stored target — it does not update the sub-controllers. If you need to re-target, hold references to the sub-controllers and set their targets directly.

NullController

A no-op controller whose update(current, dt) always returns 0. Its target is stored but unused. Handy as a placeholder while wiring up a subsystem, or as a disabled slot inside a CompoundController.

NullController(double target)

Usage

A subsystem typically owns a controller, feeds it the encoder reading each loop, and applies the returned output to its motor. This mirrors how PositionMotorSubsystem uses a Controller internally.

// A PID that drives an arm toward a target tick count.
PIDController arm = new PIDController(0, 0.005, 0.0, 0.0002);

// In the subsystem update loop:
double current = motor.getCurrentPosition();   // encoder ticks
double dt = timer.seconds();                   // seconds since last loop
timer.reset();

double output = arm.update(current, dt);
motor.setPower(output);

Adding gravity compensation with a feedforward term:

PIDController fb = new PIDController(target, 0.005, 0.0, 0.0002);
FFController  ff = new FFController(target, 0.05, 0.0, 0.01);

CompoundController controller = new CompoundController(target, fb, ff);

// Re-target: set BOTH sub-controllers, not just the compound.
fb.setTarget(newTarget);
ff.setTarget(newTarget);

double output = controller.update(current, dt);   // fb.update + ff.update