Greenhouse Climate Controller
A complete controller: it senses, decides and acts at once, using non-blocking timers, hysteresis, proportional fan speed, an interrupt-driven mode button and a live display.
- Expert
- Sensors
- 9 parts
Opens in your browser with the parts, wiring and code already in place. Press Run to watch it work, then change anything you like.
How it works
This is a complete controller, bringing together most of what the other templates teach. A temperature sensor (A0) and a potentiometer (A1, the target temperature, from 15 to 35 °C) are the inputs, and a button on pin 2 changes mode. The outputs are an LED on pin 13 standing in for a heater, a DC motor on pin 5 as a fan, and an OLED that shows the state. As on a real board, the fan would really need a transistor or driver rather than being wired straight to the pin.
Nothing uses delay(). Instead loop() looks at millis() and runs three jobs on their own timers: every 100 milliseconds it senses and decides, every 250 it redraws the OLED, and every 2 seconds it prints a report. That keeps the controller responsive to the button and the serial port the whole time. Deciding uses two ideas. Hysteresis: the heater turns on only when the temperature is more than 1 degree below the target and off only when it is more than 1 degree above, so it does not flick on and off right at the target. And a proportional fan: idle until half a degree over target, then a PWM speed that starts near 90 and rises with each extra degree up to 255.
The Mode button uses an interrupt (see the Interrupt Button Counter) to cycle AUTO, COOL ONLY and OFF immediately, ignoring bounces within 200 milliseconds. The sensor is read eight times and averaged to smooth out noise. Typing ? in the serial monitor prints a status line on demand.
Parts
- Arduino Uno
- Temperature Sensor
- Potentiometer
- Push Button
- DC Motor
- Resistor
- LED
- GND
- OLED Display
Wiring
- Arduino Uno 5V → Temperature Sensor VCC
- Temperature Sensor OUT → Arduino Uno A0
- Arduino Uno GND → Temperature Sensor GND
- Arduino Uno 5V → Potentiometer VCC
- Potentiometer Wiper → Arduino Uno A1
- Arduino Uno GND → Potentiometer GND
- Arduino Uno 5V → Push Button A
- Push Button B → Arduino Uno D2
- Arduino Uno D5 → DC Motor +
- Arduino Uno GND → DC Motor -
- Arduino Uno D13 → Resistor A
- Resistor B → LED Anode (+)
- LED Cathode (-) → GND
- Arduino Uno 3.3V → OLED Display VCC
- Arduino Uno GND → OLED Display GND
- Arduino Uno A5 → OLED Display SCL
- Arduino Uno A4 → OLED Display SDA
Arduino code
// Greenhouse Climate Controller
// The knob sets the target temperature. Below it the heater LED switches on;
// above it the fan spins faster the hotter it gets. The Mode button cycles
// AUTO -> COOL ONLY -> OFF. Drag the temperature slider while it runs to
// heat or chill the greenhouse. Type ? in the serial monitor for a status line.
//
// Nothing here uses delay(): three timers run side by side, so the controller
// keeps reacting to the button and the serial port while it waits.
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#define TEMP_PIN A0
#define SETPOINT_PIN A1
#define BUTTON_PIN 2
#define FAN_PIN 5
#define HEATER_PIN 13
#define MODE_AUTO 0
#define MODE_COOL_ONLY 1
#define MODE_OFF 2
Adafruit_SSD1306 display(128, 64, &Wire, -1);
const unsigned long CONTROL_EVERY = 100; // sense and decide
const unsigned long DISPLAY_EVERY = 250; // redraw the OLED
const unsigned long REPORT_EVERY = 2000; // log to the serial monitor
const float HYSTERESIS = 1.0; // degrees of dead band around the target
volatile int mode = MODE_AUTO;
volatile unsigned long lastPress = 0;
unsigned long lastControl = 0;
unsigned long lastDisplay = 0;
unsigned long lastReport = 0;
float tempC = 0;
float setpointC = 25;
bool heaterOn = false;
int fanSpeed = 0;
// Runs the instant the button goes HIGH, even in the middle of other work
void onModeButton() {
unsigned long now = millis();
if (lastPress == 0 || now - lastPress > 200) { // ignore contact bounce
mode = (mode + 1) % 3;
lastPress = now;
}
}
// The sensor outputs 10 mV per degree. Averaging a few readings smooths noise.
float readTemperature() {
long total = 0;
for (int i = 0; i < 8; i++) {
total = total + analogRead(TEMP_PIN);
}
float voltage = (total / 8.0) * 5.0 / 1024.0;
return voltage * 100.0;
}
// The knob covers 15 to 35 degrees
float readSetpoint() {
return 15.0 + analogRead(SETPOINT_PIN) * 20.0 / 1023.0;
}
void control() {
if (mode == MODE_OFF) {
heaterOn = false;
fanSpeed = 0;
} else {
// Hysteresis: only change the heater once the temperature has moved
// clearly past the target, so it doesn't chatter on and off at the line.
if (mode == MODE_AUTO) {
if (tempC < setpointC - HYSTERESIS) {
heaterOn = true;
} else if (tempC > setpointC + HYSTERESIS) {
heaterOn = false;
}
} else {
heaterOn = false;
}
// Proportional fan: idle until half a degree over, then 90 to 255 PWM
float over = tempC - setpointC;
if (over > 0.5) {
fanSpeed = constrain(round(90 + (over - 0.5) * 33), 90, 255);
} else {
fanSpeed = 0;
}
}
digitalWrite(HEATER_PIN, heaterOn ? HIGH : LOW);
analogWrite(FAN_PIN, fanSpeed);
}
void drawDisplay() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.print("Temp: ");
display.print(tempC, 1);
display.println(" C");
display.print("Target: ");
display.print(setpointC, 1);
display.println(" C");
display.print("Mode: ");
if (mode == MODE_AUTO) display.println("AUTO");
else if (mode == MODE_COOL_ONLY) display.println("COOL ONLY");
else display.println("OFF");
display.print("Heater: ");
display.println(heaterOn ? "ON" : "off");
display.print("Fan: ");
display.println(fanSpeed);
display.display();
}
void report() {
Serial.print("T=");
Serial.print(tempC, 1);
Serial.print("C target=");
Serial.print(setpointC, 1);
Serial.print("C mode=");
Serial.print(mode);
Serial.print(" heater=");
Serial.print(heaterOn ? "ON" : "off");
Serial.print(" fan=");
Serial.println(fanSpeed);
}
void setup() {
Serial.begin(9600);
pinMode(BUTTON_PIN, INPUT);
pinMode(FAN_PIN, OUTPUT);
pinMode(HEATER_PIN, OUTPUT);
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onModeButton, RISING);
Serial.println("Greenhouse controller ready. Type ? for status.");
}
void loop() {
unsigned long now = millis();
if (now - lastControl >= CONTROL_EVERY) {
lastControl = now;
tempC = readTemperature();
setpointC = readSetpoint();
control();
}
if (now - lastDisplay >= DISPLAY_EVERY) {
lastDisplay = now;
drawDisplay();
}
if (now - lastReport >= REPORT_EVERY) {
lastReport = now;
report();
}
if (Serial.available() > 0) {
char c = Serial.read();
if (c == '?') report();
}
}
Try changing it
- Narrow the dead band by changing HYSTERESIS from 1.0 to 0.2 and watch the heater switch more often near the target.
- Make the fan stronger by changing 33 to 66 in (over - 0.5) * 33.
- Log four times as often by changing REPORT_EVERY from 2000 to 500.