Control: closing the loop
420-302-VA · WEEK 10 · FALL 2026

Utility · keep open during the lab

Troubleshoot

Control bugs feel mystical because the misbehaviour is dynamic, the loop does something wrong over time. They stop being mystical in layers: prove the signals, then the direction, then read the gains off the curve, then audit the timing. A controller is only ever wrong in one of those four ways.

Debug in layers

Four debugging layers in order: A Signals, are PV and u sane numbers; B Direction, does positive error raise the output and raise PV; C Gains, read the response curve against the diagnosis table; D Timing, fixed period, measured dt, fresh messages. A · Signals PV sane? u in 0–100? print pv, u B · Direction +e → u up → PV up? hand test, signs C · Gains read the curve diagnosis table D · Timing fixed beat, true Δt, fresh messages A sign error at layer B makes every gain at layer C look wrong: climb in order.
The one-command test per layer: A, print pv and u for ten cycles and sanity-check both ranges; B, cup your hand over the sensor and confirm u rises; C, run one step test and match the curve to the diagnosis table; D, print the measured dt and watch for lies.

Symptoms and fixes

LayerSymptomLikely causeFix
APV frozen or garbage while the room changesSensor side: wrong pin, broken divider, or stale calibration constantsWeek 8's circuit ladder; re-run the DARK/BRIGHT calibration
Au printed outside 0–100Clamp missing or applied after set_brightnessClamp inside update(), before anything consumes u
BLoop runs away to full bright or full dark and staysSign error: e computed PV − SP, a negated gain, or LED wired so more duty means less light at the sensorFix e = SP − PV; all gains positive; hand test layer B until +e raises PV
BCorrect for small errors, runs away on big onesDuty conversion overflowing (percent vs duty_u16 raw) saturating the wrong wayAudit the percent → 0–65535 conversion at the boundary values
CSettles below SP forever, calmP-only by configuration: Ki = 0, or integral reset every cycle by a rebuilt controller objectSet Ki > 0; construct the PIDController once, outside the loop
CFast ringing that persists or growsGain past the delay's tolerance, locally or after moving to the networkCut Kp (then Ki); central loops need softer gains than local ones
CReaches SP, rolls slowly past, lazily returnsIntegral overdone, or windup: anti-windup missing while saturatedLower Ki; confirm the conditional-integration else is present
COutput shimmers at rest; LED visibly tremblesD amplifying noise: raw reads, flickering lamp nearby, Kd highAverage reads, lower Kd or go PI; move the bench off the flicker
CSaturated at 100 % and short of SP, no ringingInfeasible setpoint: plant ceiling below SP tonightLower SP or accept it: a plant limit, not a code fault
DI and D occasionally spike for one cycleΔt lying: assumed constant, or ticks_diff not used across the wrapMeasure dt each cycle with ticks_ms/ticks_diff; print it to see the jitter
DCentral loop reacts to a light level from minutes agoGhost retained message on the light topic, or the monitor subscribed after a backlogClear retained (Week 9's command); verify with the firehose that fresh values flow
DCentral loop commands arrive in bursts, loop lurchesPi computing faster than telemetry arrives, or dt assumed 1.0 s under jitterCompute only in on_message, one update per arrival, dt by time.monotonic() between messages
DNode obeys old duties after the Pi controller stopsLast command persists; nothing says "controller gone"Rung 1's status topic: node falls back to a safe duty when the controller's status goes offline, Week 9's LWT earning its keep

Before you ask for help

  1. Name your layer, and show the layer test that fails (the ten-cycle print, the hand test, the curve, the dt print).
  2. Bring the curve: a step-test CSV or a photo of the plotted response says more than any adjective.
  3. State your gains and your last change; the tuning log should already say why.
  4. For distributed weirdness, bring thirty seconds of firehose output; stale topics hide nowhere else.