Siddhant Kumar
Project 027 · Agriculture

Smart Greenhouse Controller.

A greenhouse controller that manages temperature, humidity, light and ventilation together — respecting how they interact — rather than as four independent thermostats fighting each other.

Intermediate 16–24 hours 40 min read ClimateAutomationSensors
Jump to source Bill of materials
Smart Greenhouse Controller — reference build illustration MCU VCC · GND · SIG · NC
Difficulty
Intermediate
Build time
16–24 hours
Indicative cost
₹4,600 – ₹8,500 depending on actuators
Platform
ESP32 DevKit V1 (ESP-WROOM-32)
Category
Agriculture
Last updated
28 July 2026
Contents — 26 sections

Project Overview

A greenhouse controller that manages temperature, humidity, light and ventilation together — respecting how they interact — rather than as four independent thermostats fighting each other.

A greenhouse is a coupled system, and that is what makes controlling it interesting. Opening a vent to drop the temperature also drops the humidity and admits outside CO₂. Running a heater raises the temperature and lowers the relative humidity. Misting to raise humidity cools the air. A naive controller with four independent loops — one for each variable — spends its day fighting itself: the vent opens to cool, the humidity alarm fires, the mister runs, which cools further, so the heater comes on. This project treats the greenhouse as the coupled system it is.

The controller manages temperature, humidity, light and ventilation with an awareness of how each actuator affects multiple variables. It uses a priority-and-deadband scheme rather than four independent PID loops: at any moment it decides which variable is furthest outside its acceptable band, chooses the actuator that best corrects it with the least disruption to the others, and moves in small steps. This is how commercial greenhouse computers actually work, and it produces stable conditions instead of oscillation.

The measurements are done properly — the temperature and humidity sensor is shielded and aspirated (a small fan draws air past it) so it reads the greenhouse air rather than a sun-warmed enclosure, and the light sensor measures photosynthetically active radiation so supplemental lighting is driven by what plants actually use, not by lux.

The design also respects the day/night cycle that plants live by. Target temperatures, humidity and CO₂ differ between day and night, ventilation strategy changes, and supplemental lighting extends the photoperiod to a target daily light integral. The result is a controller that maintains a genuinely good growing environment with a handful of cheap actuators, and that logs everything so you can see what your greenhouse actually does over a season.

The interior of a commercial greenhouse with rows of plants
The interior of a greenhouse. Coordinated control of temperature, humidity, light and ventilation keeps conditions stable across the whole space. Photograph sourced from Wikimedia Commons — Greenhouse interior.jpg. Reused under the licence stated on that page; please check it before republishing.

What this project does

  • Measures air temperature, humidity, light (PAR), soil moisture and optionally CO₂.
  • Controls heating, ventilation, misting/humidification and supplemental lighting.
  • Coordinates actuators with awareness of their coupled effects, avoiding self-defeating loops.
  • Applies separate day and night targets and manages the photoperiod.
  • Drives supplemental lighting toward a target daily light integral.
  • Logs all conditions and actuator states over the season.
  • Alerts on out-of-range conditions that risk crop damage (frost, overheating, condensation).

Real-World Applications

SettingHow it is used
Hobby and market-garden greenhousesStable conditions and extended seasons with cheap, coordinated automation.
Propagation and seedling raisingTight environmental control is what germination and young plants need.
Research and controlled-environment growingRepeatable conditions and full logging for experiments.
Polytunnels and high tunnelsEven passive structures benefit hugely from coordinated ventilation control.
Educational growingA complete, legible example of coupled-system control with a living result.
Specialty cropsOrchids, carnivorous plants and others with specific, coupled environmental needs.

Deployment contexts where a build of this kind earns its keep.

Features & Capabilities

  • Coordinated multi-variable control — priority-and-deadband, not four fighting loops.
  • Aspirated, shielded sensing so the reading is the greenhouse air, not a hot box.
  • PAR-based lighting driven by photosynthetically active radiation, not lux.
  • Daily light integral tracking, the metric that actually governs plant growth.
  • Day/night regime with separate targets and a managed photoperiod.
  • Vapour-pressure-deficit awareness, the humidity metric that matters for transpiration and disease.
  • Frost, overheat and condensation protection with priority overrides.
  • Full seasonal logging and a dashboard for optimisation.

Difficulty, Time & Required Skills

AttributeValue
Difficulty levelIntermediate
Estimated completion time16–24 hours
Indicative build cost₹4,600 – ₹8,500 depending on actuators
Primary disciplineAgriculture
Reference platformESP32 DevKit V1 (ESP-WROOM-32)

Skills you should have (or will pick up)

  • Arduino C++ with state machines and coordinated control
  • Multi-sensor interfacing (I²C, analogue)
  • Relay and PWM actuator control
  • Understanding of VPD, DLI and the coupling between greenhouse variables
  • MQTT and dashboarding

Bill of Materials

Every part below is commonly available from Indian and international hobby-electronics suppliers. Prices are indicative 2026 retail figures in Indian rupees and will drift — treat them as a budgeting guide, not a quotation.

ComponentKey specificationQtyApprox. cost
ESP32 DevKit V1 (ESP-WROOM-32)
Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly.
Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18× 12-bit ADC, 2× 8-bit DAC1₹450
SHT31-D temperature + humidity sensor
The built-in heater burns off condensation — essential for outdoor or greenhouse use.
−40 to +125 °C ±0.2 °C, 0–100 %RH ±2 %, 1 Hz–10 Hz, on-chip heater1₹620
BH1750 digital ambient light sensor
Far more linear than an LDR — use it whenever you need real lux, not a relative value.
1–65535 lx, 16-bit, ±20 %, spectral response close to the human eye1₹140
Capacitive soil-moisture sensor v2.0
Always pick capacitive over the cheap resistive fork — resistive probes corrode in weeks.
Corrosion-free capacitive sensing, 0–3 V analogue swing, 55 × 20 mm probe1₹180
4-channel opto-isolated relay board
All four coils energised draw ~280 mA — do not power from the MCU 5 V pin.
4 × SPDT, 10 A @ 250 VAC, active-low inputs, LED per channel1₹280
0.96″ SSD1306 OLED display
Static images burn in — invert or scroll the screen periodically.
128 × 64 monochrome, 1.3–3.3 V logic, 100 kHz–400 kHz I²C1₹250
LM2596 adjustable buck converter module
Set the output voltage with no load connected before wiring the board.
4.5–40 V in, 1.25–37 V out, 2 A (3 A peak), ~92 % efficiency1₹90
12 V 5 A SMPS adapter
Fuse the 12 V rail at ~1.5× your calculated draw.
100–240 VAC in, 12 V ±5 %, 5 A, 60 W, DC 5.5 × 2.1 mm barrel1₹650
Double-sided perfboard 7 × 9 cm + headers
Solder female headers so the MCU can be swapped without desoldering.
FR-4, 0.1″ pitch, plated through-holes, 24 × 18 grid1₹60
IP65 ABS junction enclosure 158 × 90 × 60 mm
Fit cable glands, not drilled holes, or the IP rating means nothing.
IP65, ABS, −20 to +80 °C, transparent lid, wall-mount lugs1₹260
Aspiration fan + radiation shield
Without aspiration and shielding, the temperature reads the sun on the enclosure, not the air.
40 mm 5 V fan drawing air past the SHT31 in a shield1₹200
PAR sensor (or calibrated quantum sensor)
A true PAR sensor is best; a BH1750 with a crop-specific lux-to-PAR factor is a budget approximation.
Photosynthetic photon flux, or a calibrated BH1750 approximation1₹900
Greenhouse actuators
Actuator choice depends on greenhouse size; the controller logic is the same.
12 V vent opener/fan, heater relay, misting pump, grow-light relay1₹2,500
MH-Z19B CO₂ sensor (optional)NDIR, for CO₂ enrichment control1₹2,600

Estimated total: ₹9,180, excluding tools, shipping and consumables.

Tools and consumables

  • Soldering iron (temperature controlled, 350 °C) with 0.8 mm 60/40 or lead-free solder
  • Digital multimeter — continuity, DC volts and current ranges
  • Wire strippers, flush cutters and a small set of precision screwdrivers
  • Heat-shrink tubing and a heat gun (or a lighter, carefully)
  • A laptop with a USB port and the toolchain listed above

Hardware Specifications

PartSpecificationSupplyInterfaceReference
ESP32 DevKit V1 (ESP-WROOM-32)Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18× 12-bit ADC, 2× 8-bit DAC3.3 V logic / 5 V USBUART, SPI, I²C, I²S, CAN, PWMDatasheet
SHT31-D temperature + humidity sensor−40 to +125 °C ±0.2 °C, 0–100 %RH ±2 %, 1 Hz–10 Hz, on-chip heater2.4–5.5 VI²C (0x44/0x45)Datasheet
BH1750 digital ambient light sensor1–65535 lx, 16-bit, ±20 %, spectral response close to the human eye2.4–3.6 VI²C (0x23/0x5C)Datasheet
Capacitive soil-moisture sensor v2.0Corrosion-free capacitive sensing, 0–3 V analogue swing, 55 × 20 mm probe3.3–5.5 VAnalogueDatasheet
4-channel opto-isolated relay board4 × SPDT, 10 A @ 250 VAC, active-low inputs, LED per channel5 V coil4× digitalDatasheet
0.96″ SSD1306 OLED display128 × 64 monochrome, 1.3–3.3 V logic, 100 kHz–400 kHz I²C3.3–5 VI²C (0x3C)Datasheet
LM2596 adjustable buck converter module4.5–40 V in, 1.25–37 V out, 2 A (3 A peak), ~92 % efficiency4.5–40 VScrew terminals + trimmerDatasheet
12 V 5 A SMPS adapter100–240 VAC in, 12 V ±5 %, 5 A, 60 W, DC 5.5 × 2.1 mm barrel12 VDC barrelDatasheet
Double-sided perfboard 7 × 9 cm + headersFR-4, 0.1″ pitch, plated through-holes, 24 × 18 gridDatasheet
IP65 ABS junction enclosure 158 × 90 × 60 mmIP65, ABS, −20 to +80 °C, transparent lid, wall-mount lugsDatasheet

Consolidated electrical and interface specifications for every active part in the build.

Power Budget & Supply Sizing

Add up the typical active current of every part, then size the supply with at least 50 % headroom so transmit bursts and motor inrush never brown out the controller.

LoadSupply railTypical current (mA)Notes
ESP32 DevKit V1 (ESP-WROOM-32)3.3 V logic / 5 V USB160Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly.
SHT31-D temperature + humidity sensor2.4–5.5 V1.5The built-in heater burns off condensation — essential for outdoor or greenhouse use.
BH1750 digital ambient light sensor2.4–3.6 V0.19Far more linear than an LDR — use it whenever you need real lux, not a relative value.
Capacitive soil-moisture sensor v2.03.3–5.5 V5Always pick capacitive over the cheap resistive fork — resistive probes corrode in weeks.
4-channel opto-isolated relay board5 V coil280All four coils energised draw ~280 mA — do not power from the MCU 5 V pin.
0.96″ SSD1306 OLED display3.3–5 V20Static images burn in — invert or scroll the screen periodically.
LM2596 adjustable buck converter module4.5–40 V8Set the output voltage with no load connected before wiring the board.
12 V 5 A SMPS adapter12 V5000Fuse the 12 V rail at ~1.5× your calculated draw.

Summed typical draw is 5474.69 mA. With a 1.5× design margin the supply should deliver at least 8300 mA continuously at the stated rail voltage.

Software Requirements & Development Environment

Reference toolchain: Arduino IDE 2.3.x with the ESP32 board package 3.x (or PlatformIO on VS Code). Anything newer normally works; anything older may lack the board definitions used here.

  • Install the Arduino IDE 2.3.x (or PlatformIO if you prefer a real editor and dependency locking).
  • Add https://espressif.github.io/arduino-esp32/package_esp32_index.json under File → Preferences → Additional Board Manager URLs, then install esp32 from the Boards Manager.
  • Set the correct port under Tools → Port. On Linux add yourself to the dialout group: sudo usermod -aG dialout $USER and log out and back in.
  • Open the Serial Monitor at 115200 baud — every sketch here logs its state there.
  • Keep File → Preferences → Show verbose output during: compilation switched on while you are debugging build errors.

Required libraries

LibraryWhy it is neededInstall
WiFi (ESP32 core) bundledStation/AP connection management for the ESP32.Bundled with the ESP32 Arduino core
PubSubClient 2.8Lightweight MQTT 3.1.1 client for constrained devices.Library Manager → "PubSubClient" by Nick O'Leary
ArduinoJson 7.xZero-allocation JSON serialisation and parsing.Library Manager → "ArduinoJson" by Benoit Blanchon
Adafruit BME280 2.2.xCompensation maths for the Bosch pressure/humidity/temperature sensor.Library Manager → "Adafruit BME280 Library"
Adafruit Unified Sensor 1.1.xCommon sensor event abstraction; a dependency of most Adafruit drivers.Library Manager → "Adafruit Unified Sensor"
BH1750 1.3.0Digital lux readings with selectable resolution modes.Library Manager → "BH1750" by Christopher Laws
Adafruit SSD1306 + GFX 2.5.xFramebuffer and text/graphics primitives for the OLED.Library Manager → "Adafruit SSD1306"
Preferences (NVS) bundledWear-levelled key/value storage in ESP32 flash for settings.Bundled with the ESP32 core
NTPClient / configTime bundledWall-clock time from an NTP server for timestamping.Bundled (`configTime()` on ESP32)

Block Diagram

The block diagram shows the functional decomposition of the system — what senses, what decides, what acts, and where the data ends up.

Smart Greenhouse Controller — system block diagramFunctional block diagram of the Smart Greenhouse Controller system. SenseSHT31 aspiratedT / RH / VPDPARlightSoil + CO₂root + airAssessDay/night regimetargetsPriority selectorworst deviationCoordinateCoupled-effect logicleast disruptionSafety overridesfrost/overheatActVent / heat / mistclimateLights → DLIphotoperiodall variableschosen fixcommands
Smart Greenhouse Controller — system block diagram

Circuit Diagram & Wiring

Every signal line in the build is shown below, followed by a pin-by-pin connection table you can work through with a multimeter in hand.

Smart Greenhouse Controller — wiring schematicConnection schematic showing which controller pin drives each peripheral. Sensors / InputsControllerActuators / OutputsESP32 DevKit V1(ESP-WROOM-32)3.3 V logic / 5 V USBSHT31 (aspirated)GPIO 21 / 22I²C at 0x44BH1750 / PAR sensorGPIO 21 / 22Shared I²C, 0x23Soil moistureGPIO 34AnalogueMH-Z19B CO₂ (optional)GPIO 16 / 17UARTRelay 1 → vent/exhaust fanGPIO 25VentilationRelay 2 → heaterGPIO 26HeatingRelay 3 →misting/humidifierGPIO 27HumidificationRelay 4 → grow lightsGPIO 14Supplementallighting
Smart Greenhouse Controller — wiring schematic
PeripheralPeripheral pinController pinSignal
SHT31 (aspirated)SDA / SCLGPIO 21 / 22I²C at 0x44
BH1750 / PAR sensorSDA / SCLGPIO 21 / 22Shared I²C, 0x23
Soil moistureAOUTGPIO 34Analogue
MH-Z19B CO₂ (optional)TX / RXGPIO 16 / 17UART
Relay 1 → vent/exhaust fanIN1GPIO 25Ventilation
Relay 2 → heaterIN2GPIO 26Heating
Relay 3 → misting/humidifierIN3GPIO 27Humidification
Relay 4 → grow lightsIN4GPIO 14Supplemental lighting

Wire one row at a time and tick it off — most "it does not work" reports trace back to a single swapped pair.

Wiring explanation

  • The temperature/humidity sensor must be aspirated and shielded. Mount it inside a small white radiation shield with a 40 mm fan drawing greenhouse air past it. An unshielded sensor in a greenhouse reads the sun on its own body — easily 10 °C high — and every control decision built on that is wrong.
  • Place the sensor at plant-canopy height, in the middle of the greenhouse, away from the door, the heater and direct sun. Where you measure determines what you control.
  • For lighting control, a true PAR (quantum) sensor is best. If using a BH1750, apply a crop-appropriate lux-to-PAR conversion — the factor differs between sunlight and grow-light spectra, so it is an approximation.
  • The vent/fan, heater, mister and lights are on separate relays. Size the relays and wiring for the actual actuator loads, and apply all the mains-safety practices from the smart-plug project for any mains actuators.
  • A misting pump near electronics is a real hazard — mount all electronics high and sealed, and route wiring so water cannot track back to it.
  • Give the CO₂ sensor (if fitted) a stable 5 V and disable its automatic baseline correction — a greenhouse rarely reaches outdoor CO₂, so ABC would drag its baseline wrong, exactly as in the air-quality project.

System Architecture

Read the stack from the bottom up: physical hardware, the firmware that drives it, the transport that moves data off the device, and the software a human actually looks at.

Smart Greenhouse Controller — architecture stackLayered architecture from hardware to user interface. Hardware layerESP32 DevKit V1 (ESP-WROOM-32) · SHT31-D temperature + humidity sensor ·BH1750 digital ambient light sensor · Capacitive soil-moisture sensor v2.0Driver layerwifi · pubsub · arduinojson · bmeApplication logicsampling loop · filtering · thresholds · state machineTransport layerWi-Fi + MQTT · TLS · retry and backoffPresentation layerdashboard · mobile notifications · historical charts
Smart Greenhouse Controller — architecture stack

Working Principle

The essential insight is that greenhouse variables are coupled through the actuators. Ventilation is the clearest example: opening a vent or running an exhaust fan lowers temperature, but it also lowers humidity (drier outside air comes in) and changes CO₂ (toward outdoor levels). Heating raises temperature and lowers relative humidity (warmer air holds more moisture, so the same absolute humidity is a lower relative humidity). Misting raises humidity but cools through evaporation. No actuator affects only one variable, so controlling them independently guarantees conflict.

The right humidity metric is vapour pressure deficit (VPD), not relative humidity. VPD is the difference between how much moisture the air could hold at its temperature and how much it actually holds — it is what drives transpiration (and thus water and nutrient uptake) and what governs the risk of fungal disease. The same relative humidity means very different things at different temperatures; VPD captures the physiologically relevant quantity. A well-run greenhouse targets a VPD band (roughly 0.8–1.2 kPa for many crops), and because VPD depends on both temperature and humidity, controlling it inherently couples the two.

The control strategy is priority-and-deadband coordination rather than parallel PID loops. Each variable has a target band with a deadband (a range within which no action is taken, preventing constant twitching). At each control step, the controller identifies the variable furthest outside its band, and chooses the actuator that best corrects it — but before acting, it checks whether that action would push another variable out of its band, and if so, either picks a different actuator or accepts a compromise. This mirrors how commercial greenhouse computers arbitrate between competing demands, and it is what produces stability instead of the oscillation that independent loops cause.

Light control works to a daily light integral (DLI) target. DLI is the total amount of photosynthetically active light delivered over a day, measured in moles of photons per square metre — and it, not instantaneous light level, is what governs plant growth. The controller integrates the measured PAR through the day and, if the accumulated DLI is falling short of the crop's target as the natural light wanes, runs supplemental lighting to make up the deficit. This is far more efficient than running lights on a fixed schedule regardless of the sunlight already received.

The day/night regime reflects plant physiology. Plants use a lower night temperature (the day-night temperature difference, "DIF", influences plant height and morphology), higher night humidity is tolerable, CO₂ enrichment only helps during light (photosynthesis needs light), and ventilation strategy differs. The controller switches target sets between day and night, using the light level (or a schedule) to determine which regime is active.

Finally, safety overrides sit above the coordination logic. Frost that would kill the crop and overheating that would cook it are emergencies that bypass the polite arbitration — the heater comes full on against frost, the vents open fully against overheat — regardless of what that does to humidity. And condensation control matters because a greenhouse that lets its surfaces reach dew point invites fungal disease; the controller ventilates to keep the air VPD above the condensation threshold.

The maths behind it

Vapour pressure deficit (VPD)

plainVapour pressure deficit (VPD)
Saturation vapour pressure at temperature T (°C):
  SVP = 0.6108 · exp(17.27·T / (T + 237.3))   kPa

Actual vapour pressure:
  AVP = SVP · RH/100

VPD = SVP − AVP = SVP · (1 − RH/100)

Example: T = 24 °C, RH = 65 %
  SVP = 0.6108 · exp(17.27·24/261.3) = 2.985 kPa
  VPD = 2.985 · (1 − 0.65) = 1.04 kPa   (good range)

Same RH at T = 30 °C gives VPD = 1.48 kPa (too dry) —
which is why VPD, not RH, is the right target.

Daily light integral (DLI)

plainDaily light integral (DLI)
PAR measured as photosynthetic photon flux density (PPFD),
in µmol·m⁻²·s⁻¹.

DLI = Σ PPFD · Δt / 1,000,000   mol·m⁻²·day⁻¹

Over a day, sampling every minute (Δt = 60 s):
  DLI = Σ (PPFD × 60) / 1e6

Crop targets (mol·m⁻²·day⁻¹):
  low-light (lettuce, herbs) : 12–17
  medium (tomatoes)          : 20–30
  high-light (peppers)       : 25–35

If accumulated DLI < target as sun wanes, run
supplemental lighting to close the gap.

Coordinated actuator selection

plainCoordinated actuator selection
For each variable v: error e_v = distance outside its band.
Priority = variable with the largest e_v (weighted by
crop-criticality and by how fast damage accrues).

For the chosen actuator a, predict its effect on every
variable: Δv = effect_matrix[a][v].

Apply a only if it does not push any variable from
inside its band to outside by more than a tolerance;
otherwise pick the next-best actuator or a partial step.

Safety overrides (frost, overheat) skip this arbitration.

Program Flowchart

The firmware is a single cooperative loop. Nothing blocks for long, so networking, sensing and the user interface all stay responsive.

Smart Greenhouse Controller — firmware flowchartControl flow through the main program loop. Read all sensors, compute VPDand DLIDay or night regime? SelecttargetsSafety: frost or overheat?override, act immediatelynormalWhich variable is furthest outof band?Choose the actuator that fixesit with least side-effectWould it worsen anothervariable past its band?pick alternative or compromiseapplyStep the actuator, holddeadbandManage lighting toward DLItarget, log
Smart Greenhouse Controller — firmware flowchart

Assembly Instructions

Build on a breadboard first and only commit to solder once the whole system has run for an hour without a fault.

Step-by-Step Implementation Guide

Work through these in order. Each step ends in something you can observe, so a failure is always localised to the step you just finished.

Complete Source Code

The listing below is complete and compiles as written — there are no elided sections. Read the annotations under each block before you upload it.

cppgreenhouse-controller.ino
/* ═══════════════════════════════════════════════════════════════
   Smart Greenhouse Controller — ESP32

   Coordinates temperature, humidity (via VPD), light (via DLI) and
   ventilation, respecting how each actuator affects multiple
   variables. Priority-and-deadband arbitration, not four fighting
   PID loops.
   ══════════════════════════════════════════════════════════════════ */

#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <Wire.h>
#include <Adafruit_SHT31.h>
#include <BH1750.h>
#include <Adafruit_SSD1306.h>
#include <Preferences.h>
#include <time.h>
#include <math.h>

#define WIFI_SSID "YOUR_WIFI"
#define WIFI_PASS "YOUR_PASSWORD"
#define MQTT_HOST "192.168.1.50"
#define DEVICE_ID "greenhouse-1"

#define R_VENT   25
#define R_HEAT   26
#define R_MIST   27
#define R_LIGHT  14

/* Crop targets — set for your crop. */
#define T_DAY     24.0f
#define T_NIGHT   18.0f
#define VPD_LOW    0.8f
#define VPD_HIGH   1.2f
#define DLI_TARGET 22.0f          // mol/m2/day
#define T_FROST     4.0f          // emergency heat below this
#define T_OVERHEAT 35.0f          // emergency vent above this
#define DEADBAND    1.0f          // °C hysteresis

Adafruit_SHT31   sht = Adafruit_SHT31();
BH1750           lux;
Adafruit_SSD1306 oled(128, 64, &Wire, -1);
WiFiClient       net;
PubSubClient     mqtt(net);
Preferences      prefs;

float tempC = 20, rh = 60, ppfd = 0, vpd = 0, dli = 0;
bool  isDay = true;
bool  ventOn = false, heatOn = false, mistOn = false, lightOn = false;
int   lastDay = -1;

/* ── derived quantities ─────────────────────────────────────── */
float computeVPD(float t, float relh) {
  float svp = 0.6108f * expf(17.27f * t / (t + 237.3f));
  return svp * (1.0f - relh / 100.0f);
}

float luxToPPFD(float luxVal) {
  // Approximate: sunlight ~0.0185 µmol/s per lux. Grow lights differ;
  // a true PAR sensor removes this uncertainty.
  return luxVal * 0.0185f;
}

/* ── actuators ──────────────────────────────────────────────── */
void set(int pin, bool &state, bool on) {
  if (state == on) return;
  state = on;
  digitalWrite(pin, on ? LOW : HIGH);          // active-low relays
}

/* ── coordinated control ────────────────────────────────────── */
void control() {
  float tTarget = isDay ? T_DAY : T_NIGHT;

  /* --- Safety overrides (bypass arbitration) --- */
  if (tempC < T_FROST) {
    set(R_HEAT, heatOn, true);
    set(R_VENT, ventOn, false);
    return;
  }
  if (tempC > T_OVERHEAT) {
    set(R_VENT, ventOn, true);
    set(R_HEAT, heatOn, false);
    set(R_MIST, mistOn, false);
    return;
  }

  /* --- Determine what is worst out of band --- */
  float tempErr = tempC - tTarget;             // + too hot, − too cold
  float vpdErr  = vpd < VPD_LOW  ? vpd - VPD_LOW
               : vpd > VPD_HIGH ? vpd - VPD_HIGH : 0;

  bool tempOut = fabsf(tempErr) > DEADBAND;
  bool vpdOut  = fabsf(vpdErr) > 0.1f;

  /* --- Temperature has priority (fast damage), with coupling awareness --- */
  if (tempOut && tempErr > 0) {
    // Too hot: ventilate. This also lowers humidity (raises VPD).
    // Only OK if VPD is not already too high.
    if (vpd < VPD_HIGH) { set(R_VENT, ventOn, true); set(R_HEAT, heatOn, false); }
    else {
      // Venting would over-dry. Mist while venting to hold VPD.
      set(R_VENT, ventOn, true); set(R_MIST, mistOn, true);
    }
    return;
  }
  if (tempOut && tempErr < 0) {
    // Too cold: heat. This lowers RH (raises VPD) — mist if that over-dries.
    set(R_HEAT, heatOn, true); set(R_VENT, ventOn, false);
    if (vpd > VPD_HIGH) set(R_MIST, mistOn, true); else set(R_MIST, mistOn, false);
    return;
  }

  /* --- Temperature in band: address VPD without disturbing temp much --- */
  if (vpdOut && vpdErr > 0) {
    // Too dry: mist (small cooling — acceptable inside the temp deadband).
    set(R_MIST, mistOn, true); set(R_VENT, ventOn, false);
  } else if (vpdOut && vpdErr < 0) {
    // Too humid (condensation/disease risk): ventilate gently.
    set(R_MIST, mistOn, false); set(R_VENT, ventOn, true);
  } else {
    set(R_MIST, mistOn, false);
    if (tempErr < DEADBAND * 0.5f) set(R_VENT, ventOn, false);
  }
}

/* ── lighting toward DLI ────────────────────────────────────── */
void lightControl(int hour) {
  // Supplement only when natural light is low AND we are behind on DLI.
  bool photoperiod = hour >= 6 && hour < 22;   // 16 h max
  bool behind = dli < DLI_TARGET * (hour - 6) / 16.0f;   // pro-rata target
  set(R_LIGHT, lightOn, photoperiod && ppfd < 200 && behind);
}

/* ── setup / loop ───────────────────────────────────────────── */
void setup() {
  Serial.begin(115200);
  int relays[] = { R_VENT, R_HEAT, R_MIST, R_LIGHT };
  for (int p : relays) { pinMode(p, OUTPUT); digitalWrite(p, HIGH); }
  analogSetPinAttenuation(34, ADC_11db);

  Wire.begin(21, 22);
  sht.begin(0x44);
  sht.setSampling ? 0 : 0;                       // (SHT31 has no forced mode API here)
  lux.begin(BH1750::CONTINUOUS_HIGH_RES_MODE);
  oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);

  prefs.begin("gh", false);
  dli = prefs.getFloat("dli", 0);
  lastDay = prefs.getInt("day", -1);

  WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASS);
  for (int i = 0; i < 40 && WiFi.status() != WL_CONNECTED; i++) delay(250);
  configTime(19800, 0, "pool.ntp.org");
  mqtt.setServer(MQTT_HOST, 1883);
  Serial.println("Greenhouse controller running");
}

void loop() {
  if (!mqtt.connected() && WiFi.status() == WL_CONNECTED) mqtt.connect(DEVICE_ID);
  mqtt.loop();

  static uint32_t last = 0;
  if (millis() - last < 5000) return;           // control every 5 s
  last = millis();

  tempC = sht.readTemperature();
  rh    = sht.readHumidity();
  float luxVal = lux.readLightLevel();
  ppfd  = luxToPPFD(luxVal);
  vpd   = computeVPD(tempC, rh);
  isDay = ppfd > 50;

  // Accumulate DLI (5 s interval).
  dli += ppfd * 5.0f / 1e6f;

  time_t now = time(nullptr); struct tm tmv; localtime_r(&now, &tmv);
  if (tmv.tm_yday != lastDay) {                 // new day: reset DLI
    lastDay = tmv.tm_yday;
    prefs.putInt("day", lastDay);
    Serial.printf("DLI yesterday: %.1f mol/m2\n", dli);
    dli = 0;
  }
  prefs.putFloat("dli", dli);

  control();
  lightControl(tmv.tm_hour);

  oled.clearDisplay(); oled.setTextColor(SSD1306_WHITE);
  oled.setTextSize(1);
  oled.setCursor(0, 0);  oled.printf("%.1fC  %.0f%%RH  %s", tempC, rh, isDay ? "day" : "night");
  oled.setCursor(0, 12); oled.printf("VPD %.2f kPa", vpd);
  oled.setCursor(0, 22); oled.printf("PPFD %.0f  DLI %.1f/%.0f", ppfd, dli, DLI_TARGET);
  oled.setCursor(0, 36); oled.printf("vent%s heat%s mist%s light%s",
    ventOn?"*":"-", heatOn?"*":"-", mistOn?"*":"-", lightOn?"*":"-");
  oled.display();

  JsonDocument d;
  d["temp"]=tempC; d["rh"]=rh; d["vpd"]=vpd; d["ppfd"]=ppfd; d["dli"]=dli;
  d["vent"]=ventOn; d["heat"]=heatOn; d["mist"]=mistOn; d["light"]=lightOn;
  char b[192]; size_t n = serializeJson(d, b, sizeof(b));
  mqtt.publish("farm/" DEVICE_ID "/state", (uint8_t*)b, n, true);
}
Safety overrides first, bypassing arbitrationFrost and overheat are emergencies. The heater comes full on against frost and the vents open fully against overheat, regardless of what that does to humidity. Fast crop damage takes priority over polite coordination.
Coupling-aware ventingWhen venting to cool, the controller checks whether that would push VPD too high (over-dry the air). If so, it runs the mister simultaneously to hold humidity while venting. This is the whole point — an actuator is chosen with awareness of its side effects, not in isolation.
Temperature priority within the deadbandTemperature is addressed first because plants are damaged faster by temperature extremes than by humidity ones. Only once temperature is inside its deadband does the controller act on VPD, and then it prefers actions that do not disturb temperature much.
computeVPD()The controller targets vapour pressure deficit, not relative humidity, because VPD is what governs transpiration and disease risk. The same RH means very different things at different temperatures, so controlling RH directly would be controlling the wrong quantity.
lightControl() pro-rata DLISupplemental lighting runs only when natural light is low AND the accumulated daily light integral is behind its pro-rata target for the time of day. This is far more efficient than a fixed schedule — on a sunny day the lights barely run; on a dull one they make up the deficit.
DEADBAND prevents twitchingThe one-degree deadband means the controller does not toggle actuators for tiny fluctuations. Without it, a greenhouse controller cycles its relays constantly, wearing them out and destabilising the environment.
isDay from PPFD, not just a clockThe day/night regime switches on actual light level, so a dull day or an early dusk correctly triggers night targets. Using light rather than only a schedule makes the regime track reality.

Configuration & Calibration

Configuration steps

  • Set the crop targets: T_DAY, T_NIGHT, the VPD band and DLI_TARGET. These are crop-specific — look up your crop's requirements rather than using generic values.
  • Aspirate and shield the temperature/humidity sensor. This is the single most important setup step — an unshielded greenhouse sensor is useless.
  • Calibrate the lux-to-PAR factor for your light sources, or fit a true PAR sensor. Grow-light spectra differ from sunlight, so one factor is an approximation.
  • Set the safety thresholds (T_FROST, T_OVERHEAT) conservatively for your crop — these prevent catastrophic loss and should trigger well before real damage.
  • Tune the deadband so actuators do not cycle rapidly. Larger greenhouses have more thermal mass and tolerate larger deadbands.

Calibration procedure

An uncalibrated sensor produces confident, precise, wrong numbers. Do this once per physical unit and record the constants.

  1. Verify sensor placement and aspiration

    Compare the aspirated sensor against a reference thermometer in shade. They should agree closely. Then check the sensor in sun without aspiration — the difference (often 5–10 °C) shows why aspiration is essential.

  2. Calibrate PAR

    If using a BH1750, compare against a borrowed quantum sensor under both sunlight and your grow lights, and derive separate conversion factors. If using a true PAR sensor, verify its calibration is current.

  3. Map the actuator effects

    Run each actuator alone and log how it changes temperature, humidity and VPD over ten minutes. This gives you the real coupling for your greenhouse, which you can use to tune the coordination logic.

  4. Tune the deadbands over a day

    Watch the actuator states over a full day. If any actuator cycles more than a few times an hour, widen its deadband. Stable conditions with infrequent switching is the goal.

Network Architecture & Connectivity

Smart Greenhouse Controller — network topologyPath taken by telemetry from field node to end user. Edge nodesGatewayCloudClientsGreenhouse controllerESP32Outdoor weatheroptionalWi-Fi 2.4 GHzRouteror LoRa for remoteMQTT 1883Local broker + Grafanaseasonal loggingGrafanaseason trendsPhone alertsfrost/overheat
Smart Greenhouse Controller — network topology

Dashboard setup

The most useful panel overlays temperature, VPD and actuator states over a day. You can see the coordination working — venting bringing temperature down, misting holding VPD as it does — and spot any fighting between actuators that needs tuning.

A seasonal DLI chart against target shows whether your supplemental lighting strategy is actually delivering the light the crop needs, which correlates directly with yield.

Security considerations

  • Use broker authentication — an open control topic lets anyone open your vents on a frosty night.
  • Keep frost and overheat protection local and independent of the network; a greenhouse can be killed in an hour by a failed connection during a cold snap.
  • Alert on communication loss so you know if the controller has gone silent.

Testing Procedure & Expected Output

Test from the bottom up. Confirm power, then each sensor in isolation, then the integrated loop — the first failing step tells you exactly where to look.

TestWhat you should see
Compare the aspirated sensor to a reference in sunClose agreement, unlike an unshielded sensor which reads far too high — confirming aspiration works.
Raise the temperature above targetThe vent opens; if VPD is already high, the mister runs alongside it — coordination, not just venting.
Lower the temperature below targetThe heater runs; if that over-dries the air, the mister supports VPD.
Drop humidity so VPD exceeds the band, temperature in rangeThe mister runs without the heater or vent fighting it.
Simulate frost (cool the sensor below T_FROST)The heater goes full on immediately, bypassing normal arbitration.
Simulate overheatThe vents open fully immediately, the heater and mister off.
Cover the light sensor during the dayThe grow lights come on only if the DLI is behind its pro-rata target — not automatically.
Watch actuator states over a dayStable conditions with infrequent switching and no oscillation between actuators.

Bench-test checklist. If a row fails, stop and fix it before moving on.

Expected output

With everything wired and the firmware uploaded, the Serial Monitor at 115200 baud should look similar to the trace below. Values will differ; the shape of the output should not.

A Grafana time-series dashboard
A dashboard. Overlaying conditions and actuator states over a day reveals whether the coordination is working or the actuators are fighting. Photograph sourced from Wikimedia Commons — Grafana dashboard.png. Reused under the licence stated on that page; please check it before republishing.

Troubleshooting: Common Errors & Fixes

Temperature reads far too high

Likely cause. Sensor not aspirated or shielded — reading the sun on its own body.

Fix. This is the most common and most damaging error. Fit a radiation shield and an aspiration fan drawing greenhouse air past the sensor. An unshielded sensor in a greenhouse is worse than useless because every decision built on it is wrong.

Actuators fight each other (vent and heater cycling)

Likely cause. Independent control logic, or deadbands too tight.

Fix. Ensure the coordination logic is checking coupled effects — venting to cool should not trigger the heater. Widen the deadbands. The whole design goal is to prevent this; if it happens, the arbitration is being bypassed somewhere.

Humidity control never settles

Likely cause. Controlling RH instead of VPD, or fighting the temperature loop.

Fix. Target VPD, not RH — RH changes with temperature even at constant moisture, so an RH loop chases the temperature loop. Address VPD only once temperature is in its band, with actions that do not disturb temperature.

Grow lights run all day even in sun

Likely cause. Lighting on a fixed schedule rather than a DLI target.

Fix. Drive lighting from the daily light integral: only supplement when natural PAR is low and the accumulated DLI is behind its pro-rata target. This is both more efficient and better for the plants than a fixed schedule.

The greenhouse froze despite the controller

Likely cause. Network dependency, or the heater actuator failed.

Fix. Frost protection must be entirely local and must not depend on the network. Verify the heater relay and actuator work, add a temperature alarm to your phone, and consider a mechanical thermostat as an independent backup for the frost case — a lost crop is expensive.

The sketch will not upload — "Failed to connect" or "avrdude: stk500_recv()"

Likely cause. The bootloader is not being reached: wrong port, wrong board, a serial monitor holding the port open, or a USB cable that only carries power.

Fix. Close every serial monitor, confirm Tools → Board and Port, and swap to a known data-capable USB cable. On an ESP32 hold BOOT while the IDE prints "Connecting…", then release. If a peripheral is wired to the UART pins (GPIO 1/3 on ESP32, D0/D1 on Uno) unplug it — it fights the programmer.

The board resets in a loop, or the serial monitor prints "Brownout detector was triggered"

Likely cause. The supply cannot deliver peak current. Wi-Fi transmit bursts, relay coils and servos all pull far more than their average draw.

Fix. Power peripherals from a separate regulated supply with a common ground rather than from the board 5 V pin. Add a 470–1000 µF electrolytic capacitor across the supply near the load, and use a real power adapter rather than a laptop USB port.

Serial monitor shows garbage characters

Likely cause. Baud rate mismatch between Serial.begin() and the monitor, or a floating/shared UART line.

Fix. Set the monitor to 115200 to match the sketch. If it still garbles, the crystal or the USB bridge is being confused by noise — shorten the cable and keep motor wiring away from the USB lead.

An I²C device is not detected

Likely cause. Wrong address, missing pull-ups, swapped SDA/SCL, or a bus too long for the pull-up value.

Fix. Run an I²C scanner sketch first — it should print the device address. Most breakout boards include 4.7 kΩ pull-ups, but if you have chained four of them the parallel resistance is too low; remove the pull-ups from all but one board. Keep the bus under 30 cm at 100 kHz.

Wi-Fi connects but MQTT never does (state -2)

Likely cause. Wrong broker address or port, a firewall in the way, or the broker requiring credentials the sketch is not sending.

Fix. Test from a laptop on the same network first: mosquitto_sub -h <broker> -t "#" -v. If that works, the problem is on the device — check the IP literal, port 1883 (or 8883 for TLS), and that client.setServer() runs before connect(). PubSubClient state codes are documented in its header.

Readings arrive for a while and then stop

Likely cause. The Wi-Fi or MQTT session dropped and the sketch never reconnects, or the broker dropped the client on keep-alive timeout.

Fix. Never assume the link stays up. Check WiFi.status() and client.connected() at the top of every loop and reconnect with exponential backoff. Add a watchdog so a wedged network stack reboots the device instead of going silent.

Performance Optimisation

  • Control on a slow cadence (5–30 s). Greenhouse thermal dynamics are minutes-scale; faster control just cycles actuators.
  • Use deadbands generously — greenhouses have thermal mass and tolerate wide bands, and infrequent switching extends relay and actuator life.
  • Persist the DLI accumulator so a reboot mid-day does not lose the day's light record and wrongly run the lights.
  • Replace every delay() with a millis() comparison — blocking delays are the single most common cause of dropped readings.
  • Sample sensors on a fixed cadence and publish on a slower one; you almost never need to transmit at the sampling rate.
  • Move networking into its own FreeRTOS task so a slow DNS lookup cannot stall the control loop.
  • Use uint8_t / uint16_t where the range allows; on an 8-bit AVR a 32-bit add costs four times as much.
  • Batch several samples into one MQTT publish. Radio time, not CPU time, dominates the energy budget.
  • Set the MQTT keep-alive to a value that matches your reporting interval so the broker does not churn reconnections.
  • For battery builds use deep sleep between samples: an ESP32 drops from ~160 mA awake to about 10 µA asleep, which is the difference between days and months of runtime.
  • Profile before optimising — print micros() deltas around each stage and fix the slowest one first.

Safety Precautions

  • Frost and overheat protection must be local, independent of any network, and reliable — a crop can be lost in an hour, and a network outage during a cold snap must not be able to cause that.
  • Apply all mains-safety practices from the smart-plug project for any mains-powered actuators (heaters, fans, pumps). A greenhouse is a wet environment, which makes mains safety more critical, not less.
  • Keep all electronics high and sealed against misting and condensation. Water tracking back to a mains actuator in a greenhouse is a serious hazard.
  • Fit an independent mechanical or thermal backup for the frost case if the crop is valuable — belt and braces.
  • Do not enrich CO₂ to levels that are unsafe for people entering the greenhouse; interlock enrichment with occupancy if used.
  • Mains voltage kills. Anything on the load side of the relay is at 230 V. Do not work on a powered circuit, and never leave exposed mains wiring on a bench where someone could touch it.
  • Keep at least 6 mm of creepage between the mains and low-voltage sides of any board you make, and never route mains tracks under the microcontroller.
  • Have a qualified electrician do the final installation into a consumer unit or wall fitting. In most jurisdictions this is a legal requirement, not a suggestion.
  • Fit an RCD/RCBO upstream and fuse the load appropriately for its rating.
  • Wear eye protection when soldering or cutting, and solder in a ventilated space — rosin flux fumes are a respiratory irritant.
  • Power the circuit through a bench supply with a current limit while you are testing. A 300 mA limit turns a wiring mistake into a beep instead of a dead board.
  • Disconnect power before changing any wiring. Hot-plugging a sensor onto a live bus is the fastest way to lose a controller.

Maintenance

  • Re-check every screw terminal and header after the first week — thermal cycling loosens connections that felt tight on day one.
  • Clean the sensing element on a schedule. Optical and electrochemical sensors foul, and a fouled sensor reports plausible nonsense rather than failing outright.
  • Keep the broker and dashboard containers patched, and rotate device credentials at least once a year.
  • Recalibrate at the interval given in the calibration section, and keep the constants in a text file next to the firmware — not only in flash.
  • Keep a short logbook of firmware versions and what changed. Six months later you will not remember why that constant is 1.083.

Future Improvements & Upgrades

A working v1 is a platform, not a finish line. These are the upgrades that add the most capability for the least rework.

  • Add predictive control using a weather forecast — pre-heat before a forecast frost, pre-ventilate before a hot afternoon, rather than only reacting.
  • Add CO₂ enrichment control that only enriches during light and at a level the crop can use, coordinated with ventilation (venting wastes enrichment).
  • Add irrigation integration (the drip irrigation project) so the root zone is managed together with the aerial environment.
  • Add multiple zones for greenhouses with different crops or a propagation area needing different conditions.
  • Add a disease-risk model from leaf wetness and VPD history, alerting when conditions favour fungal disease.
  • Design a proper PCB. Once the breadboard version has run for a month, moving to a two-layer board removes the intermittent-contact failures that dominate prototype faults.
  • Add over-the-air firmware updates so you never have to physically reach a deployed node again.
  • Add persistent local storage (microSD or the on-chip flash) so a network outage does not create a hole in your data.
  • Move configuration out of the source: a captive-portal setup page or a JSON config file makes the build reusable without a recompile.
  • Add a battery and solar option so the unit survives a power cut and can be sited away from a socket.
  • Write a small test harness that feeds synthetic sensor values through the decision logic, so you can validate thresholds without physically triggering the event.

Frequently Asked Questions

Why VPD instead of relative humidity?

Because VPD is what plants actually respond to and RH is not. Vapour pressure deficit is the difference between how much moisture the air can hold and how much it does — it drives transpiration (and thus water and nutrient uptake) and governs fungal disease risk. The same 70 % RH means a comfortable VPD at 20 °C and an over-dry one at 30 °C. Controlling RH directly controls the wrong quantity; controlling VPD controls the physiologically relevant one.

Why not just use four separate thermostats?

Because the greenhouse variables are coupled through the actuators, and four independent loops fight each other. Venting to cool also dries the air, so the humidity loop responds, which cools further, so the heating loop responds. The result is constant oscillation and unstable conditions. Coordinated control — deciding which variable is worst and choosing an actuator with awareness of its side effects — is what commercial greenhouse computers do and what produces stable conditions.

What is DLI and why does it matter more than light level?

Daily light integral is the total amount of photosynthetically useful light delivered over a whole day, in moles of photons per square metre. Plant growth depends on the total light received, not the instantaneous level — a plant does the same photosynthesis whether it gets moderate light all day or intense light for half of it, as long as the total is the same. Driving supplemental lighting to a DLI target is efficient (you only add what the day fell short by) and matches what the plant actually needs.

How important is the aspirated sensor really?

It is the difference between a working controller and a broken one. A temperature sensor sitting in a greenhouse in sunlight absorbs radiation and reads its own hot body — easily 5–10 °C above the actual air temperature. Every control decision built on that reading is wrong: the controller vents when it should not, and the plants cook while the display says everything is fine. Aspirating and shielding the sensor so it reads the moving air is non-negotiable.

Can this handle a large commercial greenhouse?

The control logic scales, but the actuators and the sensing do not directly — a large greenhouse needs multiple sensor points (conditions vary across a big space), multiple actuator zones, and much larger ventilation and heating. The coordination principle is exactly what commercial greenhouse computers use, so the approach is sound; the implementation would grow into a multi-zone, multi-sensor system, which is a natural extension.

What happens if the Wi-Fi drops?

Everything keeps running — control, safety overrides and lighting are all local. The network only adds remote monitoring and alerting. This is deliberate and essential: frost protection especially must never depend on a connection, because a cold snap during an outage could kill the crop. For a valuable crop, an independent mechanical frost backup is worth adding on top.

References & Learning Resources

These are the primary sources worth reading in full. Manufacturer datasheets always outrank forum posts when the two disagree.

  1. Runkle, "Daily Light Integral: A Useful Concept for Greenhouse Lighting"Michigan State University Extension
  2. Prenger & Ling, "Greenhouse Condensation Control" and VPD managementOhio State University Extension
  3. SHT31 humidity and temperature sensor — datasheetSensirion
  4. FAO, "Good Agricultural Practices for greenhouse vegetable production"FAO
  5. Körner & Challa, "Process-based humidity control regime for a greenhouse"Computers and Electronics in Agriculture, 2003
  6. Vapour pressure deficit — background and calculationWikipedia