I am, by any reasonable definition, a coffee nerd. I weigh my beans to the tenth of a gram. I know my group head holds temperature better than the cheap machine it replaced. And the reason that better machine pulls a better shot comes down to four letters that should be familiar to anyone who's gone down the espresso rabbit hole: PID.
A budget espresso machine controls its boiler with a simple thermostat — heat full-blast until it's too hot, switch off until it's too cold, repeat. The temperature saws up and down by several degrees, and your shot tastes different every time. A good machine runs a PID controller instead, holding the water within a fraction of a degree of, say, 93°C. Same boiler, same heating element — vastly better coffee. The magic isn't more power. It's control.
When I set out to keep a rider's heart rate pinned in Zone 2, I realised I'd already met this problem. Holding a heart rate is holding a temperature. So Justzone2's zone targeting is, at its heart, the same controller that's in a nice espresso machine — pointed at your cardiovascular system instead of a boiler.
What a PID actually does (the coffee version)
A PID controller has one job: drive a measured value (the process variable) to a target (the setpoint) and hold it there. It does this by watching the error — the gap between where you are and where you want to be — and reacting in three ways:
P — Proportional. React to the error right now. Boiler 8° too cold? Push hard. 1° off? Ease up. The bigger the gap, the bigger the response.
I — Integral. React to the error that won't go away. If your machine stubbornly sits 2° cool no matter what P does, the integral term quietly accumulates that persistent miss and trims it out. It's what kills the steady-state offset.
D — Derivative. React to how fast the error is changing. Temperature rocketing up toward the target? Start backing off early, before you overshoot and scorch the shot.
Now swap the boiler for a rider. The setpoint is the middle of your Zone 2 heart-rate band. The process variable is your live heart rate. And the "heating element" is the target power we send to the Wahoo KICKR over Bluetooth — more watts drives your heart rate up, fewer lets it drift down.
The core, in about five lines
Strip away the safety rails and the controller is genuinely small. Every second we read the heart rate, compute the error against the setpoint, and turn it into a wattage to add to (or subtract from) your target power:
A positive error means your heart rate is below the zone, so we add watts. Negative means you're above it, so we back off. The proportional term does the bulk of the work; the integral term mops up the slow, stubborn drift — exactly like the boiler that always runs a touch cool.
The real values, all tuned on actual rides:
| Parameter | Value | What it's for |
|---|---|---|
| Kp | 0.5 W/bpm | Proportional gain — react to the error now |
| Ki | 0.008 W/(bpm·s) | Integral gain — erase steady-state drift |
| setpoint | mid Zone 2 | Midpoint of your HR band |
| HR smoothing | 45 s average | Kill beat-to-beat noise before it hits the loop |
| output clamp | ±30 W | Assist your target — never hijack it |
| grace period | 180 s | Let HR settle before the loop engages |
| sample rate | 1 Hz | One correction per second |
Where it stops being a textbook PID
Here's the confession the title buried: there's no D in this PID. And the reason is pure coffee-nerd pedantry about signal quality.
A derivative term reacts to the rate of change of the error. That's brilliant when your sensor is clean — but a heart-rate signal is anything but. It jitters beat to beat, a strap shifts, a packet drops over Bluetooth and you get a phantom 20-bpm spike. Feed that into a derivative term and it amplifies every bit of that noise, yanking the power up and down on garbage data. Coffee people know the equivalent: a noisy thermocouple makes a D-heavy controller chatter the heater. The cure in both worlds is the same — don't differentiate noise.
So instead of a classic derivative, we do something gentler. We keep a heavily smoothed view of your heart rate — a 300-second exponential moving average — and look at the trend of that slow signal: is your HR drifting up, holding, or falling? That trend (in bpm per minute) becomes a gradient factor that schedules the gain:
If your heart rate is already climbing — the classic cardiac drift of a long session — the gradient factor shrinks toward zero and we stop adding power, because the last thing a rising heart rate needs is more watts. It's a derivative's anticipation without a derivative's noise: feedforward damping from a signal slow enough to trust.
You can't un-burn a shot
The other place coffee instinct shaped the code: asymmetry. Over-extracting an espresso is unrecoverable — once it's bitter, it's bitter — so a careful barista creeps up on the target and bails the moment it tips. The controller treats your heart rate the same way. Pushing HR too high is the costly direction: it kicks you out of Zone 2, spikes fatigue, and ruins the whole point of the session. So the loop is deliberately lopsided:
| Direction | Max rate | Why |
|---|---|---|
| add power | +5 W / 90 s | Creep up cautiously — never chase HR over the line |
| shed power | −5 W / 45 s | Retreat twice as fast when HR runs hot |
Quick to back off, slow to push — and that increase rate is itself throttled by the gradient factor, so a rising heart rate makes the controller even more reluctant to add load. The result is a power line that eases rather than lurches.
The rails that keep it honest
Two more safeguards, both with espresso cousins. Anti-windup: the integral term is clamped so it can't silently accumulate a huge correction during the grace period or a sensor dropout and then dump it all at once — the controller equivalent of a boiler that forgets it's already been heating for a minute. And output clamping: the whole correction is bounded to ±30 W of the power you chose. The app assists your target; it never seizes control of the workout. There's a hard floor at 50 W so it can never stall you out.
Tuning is just dialling in
Every coffee nerd has dialled in a new bag: tweak one variable, taste, adjust, repeat. Tuning these gains was the same loop. Too much proportional gain and the system hunts — power and heart rate oscillating around the target like a machine overshooting its temperature every cycle. Too much integral and it lurches: a slow, ponderous overshoot as the accumulated term overcorrects. I landed deliberately conservative, because a Zone 2 ride is long and the human body is a laggy, slow-responding plant — a big thermal mass, basically. Better smooth and patient than snappy and twitchy.
The best control systems are invisible. You don't think about the PID in your espresso machine — you just get a good shot.
That's the whole goal here too. You set a power target, switch on zone targeting, and forget the controller exists. It sits in the background a thousand corrections deep, creeping up and easing off, holding you in the zone while you get lost in a podcast. Just like a good espresso machine: all that careful control, entirely so you never have to think about it.
Now if you'll excuse me, my next shot is calling — and I've got a base-building hour to spin first.