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
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
| Layer | Symptom | Likely cause | Fix |
|---|---|---|---|
| A | PV frozen or garbage while the room changes | Sensor side: wrong pin, broken divider, or stale calibration constants | Week 8's circuit ladder; re-run the DARK/BRIGHT calibration |
| A | u printed outside 0–100 | Clamp missing or applied after set_brightness | Clamp inside update(), before anything consumes u |
| B | Loop runs away to full bright or full dark and stays | Sign error: e computed PV − SP, a negated gain, or LED wired so more duty means less light at the sensor | Fix e = SP − PV; all gains positive; hand test layer B until +e raises PV |
| B | Correct for small errors, runs away on big ones | Duty conversion overflowing (percent vs duty_u16 raw) saturating the wrong way | Audit the percent → 0–65535 conversion at the boundary values |
| C | Settles below SP forever, calm | P-only by configuration: Ki = 0, or integral reset every cycle by a rebuilt controller object | Set Ki > 0; construct the PIDController once, outside the loop |
| C | Fast ringing that persists or grows | Gain past the delay's tolerance, locally or after moving to the network | Cut Kp (then Ki); central loops need softer gains than local ones |
| C | Reaches SP, rolls slowly past, lazily returns | Integral overdone, or windup: anti-windup missing while saturated | Lower Ki; confirm the conditional-integration else is present |
| C | Output shimmers at rest; LED visibly trembles | D amplifying noise: raw reads, flickering lamp nearby, Kd high | Average reads, lower Kd or go PI; move the bench off the flicker |
| C | Saturated at 100 % and short of SP, no ringing | Infeasible setpoint: plant ceiling below SP tonight | Lower SP or accept it: a plant limit, not a code fault |
| D | I and D occasionally spike for one cycle | Δt lying: assumed constant, or ticks_diff not used across the wrap | Measure dt each cycle with ticks_ms/ticks_diff; print it to see the jitter |
| D | Central loop reacts to a light level from minutes ago | Ghost retained message on the light topic, or the monitor subscribed after a backlog | Clear retained (Week 9's command); verify with the firehose that fresh values flow |
| D | Central loop commands arrive in bursts, loop lurches | Pi computing faster than telemetry arrives, or dt assumed 1.0 s under jitter | Compute only in on_message, one update per arrival, dt by time.monotonic() between messages |
| D | Node obeys old duties after the Pi controller stops | Last 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
- Name your layer, and show the layer test that fails (the ten-cycle print, the hand test, the curve, the dt print).
- Bring the curve: a step-test CSV or a photo of the plotted response says more than any adjective.
- State your gains and your last change; the tuning log should already say why.
- For distributed weirdness, bring thirty seconds of firehose output; stale topics hide nowhere else.