The Deadband That Keeps My SCOBY Warm
My kombucha jar sits on a seedling heat mat because the SCOBY sulks below about 22°C and my basement runs cool. The obvious control law is: if it’s colder than 24, turn the mat on; if it’s warmer, turn it off. That version is a disaster. Right at the setpoint the sensor noise flickers 23.98, 24.01, 23.99, and the relay clacks on and off several times a second — relay chatter, the electromechanical equivalent of a nervous tic. It wears out contacts and does nothing useful for the tea.
The cure is a deadband: two thresholds instead of one. Heat on only when you fall below 23.5, heat off only when you climb past 24.5, and in the 1°C gap between them, do nothing at all. Whatever state you were in, you keep. That memory-in-the-middle is hysteresis, and it’s the same trick a Schmitt trigger plays on a noisy voltage. It’s why your household thermostat doesn’t machine-gun the furnace, and by the 1980s it was baked into every embedded temperature controller with a microcontroller and a thermistor.
def thermostat(temps, setpoint=24.0, deadband=0.5):
heating = False
for t in temps:
if heating and t >= setpoint + deadband:
heating = False
elif not heating and t <= setpoint - deadband:
heating = True
print(f"{t:5.1f} C heat={'ON ' if heating else 'off'}")
thermostat([22.0, 23.6, 24.4, 24.6, 24.1, 23.3, 23.5])
22.0 C heat=ON
23.6 C heat=ON
24.4 C heat=ON <- past setpoint, still heating
24.6 C heat=off <- crossed the upper edge
24.1 C heat=off <- inside the band, stays off
23.3 C heat=ON <- crossed the lower edge
Notice 24.4 keeps heating and 24.1 stays off — same temperature region, opposite decisions, because the controller remembers where it came from. The identical logic in Zig, where the two comparisons and the single bit of state are all the machine actually needs:
const std = @import("std");
pub fn main() void {
const temps = [_]f32{ 22.0, 23.6, 24.4, 24.6, 24.1, 23.3, 23.5 };
const set: f32 = 24.0;
const band: f32 = 0.5;
var heating = false;
for (temps) |t| {
if (heating and t >= set + band) {
heating = false;
} else if (!heating and t <= set - band) {
heating = true;
}
std.debug.print("{d:5.1} C heat={s}\n", .{ t, if (heating) "ON " else "off" });
}
}
Wider deadband, calmer relay but sloppier temperature; narrower band, tighter control but more switching. I’ve settled on about a degree, which lets the mat cycle a few times an hour and holds the tea in its happy range. The brew doesn’t care about the exact number. It cares that I stopped trying to be precise about a moving target and let a small zone of not-deciding do the work.