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.
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
| Setting | How it is used |
|---|---|
| Hobby and market-garden greenhouses | Stable conditions and extended seasons with cheap, coordinated automation. |
| Propagation and seedling raising | Tight environmental control is what germination and young plants need. |
| Research and controlled-environment growing | Repeatable conditions and full logging for experiments. |
| Polytunnels and high tunnels | Even passive structures benefit hugely from coordinated ventilation control. |
| Educational growing | A complete, legible example of coupled-system control with a living result. |
| Specialty crops | Orchids, 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
| Attribute | Value |
|---|---|
| Difficulty level | Intermediate |
| Estimated completion time | 16–24 hours |
| Indicative build cost | ₹4,600 – ₹8,500 depending on actuators |
| Primary discipline | Agriculture |
| Reference platform | ESP32 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.
| Component | Key specification | Qty | Approx. 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 DAC | 1 | ₹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 heater | 1 | ₹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 eye | 1 | ₹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 probe | 1 | ₹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 channel | 1 | ₹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²C | 1 | ₹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 % efficiency | 1 | ₹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 barrel | 1 | ₹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 grid | 1 | ₹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 lugs | 1 | ₹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 shield | 1 | ₹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 approximation | 1 | ₹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 relay | 1 | ₹2,500 |
| MH-Z19B CO₂ sensor (optional) | NDIR, for CO₂ enrichment control | 1 | ₹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
| Part | Specification | Supply | Interface | Reference |
|---|---|---|---|---|
| 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 DAC | 3.3 V logic / 5 V USB | UART, SPI, I²C, I²S, CAN, PWM | Datasheet |
| SHT31-D temperature + humidity sensor | −40 to +125 °C ±0.2 °C, 0–100 %RH ±2 %, 1 Hz–10 Hz, on-chip heater | 2.4–5.5 V | I²C (0x44/0x45) | Datasheet |
| BH1750 digital ambient light sensor | 1–65535 lx, 16-bit, ±20 %, spectral response close to the human eye | 2.4–3.6 V | I²C (0x23/0x5C) | Datasheet |
| Capacitive soil-moisture sensor v2.0 | Corrosion-free capacitive sensing, 0–3 V analogue swing, 55 × 20 mm probe | 3.3–5.5 V | Analogue | Datasheet |
| 4-channel opto-isolated relay board | 4 × SPDT, 10 A @ 250 VAC, active-low inputs, LED per channel | 5 V coil | 4× digital | Datasheet |
| 0.96″ SSD1306 OLED display | 128 × 64 monochrome, 1.3–3.3 V logic, 100 kHz–400 kHz I²C | 3.3–5 V | I²C (0x3C) | Datasheet |
| LM2596 adjustable buck converter module | 4.5–40 V in, 1.25–37 V out, 2 A (3 A peak), ~92 % efficiency | 4.5–40 V | Screw terminals + trimmer | Datasheet |
| 12 V 5 A SMPS adapter | 100–240 VAC in, 12 V ±5 %, 5 A, 60 W, DC 5.5 × 2.1 mm barrel | 12 V | DC barrel | Datasheet |
| Double-sided perfboard 7 × 9 cm + headers | FR-4, 0.1″ pitch, plated through-holes, 24 × 18 grid | — | — | Datasheet |
| IP65 ABS junction enclosure 158 × 90 × 60 mm | IP65, ABS, −20 to +80 °C, transparent lid, wall-mount lugs | — | — | Datasheet |
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.
| Load | Supply rail | Typical current (mA) | Notes |
|---|---|---|---|
| ESP32 DevKit V1 (ESP-WROOM-32) | 3.3 V logic / 5 V USB | 160 | Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly. |
| SHT31-D temperature + humidity sensor | 2.4–5.5 V | 1.5 | The built-in heater burns off condensation — essential for outdoor or greenhouse use. |
| BH1750 digital ambient light sensor | 2.4–3.6 V | 0.19 | Far more linear than an LDR — use it whenever you need real lux, not a relative value. |
| Capacitive soil-moisture sensor v2.0 | 3.3–5.5 V | 5 | Always pick capacitive over the cheap resistive fork — resistive probes corrode in weeks. |
| 4-channel opto-isolated relay board | 5 V coil | 280 | All four coils energised draw ~280 mA — do not power from the MCU 5 V pin. |
| 0.96″ SSD1306 OLED display | 3.3–5 V | 20 | Static images burn in — invert or scroll the screen periodically. |
| LM2596 adjustable buck converter module | 4.5–40 V | 8 | Set the output voltage with no load connected before wiring the board. |
| 12 V 5 A SMPS adapter | 12 V | 5000 | Fuse 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.jsonunder 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
dialoutgroup:sudo usermod -aG dialout $USERand 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
| Library | Why it is needed | Install |
|---|---|---|
| WiFi (ESP32 core) bundled | Station/AP connection management for the ESP32. | Bundled with the ESP32 Arduino core |
| PubSubClient 2.8 | Lightweight MQTT 3.1.1 client for constrained devices. | Library Manager → "PubSubClient" by Nick O'Leary |
| ArduinoJson 7.x | Zero-allocation JSON serialisation and parsing. | Library Manager → "ArduinoJson" by Benoit Blanchon |
| Adafruit BME280 2.2.x | Compensation maths for the Bosch pressure/humidity/temperature sensor. | Library Manager → "Adafruit BME280 Library" |
| Adafruit Unified Sensor 1.1.x | Common sensor event abstraction; a dependency of most Adafruit drivers. | Library Manager → "Adafruit Unified Sensor" |
| BH1750 1.3.0 | Digital lux readings with selectable resolution modes. | Library Manager → "BH1750" by Christopher Laws |
| Adafruit SSD1306 + GFX 2.5.x | Framebuffer and text/graphics primitives for the OLED. | Library Manager → "Adafruit SSD1306" |
| Preferences (NVS) bundled | Wear-levelled key/value storage in ESP32 flash for settings. | Bundled with the ESP32 core |
| NTPClient / configTime bundled | Wall-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.
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.
| Peripheral | Peripheral pin | Controller pin | Signal |
|---|---|---|---|
| SHT31 (aspirated) | SDA / SCL | GPIO 21 / 22 | I²C at 0x44 |
| BH1750 / PAR sensor | SDA / SCL | GPIO 21 / 22 | Shared I²C, 0x23 |
| Soil moisture | AOUT | GPIO 34 | Analogue |
| MH-Z19B CO₂ (optional) | TX / RX | GPIO 16 / 17 | UART |
| Relay 1 → vent/exhaust fan | IN1 | GPIO 25 | Ventilation |
| Relay 2 → heater | IN2 | GPIO 26 | Heating |
| Relay 3 → misting/humidifier | IN3 | GPIO 27 | Humidification |
| Relay 4 → grow lights | IN4 | GPIO 14 | Supplemental 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.
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)
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)
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
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.
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.
/* ═══════════════════════════════════════════════════════════════
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);
}
Configuration & Calibration
Configuration steps
- Set the crop targets:
T_DAY,T_NIGHT, the VPD band andDLI_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.
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.
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.
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.
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
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.
| Test | What you should see |
|---|---|
| Compare the aspirated sensor to a reference in sun | Close agreement, unlike an unshielded sensor which reads far too high — confirming aspiration works. |
| Raise the temperature above target | The vent opens; if VPD is already high, the mister runs alongside it — coordination, not just venting. |
| Lower the temperature below target | The heater runs; if that over-dries the air, the mister supports VPD. |
| Drop humidity so VPD exceeds the band, temperature in range | The 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 overheat | The vents open fully immediately, the heater and mister off. |
| Cover the light sensor during the day | The grow lights come on only if the DLI is behind its pro-rata target — not automatically. |
| Watch actuator states over a day | Stable 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.
Troubleshooting: Common Errors & Fixes
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 amillis()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_twhere 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
References & Learning Resources
These are the primary sources worth reading in full. Manufacturer datasheets always outrank forum posts when the two disagree.
- Runkle, "Daily Light Integral: A Useful Concept for Greenhouse Lighting"Michigan State University Extension
- Prenger & Ling, "Greenhouse Condensation Control" and VPD managementOhio State University Extension
- SHT31 humidity and temperature sensor — datasheetSensirion
- FAO, "Good Agricultural Practices for greenhouse vegetable production"FAO
- Körner & Challa, "Process-based humidity control regime for a greenhouse"Computers and Electronics in Agriculture, 2003
- Vapour pressure deficit — background and calculationWikipedia