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.
The base type: a target plus
update(current, dt).Proportional + integral + derivative on error.
Static + linear + cosine term from the measurement.
Sums the outputs of several sub-controllers.
Always returns
0.The Controller base type
All controllers extend Controller, an abstract class with one field and three
members you interact with:
| Member | Signature | Description |
|---|---|---|
| constructor | Controller(double target) | Stores the initial setpoint. |
update | abstract 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. |
getTarget | double getTarget() | Returns the current setpoint. |
setTarget | void setTarget(double target) | Changes the setpoint. Takes effect on the next update(). |
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, timeskD.
As implemented, the full output is:
error * kP + integral * kI + derivative * kD
| Constructor | Parameters |
|---|---|
PIDController(double target, double kP, double kI, double kD) |
target initial setpoint · kP proportional gain ·
kI integral gain · kD derivative gain |
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.
| Constructor | Parameters |
|---|---|
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 |
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.
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