Author: Richard Simpson
License: MIT
Platform: Arduino Mega 2560 (PlatformIO)
An 8-zone temperature controller for reptile enclosures. Each zone runs an independent PID loop with a 30-second time-proportional relay window. All configuration and tuning is done from a front-panel LCD menu — no re-flashing required after initial setup.
| Component | Quantity | Notes |
|---|---|---|
| Arduino Mega 2560 | 1 | |
| DS18B20 temperature sensor | 8 | All on a single 1-Wire bus; 4.7 kΩ pull-up to 5 V required |
| DHT22 temperature/humidity sensor | 2 | Left and Right — optional, see ENABLE_DHT22 |
| I²C 20×4 character LCD | 1 | Default address 0x27 (try 0x3F if blank) |
| Relay module (8-channel) | 1 | Active-LOW |
| Tactile buttons | 4 | UP / DOWN / SELECT / BACK |
| Alarm indicator (LED, buzzer, etc.) | 1 | Driven by PIN_ALARM |
Zone layout
| Zones | Label | Load |
|---|---|---|
| 0 – 3 | L1 – L4 | Ceramic heat lamps (above) |
| 4 – 7 | M1 – M4 | Heat mats (below) |
| Signal | Pin |
|---|---|
| DS18B20 data (1-Wire) | 2 |
| DHT22 Left | 3 |
| DHT22 Right | 4 |
| Button UP | 5 |
| Button DOWN | 6 |
| Button SELECT | 7 |
| Button BACK | 8 |
| Alarm output | 30 |
| Relay L1 – L4 | 22 – 25 |
| Relay M1 – M4 | 26 – 29 |
Buttons are wired between pin and GND (internal pull-up used).
All relay pins are active-LOW (RELAY_ACTIVE_HIGH false).
Alarm pin is active-HIGH (ALARM_ACTIVE_HIGH true) — blinks at 1 Hz when any alarm is active.
Flash the firmware with all sensor addresses left as 0x00 (the default).
Open a serial monitor at 115200 baud. On boot the controller prints all
discovered sensor addresses:
DS18B20 sensors found: 8
Copy into ZONE_ADDRS in config.h (then assign to zones):
{ 0x28, 0xFF, 0xA1, 0x2B, 0x4C, 0x1E, 0x00, 0xD3 }, // sensor 0
{ 0x28, 0xFF, 0x3C, 0x44, 0x7A, 0x02, 0x00, 0x91 }, // sensor 1
...
Identify which physical sensor is which by warming them one at a time and
watching which address reports a temperature rise. Then fill in
ZONE_ADDRS in src/config.h with each sensor assigned to the correct
zone row (L1 → M4) and reflash.
Navigate the front-panel menu → Edit Temps to set the target temperature and alarm thresholds for each zone. Settings are saved to EEPROM and survive power cycles.
Use Tune All Zones from the menu to run the relay feedback autotuner on all 8 zones simultaneously. The controller switches each zone to bang-bang mode, measures the natural oscillation, and calculates Ziegler-Nichols PID parameters automatically. Accept the results when prompted.
The home screen cycles automatically through all enabled screens every second:
| Screen | Content | When shown |
|---|---|---|
| LAMPS | L1–L4 current temp, setpoint, relay state (H = heating) |
Always |
| MATS | M1–M4 current temp, setpoint, relay state | Always |
| INFO | Left/Right DHT22 temperature and humidity | When ENABLE_DHT22 1 |
| ALARM | Zones in alarm or sensor fail, alarm count since boot | When any alarm is active |
Pressing any button stops the auto-cycle. It resumes when returning to the home screen from the menu.
Each zone has a configurable low and high alarm threshold (set via Edit Temps in the menu). An alarm activates the first time the temperature crosses the threshold and clears automatically once the temperature returns to within range.
The alarm output pin blinks at 1 Hz whenever any alarm is active and goes off once all alarms clear.
Each zone maintains a circular buffer of the last SENSOR_AVG_SAMPLES (default 4)
raw DS18B20 readings. The running average is what the PID and alarm logic act on,
smoothing out occasional noisy samples. On sensor fail the buffer is cleared so stale
readings cannot pollute the first good reading after recovery.
If a DS18B20 sensor returns an error reading:
- The relay for that zone is forced off immediately (no minimum time guard)
- The PID integral is reset so it restarts cleanly when the sensor recovers
- Any autotune running on that zone is aborted
- A sensor-fail alarm is raised (counted in the boot total, shown on the ALARM screen as
L1: SENSOR FAIL)
A hardware watchdog timer resets the MCU if the main loop stalls for more than 8 seconds (e.g. I²C lockup). On reset all relays are initialised off.
Relays will not change state more often than once per MIN_RELAY_MS (default
1 second) to prevent chatter. Sensor-fail forces the relay off immediately,
bypassing this guard.
Press SELECT from the home screen to enter the menu.
[HOME — auto-cycles through active screens]
UP/DOWN: manual screen select
SELECT → [ZONE SELECT]
UP/DOWN: choose zone or "Tune All Zones"
SELECT (on a zone) → [ZONE ACTION]
> Edit Temps → [ZONE EDIT — TEMPS]
Setpoint / Alarm Lo / Alarm Hi
UP/DN: adjust SEL: next field / commit
BACK: discard
> Edit PID → [ZONE EDIT — PID]
Kp / Ki / Kd
UP/DN: adjust SEL: next field / commit
BACK: discard
> Start Autotune → [SINGLE-ZONE AUTOTUNE]
Shows: temp, setpoint, cycles done, elapsed
BACK: cancel
SEL (when done): accept result
SELECT (Tune All) → [TUNE ALL — all 8 zones simultaneously]
Grid: each zone shows cycles/DONE/FAIL
BACK: cancel all
SEL (when all done): accept successful results
All hardware pin assignments and tunable constants are in one file.
Pins
| Constant | Default | Description |
|---|---|---|
PIN_ONE_WIRE |
2 | DS18B20 1-Wire data pin |
PIN_DHT_LEFT / PIN_DHT_RIGHT |
3, 4 | DHT22 data pins |
PIN_BTN_UP/DOWN/SELECT/BACK |
5–8 | Button pins |
PIN_ALARM |
30 | Alarm output pin |
RELAY_PINS[8] |
22–29 | Relay output pins, zones L1–M4 in order |
RELAY_ACTIVE_HIGH |
false |
Set true for active-HIGH relay modules |
ALARM_ACTIVE_HIGH |
true |
Set false for active-LOW alarm hardware |
LCD_I2C_ADDR |
0x27 |
I²C address of the LCD |
ZONE_ADDRS[8][8] |
all zeros | DS18B20 addresses — fill in after first flash |
Control
| Constant | Default | Description |
|---|---|---|
PID_WINDOW_MS |
30000 | Time-proportional relay window (ms) |
MIN_RELAY_MS |
1000 | Minimum time a relay stays on or off (ms) |
AUTOTUNE_CYCLES |
5 | Oscillation cycles averaged during autotune |
AUTOTUNE_NOISEBAND |
0.5 °C | Relay hysteresis during autotune |
DEFAULT_SETPOINT |
28.0 °C | Setpoint used before first EEPROM write |
DEFAULT_KP/KI/KD |
2.0 / 0.5 / 0.1 | PID gains used before autotune |
DEFAULT_ALARM_LOW |
10.0 °C | Alarm threshold — adjust per enclosure |
DEFAULT_ALARM_HIGH |
45.0 °C | Alarm threshold — adjust per enclosure |
SENSOR_AVG_SAMPLES |
4 | Readings averaged before feeding the PID (1 = no averaging) |
DS18B20_POLL_MS |
2000 | Temperature reading interval (ms) |
Display / UI
| Constant | Default | Description |
|---|---|---|
TEMP_UNIT_F |
0 |
0 = Celsius, 1 = Fahrenheit (display only — PID stays in °C) |
ENABLE_DHT22 |
1 |
0 = disable DHT22; removes INFO screen from rotation |
HOME_CYCLE_MS |
1000 | Home screen auto-cycle interval (ms) |
ALARM_BLINK_MS |
500 | Alarm pin half-period (500 ms on + 500 ms off = 1 Hz) |
DISPLAY_REFRESH_MS |
500 | Live data refresh interval on home screens (ms) |
Requires PlatformIO.
# Build for hardware
pio run
# Flash
pio run --target upload
# Serial monitor (115200 baud — shows DS18B20 addresses on boot)
pio device monitor
# Run native unit tests (no hardware required)
pio test -e nativesrc/
config.h All pin assignments and tunable constants
main.cpp Setup, main loop, watchdog
sensors/
ds18b20.h/cpp Non-blocking 1-Wire reading + address scanner
dht22.h/cpp DHT22 humidity/temperature (optional, display only)
control/
pid.h/cpp Discrete PID — derivative on measurement, anti-windup
zone.h Zone data structure (PID, relay, alarms, sensor state)
zone_manager.h/cpp 8-zone relay driver, PID orchestration, alarm checking,
sensor-fail safety, autotune routing, alarm pin blink
autotune.h/cpp Relay feedback autotuner (Åström-Hägglund method)
storage/
settings.h/cpp EEPROM load/save — setpoints, PID gains, alarm thresholds
ui/
buttons.h/cpp Debounced input with auto-repeat on UP/DOWN
display.h/cpp LCD screen rendering (all screens including alarm)
menu.h/cpp Menu state machine + home screen auto-cycle
test/
mocks/ Arduino library stubs for native builds
test_pid/ 9 PID unit tests
test_menu/ 24 menu state machine tests
test_zone_manager/ 23 zone manager tests (averaging, sensor fail, alarms, relay timing)
test_settings/ 7 EEPROM persistence tests
MIT — see LICENSE.