6 sketches that compile on a stock Arduino IDE with zero libraries to install, each carrying the part tutorials skip: compressor short-cycle lockout, sensor-fault cut-off, over-temperature latch, dry-run detector and PI anti-windup. Every timer uses unsigned millis() arithmetic, so nothing jumps when millis() wraps at 4294967296 ms (49.71 days) - the point where a naive thermostat's timers break. A thermostat that only compares a number to a setpoint will hold the relay closed against a shorted sensor all night.
arduino:avr:uno
Free slice of the pack: a complete single-stage heating thermostat you can upload as-is - hysteresis band, sensor-fault cut-off, and minimum on/off timers so the relay cannot chatter around the setpoint.
Start here. Wiring: 5V to one leg of a 10k NTC thermistor, the other leg to A0, and a 10k 1% resistor from A0 to GND (the sensor is on top of the divider - reverse it and the temperature reads backwards). D8 goes to the IN pin of a relay module, its VCC to 5V and its GND to GND. D13 is the on-board LED and mirrors the relay, so you can test the logic with nothing else connected. Open Serial Monitor at 9600 baud and one CSV line per second appears: temp_c,setpoint_c,heater,lockout_s. CHANGE THIS ONE LINE FOR YOUR PART: BETA_COEFFICIENT - it is printed on your thermistor's datasheet (3950 and 3435 are the two common ones) and it is the only number that decides whether the temperature column is right. Second line to change: RELAY_ACTIVE_LOW, set to false if your board is active-high. What a tutorial leaves out and this has: MIN_OFF_MS keeps the output off for three minutes after it stops, and because lastSwitchMs is set in setup(), that off-delay also runs after a power cut - so a whole street of boards coming back after an outage do not all switch on in the same second.
| Pin | Mode | Connect to |
|---|---|---|
A0 | INPUT | 10k NTC thermistor to 5V, 10k fixed resistor to GND (divider midpoint) |
8 | OUTPUT | heater relay module IN |
13 | OUTPUT | on-board LED, mirrors the relay |
| Name | Value | Unit |
|---|---|---|
SERIES_RESISTOR | 10000 | ohm - the fixed resistor from A0 to GND |
NOMINAL_RESISTANCE | 10000 | ohm - your thermistor's resistance at 25 C |
NOMINAL_TEMP_C | 25 | C - the temperature that resistance is quoted at |
BETA_COEFFICIENT | 3950 | K - the Beta value on your thermistor datasheet |
SETPOINT_C | 21 | C - the temperature you want to hold |
HYSTERESIS_C | 0.8 | C - half the switching band, above and below the setpoint |
MIN_OFF_MS | 180000 | ms - shortest time the output stays off (3 min) |
MIN_ON_MS | 60000 | ms - shortest time the output stays on (1 min) |
SAMPLE_MS | 1000 | ms between readings |
ADC_FAULT_LOW | 20 | raw ADC - below this the sensor is treated as shorted |
ADC_FAULT_HIGH | 1003 | raw ADC - above this the sensor is treated as open |
RELAY_ACTIVE_LOW | 1 | 1 = your relay board switches ON with a LOW output; set 0 for active-high |
Heating and cooling from one sensor with a dead band between them, a changeover delay, and a single mode variable that makes running both outputs at once impossible.
For anything with a fridge or a fan as well as a heater: fermentation chamber, curing cabinet, wine fridge, incubator. Wiring is the ThermostatHysteresis divider on A0, plus D8 to the heat relay IN and D9 to the cool relay IN. Serial Monitor at 9600 baud prints temp_c,setpoint_c,mode,held_s where mode is 0=idle 1=heat 2=cool. CHANGE THIS ONE LINE FOR YOUR PART: DEADBAND_C - it is the half-width of the do-nothing band, so 1.5 means the chamber is left alone between 18.5 C and 21.5 C at a setpoint of 20. What a tutorial leaves out and this has: the two outputs are driven from one mode variable, so no combination of readings can energise both; MIN_OFF_MS (five minutes by default) holds a compressor off after it stops, because restarting a compressor against head pressure seconds after it stopped is the standard way to destroy one; and CHANGEOVER_MS makes the controller wait ten minutes after heating before it is allowed to cool, so a badly placed sensor cannot make the box fight itself.
| Pin | Mode | Connect to |
|---|---|---|
A0 | INPUT | 10k NTC thermistor to 5V, 10k fixed resistor to GND |
8 | OUTPUT | heat relay IN |
9 | OUTPUT | cool / compressor relay IN |
13 | OUTPUT | on-board LED, lit while either output runs |
| Name | Value | Unit |
|---|---|---|
SERIES_RESISTOR | 10000 | ohm - the fixed resistor from A0 to GND |
NOMINAL_RESISTANCE | 10000 | ohm - your thermistor's resistance at 25 C |
NOMINAL_TEMP_C | 25 | C - the temperature that resistance is quoted at |
BETA_COEFFICIENT | 3950 | K - the Beta value on your thermistor datasheet |
SETPOINT_C | 20 | C - the temperature you want to hold |
DEADBAND_C | 1.5 | C - half the do-nothing band; nothing runs inside it |
MIN_ON_MS | 120000 | ms - shortest run for either output (2 min) |
MIN_OFF_MS | 300000 | ms - compressor off-delay (5 min) |
CHANGEOVER_MS | 600000 | ms - wait before switching between heat and cool (10 min) |
SAMPLE_MS | 1000 | ms between readings |
ADC_FAULT_LOW | 20 | raw ADC - below this the sensor is treated as shorted |
ADC_FAULT_HIGH | 1003 | raw ADC - above this the sensor is treated as open |
RELAY_ACTIVE_LOW | 1 | 1 = your relay board switches ON with a LOW output; set 0 for active-high |
The same thermostat with the setpoint on a potentiometer, plus the two filters that stop a cheap pot and a noisy ADC from making the relay chatter.
Use this when whoever operates the box should be able to change the temperature without a laptop. Wiring: the A0 divider as before, plus a 10k potentiometer with one end to 5V, the other to GND and the wiper to A1. D8 drives the relay. Serial Monitor at 9600 baud prints temp_c,setpoint_c,heater,knob_adc so you can see the knob position while you turn it. CHANGE THESE TWO LINES FOR YOUR PART: SETPOINT_MIN_C and SETPOINT_MAX_C - the full sweep of the knob covers exactly that range, so a 5 to 35 range gives roughly 3 C per hour on a clock face and a 15 to 25 range gives you a knob you can actually set. What a tutorial leaves out and this has: KNOB_DEADBAND_ADC ignores wiper movement smaller than 8 ADC counts, so a worn pot cannot drift the setpoint on its own, and an exponential moving average with SMOOTHING_ALPHA damps the temperature reading before it is compared - raise ALPHA toward 1.0 to react faster, lower it toward 0.05 for a very steady chamber.
| Pin | Mode | Connect to |
|---|---|---|
A0 | INPUT | 10k NTC thermistor to 5V, 10k fixed resistor to GND |
A1 | INPUT | wiper of a 10k potentiometer (ends to 5V and GND) |
8 | OUTPUT | heater relay module IN |
13 | OUTPUT | on-board LED, mirrors the relay |
| Name | Value | Unit |
|---|---|---|
SERIES_RESISTOR | 10000 | ohm - the fixed resistor from A0 to GND |
NOMINAL_RESISTANCE | 10000 | ohm - your thermistor's resistance at 25 C |
NOMINAL_TEMP_C | 25 | C - the temperature that resistance is quoted at |
BETA_COEFFICIENT | 3950 | K - the Beta value on your thermistor datasheet |
SETPOINT_MIN_C | 5 | C - knob fully anticlockwise |
SETPOINT_MAX_C | 35 | C - knob fully clockwise |
KNOB_DEADBAND_ADC | 8 | raw ADC - smaller knob movements are ignored |
SMOOTHING_ALPHA | 0.2 | 0-1 weight of each new reading in the moving average |
HYSTERESIS_C | 0.5 | C - half the switching band |
MIN_OFF_MS | 180000 | ms - shortest time the output stays off |
MIN_ON_MS | 60000 | ms - shortest time the output stays on |
SAMPLE_MS | 1000 | ms between readings |
ADC_FAULT_LOW | 20 | raw ADC - below this the sensor is treated as shorted |
ADC_FAULT_HIGH | 1003 | raw ADC - above this the sensor is treated as open |
RELAY_ACTIVE_LOW | 1 | 1 = your relay board switches ON with a LOW output; set 0 for active-high |
The unattended-overnight version: an over-temperature latch, a no-rise detector for a heater that has failed, and a debounced button that is the only way back on.
This is the one to run when nothing is watching the box - a reptile mat, a brooder, a proofing cabinet. Wiring: the A0 divider, D8 to the heater relay or SSR, a push button from D2 to GND (INPUT_PULLUP, no resistor needed), and an alarm LED with a 220 ohm resistor on D9. Serial Monitor at 9600 baud prints temp_c,setpoint_c,heater,fault_code where the code is 0 none, 1 over-temperature, 2 no rise, 3 sensor. CHANGE THIS ONE LINE FOR YOUR PART: LIMIT_C - set it just above the hottest surface temperature your animal, dough or plastic can survive, not just above your setpoint. What a tutorial leaves out and this has: three separate things latch the heater off and stay latched - crossing LIMIT_C, a shorted or open sensor, and a heater that has run for RISE_WINDOW_MS without the temperature climbing MIN_RISE_C (that is the signature of a burnt-out element, a tripped socket or a sensor taped to the wrong surface). Clearing the latch needs a physical button press, debounced over DEBOUNCE_MS, and the press is refused while the sensor still reads at or above the setpoint, so you cannot restart a fault by leaning on the button.
| Pin | Mode | Connect to |
|---|---|---|
A0 | INPUT | 10k NTC thermistor to 5V, 10k fixed resistor to GND |
2 | INPUT_PULLUP | reset push button to GND |
8 | OUTPUT | heater relay or SSR IN |
9 | OUTPUT | alarm LED through a 220 ohm resistor to GND |
| Name | Value | Unit |
|---|---|---|
SERIES_RESISTOR | 10000 | ohm - the fixed resistor from A0 to GND |
NOMINAL_RESISTANCE | 10000 | ohm - your thermistor's resistance at 25 C |
NOMINAL_TEMP_C | 25 | C - the temperature that resistance is quoted at |
BETA_COEFFICIENT | 3950 | K - the Beta value on your thermistor datasheet |
SETPOINT_C | 30 | C - the temperature you want to hold |
HYSTERESIS_C | 0.5 | C - half the switching band |
LIMIT_C | 45 | C - hard cut-out, latches the heater off |
MIN_RISE_C | 0.5 | C the heater must gain inside RISE_WINDOW_MS |
RISE_WINDOW_MS | 300000 | ms of continuous heating checked for that rise (5 min) |
DEBOUNCE_MS | 30 | ms the button must be steady before it counts |
SAMPLE_MS | 1000 | ms between readings |
ADC_FAULT_LOW | 20 | raw ADC - below this the sensor is treated as shorted |
ADC_FAULT_HIGH | 1003 | raw ADC - above this the sensor is treated as open |
RELAY_ACTIVE_LOW | 1 | 1 = your relay board switches ON with a LOW output; set 0 for active-high |
Day and night setpoints without an RTC module, a hard cap on how many minutes the heater may run per 24 hours, and timers that stay correct when millis() wraps.
For a greenhouse, a workshop or a spare room where the temperature should drop overnight and where the bill matters. Wiring is the A0 divider plus D8 to the relay - no clock module to buy. You set BOOT_HOUR to the hour on your own clock at the moment you press Upload, and the sketch counts hours from there; there is no real-time clock in an Uno and the 16 MHz oscillator drifts, so treat this as setback, not as a schedule you would trust to the minute after a month. Serial Monitor at 9600 baud prints hour,temp_c,setpoint_c,heater,run_min,capped. CHANGE THIS ONE LINE FOR YOUR PART: BOOT_HOUR, every time you upload. What a tutorial leaves out and this has: DAILY_RUN_CAP_MS (28800000 ms = 8 hours) is a budget of heater runtime per rolling 24 hour window - when it is spent the output is forced off and capped shows 1, which turns a stuck door or an open vent into a bounded bill instead of an unbounded one. Every timer here compares unsigned differences, so nothing jumps when millis() wraps at 4294967296 ms - 49.71 days - which is exactly when a naive schedule using a plain comparison would fail.
| Pin | Mode | Connect to |
|---|---|---|
A0 | INPUT | 10k NTC thermistor to 5V, 10k fixed resistor to GND |
8 | OUTPUT | heater relay module IN |
13 | OUTPUT | on-board LED, mirrors the relay |
| Name | Value | Unit |
|---|---|---|
SERIES_RESISTOR | 10000 | ohm - the fixed resistor from A0 to GND |
NOMINAL_RESISTANCE | 10000 | ohm - your thermistor's resistance at 25 C |
NOMINAL_TEMP_C | 25 | C - the temperature that resistance is quoted at |
BETA_COEFFICIENT | 3950 | K - the Beta value on your thermistor datasheet |
BOOT_HOUR | 18 | hour 0-23 on your clock when you press Upload |
DAY_START_HOUR | 6 | hour the day setpoint takes over |
NIGHT_START_HOUR | 22 | hour the night setpoint takes over |
DAY_SETPOINT_C | 21 | C held during the day |
NIGHT_SETPOINT_C | 17 | C held overnight |
HYSTERESIS_C | 0.6 | C - half the switching band |
MIN_OFF_MS | 180000 | ms - shortest time the output stays off |
MIN_ON_MS | 60000 | ms - shortest time the output stays on |
DAILY_RUN_CAP_MS | 28800000 | ms of heating allowed per 24 h window (8 h) |
DAY_WINDOW_MS | 86400000 | ms in the budget window (24 h) |
MS_PER_HOUR | 3600000 | ms per hour tick |
SAMPLE_MS | 1000 | ms between readings |
ADC_FAULT_LOW | 20 | raw ADC - below this the sensor is treated as shorted |
ADC_FAULT_HIGH | 1003 | raw ADC - above this the sensor is treated as open |
RELAY_ACTIVE_LOW | 1 | 1 = your relay board switches ON with a LOW output; set 0 for active-high |
Time-proportioned PI control for an SSR: instead of full-on/full-off it holds a duty percentage, with anti-windup on the integral and a ceiling on the duty.
Use this where on/off overshoots: sous-vide, a fermenter with a small heater, a 3D-printer enclosure, a curing box. Wiring: the A0 divider, and D3 to the input + of a solid state relay or a logic-level MOSFET gate, with the SSR input - to GND. Do not use a mechanical relay here - the output switches every WINDOW_MS and contacts would wear out. Serial Monitor at 9600 baud prints temp_c,setpoint_c,duty_pct,integral_pct once per window. CHANGE THIS ONE LINE FOR YOUR PART: KP_PERCENT_PER_C - it is percent of duty per degree of error, so 25 means the heater is at full allowed duty when you are 3.2 C below setpoint; raise it if the approach is sluggish, lower it if it overshoots. What a tutorial leaves out and this has: the integral term only accumulates while the output is off its limits (that is anti-windup - without it a lid left open winds the integral up and the box overshoots for an hour afterwards), MAX_DUTY_PERCENT caps the duty at 80 so an element rated for intermittent use is never held on continuously, LIMIT_C latches the output off entirely, and the whole window is driven by millis() rather than delay(), so the sensor is still read and the output can still be cut while a window is running.
| Pin | Mode | Connect to |
|---|---|---|
A0 | INPUT | 10k NTC thermistor to 5V, 10k fixed resistor to GND |
3 | OUTPUT | SSR input + or logic-level MOSFET gate |
13 | OUTPUT | on-board LED, mirrors the SSR |
| Name | Value | Unit |
|---|---|---|
SERIES_RESISTOR | 10000 | ohm - the fixed resistor from A0 to GND |
NOMINAL_RESISTANCE | 10000 | ohm - your thermistor's resistance at 25 C |
NOMINAL_TEMP_C | 25 | C - the temperature that resistance is quoted at |
BETA_COEFFICIENT | 3950 | K - the Beta value on your thermistor datasheet |
SETPOINT_C | 60 | C - the temperature you want to hold |
KP_PERCENT_PER_C | 25 | percent of duty per degree of error |
KI_PERCENT_PER_C_S | 0.15 | percent of duty per degree per second |
INTEGRAL_MAX_PERCENT | 40 | percent - clamp on the accumulated integral |
MAX_DUTY_PERCENT | 80 | percent - highest duty the heater may ever be given |
LIMIT_C | 85 | C - latches the output off for good until power cycle |
WINDOW_MS | 5000 | ms - length of one time-proportioned window |
ADC_FAULT_LOW | 20 | raw ADC - below this the sensor is treated as shorted |
ADC_FAULT_HIGH | 1003 | raw ADC - above this the sensor is treated as open |
After the calculator you have correct numbers and still no controller that survives a shorted sensor, a compressor restarted twenty seconds after it stopped, or an element that has burnt out while the relay stays cheerfully closed. The paid pack is the machine that acts on those numbers: 6 sketches
One payment, one licence key for this tool. The key is shown right after payment.
The alternative is a sealed panel controller: its hysteresis, its minimum-off delay and what it does with a broken sensor are decided by the maker, and you can neither read nor change any of them. Here you get the source of 6 cont
Buy the full version — $44