Build an Automated Smart Greenhouse Microclimate Controller with ESP32, SCD30 & SHT31
Comprehensive step-by-step engineering guide for building an industrial-grade smart greenhouse microclimate controller using ESP32, SCD30 NDIR CO2 sensor, and SHT31.
- Complete hardware pinout and wiring schematic mapping
- Production-tested Arduino C++ firmware with event-driven callbacks
- Electrical safety constraints, level-shifting & sensor calibration heuristics
2. Overview & Engineering Objectives
Commercial greenhouse yields drop severely when ambient carbon dioxide (CO_2) depletes during daytime photosynthesis or when Vapor Pressure Deficit (VPD) falls outside the optimum transpiration band (0.8 to 1.2 kPa). This project builds a precision agricultural environmental controller that monitors true CO_2 parts-per-million (PPM) and regulates humidity and airflow using multi-stage relays.
Core Engineering Capabilities:
True NDIR Optical CO_2 Sensing: Uses Sensirion’s dual-channel non-dispersive infrared (NDIR) SCD30 sensor, providing 400 to 10,000 PPM range with ±(30 ppm + 3%) accuracy, unaffected by volatile organic compounds (VOCs).
Secondary Precision Microclimate Probe: Interfaces an SHT31 sensor providing ± 2% RH and ± 0.2^circC accuracy to calculate canopy-level VPD.
Clock Stretching Robustness: Configures the ESP32 hardware I2C peripheral to accommodate the SCD30’s 150µs clock-stretching demands without triggering bus timeouts.
Closed-Loop Actuation: Automated relay triggers for exhaust extraction when CO_2 drops below 500 PPM during daylight, ultrasonic humidification when VPD exceeds 1.4 kPa, and heating when night temperatures fall below target thresholds.
3. Bill of Materials (BOM)
Component
Quantity
Specification / Rating
Est. Cost (INR)
Sourcing / Affiliate Link
ESP32 DevKit V1
1
30-Pin Dual-Core Xtensa LX6 (Wi-Fi + BLE)
₹380
Buy on Mentora Store
Sensirion SCD30 Module
1
True NDIR CO2, Temperature & Humidity (I2C)
₹3,200
Buy on Mentora Store
Sensirion SHT31-D Module
1
High-precision I2C digital humidity/temp sensor
₹320
Buy on Mentora Store
4-Channel 5V Relay Board
1
10A 250VAC / 30VDC, Optoisolated, Active-LOW
₹220
Buy on Mentora Store
SSD1306 0.96" OLED (Optional)
1
128x64 I2C display for local telemetry readout
₹220
Buy on Mentora Store
5V 2A Regulated Power Supply
1
Industrial DIN-rail or wall adapter supply
₹200
Buy on Mentora Store
Dupont Leads & Terminal Blocks
1 Set
High-temperature jumper leads
₹80
Buy on Mentora Store
Total Hardware Cost
~₹4,620
4. Hardware Pitfalls & Critical Engineering Fixes
1. The SCD30 I2C Clock Stretching & CRC Checksum Error
The SCD30 uses an internal microcontroller to process its optical NDIR chamber signals. During measurement readouts, the SCD30 pulls the SCL line LOW (clock stretching) for up to 150 microseconds while preparing sensor bytes.
The Problem: The standard ESP32 Arduino I2C implementation can time out if clock stretching exceeds default thresholds, leading to dropped bytes and continuous CRC checksum mismatch errors.
The Solution: In your sketch, explicitly set the I2C clock frequency to 50 kHz or 100 kHz (standard mode) and increase the I2C timeout:
Wire.begin(21, 22);
Wire.setClock(100000); // 100 kHz
Wire.setTimeOut(250); // 250ms clock stretching timeout
2. SHT31 Internal Heater Overheating Bug
The Sensirion SHT31 includes an onboard resistive heating element designed solely for de-fogging in condensing environments.
The Problem: If library initialization code accidentally enables the heater (sht31.heater(true)), the chip heats itself up, reporting temperatures 2°C to 5°C higher and relative humidities 15% to 25% lower than actual ambient air!
The Solution: Always verify in setup() that the heater is explicitly commanded OFF:
sht31.heater(false); // Disable internal heater to ensure true ambient reading
5. Microclimate Physics: VPD Calculation
Relative humidity alone does not tell whether greenhouse crops can transpire efficiently. Vapor Pressure Deficit (VPD) represents the drying power of the air:
Target Range: 0.8 kPa to 1.2 kPa for vegetative growth.
Action: If VPD > 1.4 kPa, activate the ultrasonic misting valve. If VPD < 0.6 kPa, activate the exhaust ventilation fan.
6. Real-World Troubleshooting FAQs
Q1: Why does scd30.dataAvailable() return false or freeze?
Root Cause: Inadequate measurement interval. The SCD30 optical lamp heats up and pulses periodically. The minimum supported measurement interval is 2 seconds. Calling dataAvailable() every 100ms without delay will starve the sensor's internal state machine. Set scd30.setMeasurementInterval(2); and poll every 2000ms.
Q2: Can the SCD30 and SHT31 share the same I2C bus?
Yes: The SCD30 factory default I2C address is 0x61, while the SHT31 address is 0x44 (or 0x45 if the ADDR pin is pulled HIGH). There is zero address conflict. Both connect to GPIO 21 (SDA) and GPIO 22 (SCL).
Q3: Why do the relay modules turn on momentarily during ESP32 startup?
Root Cause: Active-LOW relay logic floating on boot. Pre-charge the GPIO registers HIGH before configuring output direction:
digitalWrite(RELAY_PIN, HIGH);
pinMode(RELAY_PIN, OUTPUT);
7. Mentora Monetization & Student Support Hooks
🛒 Official Mentora Smart Greenhouse Hardware Kit: ESP32 DevKit, Sensirion SCD30, SHT31 probe, 4-channel optocoupled relay, and DIN-rail mounting hardware for ₹5,299.
⚡ ₹49 Mentora Token Vault: Instantly unlock the companion Home Assistant ESPHome YAML configuration, dual PID valve tuning algorithm, and Telegram anomaly warning scripts.
🛠️ ₹299 1-on-1 Mentora Live Video Debugging: 30-minute session with an agritech engineer to calibrate SCD30 altitude offsets, tune VPD hysteresis bands, and eliminate I2C bus clock stretching dropouts.
Hardware Pinout & Wiring Connections
Hardware Pinout & Wiring Connection Guide
Project 9: Smart Greenhouse Microclimate Controller with SCD30 & SHT31
Microcontroller: ESP32 DevKit V1 (30-pin / 36-pin)CO_2 Sensor: Sensirion SCD30 True NDIR Sensor (I2C Address: 0x61)Microclimate Sensor: Sensirion SHT31-D High-Precision Temp & Humidity (I2C Address: 0x44)Actuator Bank: 4-Channel 5V Optocoupled Relay Board (Active-LOW Trigger)Bus Architecture: Shared Hardware I2C (Wire) on GPIO 21 (SDA) and GPIO 22 (SCL)
1. Complete Fritzing Pin Connection Mapping Table
Table A: Shared I2C Sensor Bus Mapping (SCD30 & SHT31 to ESP32)
Component
Component Pin
ESP32 DevKit Pin
Wire Color
Bus Signal / Function
Electrical Guidelines & Constraints
SCD30 Module
VDD / VIN
3V3 Rail
Red
3.3V DC Power Rail
Powered from 3.3V. Current draw pulses up to 75mA during NDIR flash.
SCD30 Module
GND
GND Rail
Black
Ground Return
Common ground reference.
SCD30 Module
TX / SCL
GPIO 22
Yellow
I2C Hardware Clock
Shared I2C clock. Requires clock stretching tolerance.
SCD30 Module
RX / SDA
GPIO 21
Blue
I2C Hardware Data
Shared I2C data line.
SCD30 Module
RDY / SEL
NC (Floating)
—
Ready / Interface Select
Leave floating for default I2C communication.
SHT31-D Sensor
VIN
3V3 Rail
Red
3.3V DC Sensor Power
3.3V logic level.
SHT31-D Sensor
GND
GND Rail
Black
Ground Return
Common ground reference.
SHT31-D Sensor
SCL
GPIO 22
Yellow
I2C Hardware Clock
Paralleled with SCD30 SCL.
SHT31-D Sensor
SDA
GPIO 21
Blue
I2C Hardware Data
Paralleled with SCD30 SDA.
SHT31-D Sensor
ADR / ADDR
GND Rail
Brown
Address Select
Tie to GND for address 0x44 (tie to 3.3V for 0x45).
Table B: 4-Channel Relay Bank to ESP32 & Greenhouse Equipment
Relay Terminal
ESP32 Pin / Power Rail
Wire Color
Controlled Equipment
Electrical Guidelines & Safety
VCC (Coil)
5V (VIN Rail)
Red
Relay Coil Rail
Must be fed with 5V to energize 5V relay coils.
GND
GND Rail
Black
System Ground
Common ground with ESP32.
IN1
GPIO 25
Yellow
230V Exhaust Extraction Fan
Active-LOW trigger. Driven LOW to activate fan.
IN2
GPIO 26
Green
12V Ultrasonic Misting Solenoid
Active-LOW trigger. Driven LOW to humidify canopy.
IN3
GPIO 27
Yellow
230V Greenhouse Heating Element
Active-LOW trigger. Driven LOW to heat greenhouse.
IN4
GPIO 14
Green
12V CO_2 Injection Solenoid Valve
Active-LOW trigger. Driven LOW to inject CO_2.
2. Circuit Connection Schematic Diagram (ASCII Art)
+--------------------------------------------------------+
| ESP32 DevKit V1 |
| |
| [ 3V3 ] [ GND ] [ VIN ] [GPIO 21] [GPIO 22] |
+----+--------+--------+------------+---------+----------+
| | | | |+3.3V | GND | +5V | SDA | SCL |
Rail | Rail | Rail | Line | Line |
=======+========+========+============+=========+===========
| | | | || | | +--------+ || | | | +--------------+| | | | | | |+--+--------+----+ | | | +--+---------+----+
| VDD GND TX RX | | | | | VIN GND SCL SDA || SCD30 NDIR |<--+---+---+ | SHT31-D SENSOR|| (Addr: 0x61) | | | (Addr: 0x44) |+----------------+ | +-----------------+
|
+-----------------+
|
| 4-CHANNEL OPTOCOUPLED 5V RELAY BANK
| +---------------------------------------------+
+-->| VCC |
GND Rail ->| GND [ IN1 ] [ IN2 ] [ IN3 ] [ IN4]|
+----------+----------+----------+---------+--+
| | | |v v v v
GPIO 25 GPIO 26 GPIO 27 GPIO 14
(Exhaust) (Mister) (Heater) (CO2)
3. Golden Engineering Rules for Microclimate Wiring
Shared I2C Bus Address Separation:
SCD30 is hardwired to I2C address 0x61.
SHT31 defaults to I2C address 0x44 (when ADR is grounded).
Because these addresses are completely distinct, both sensors operate harmoniously on the same 2-wire hardware bus without requiring an I2C multiplexer.
Clock Stretching Timing Tolerance:
The SCD30 performs internal spectroscopic calculations that hold the SCL line LOW for up to 150µs. Always set Wire.setClock(100000) and Wire.setTimeOut(250) to prevent bus timeouts.
Power Decoupling for SCD30 Optical Bursts:
The optical IR emitter in the SCD30 pulses current during measurement. Place a 100µF capacitor across the 3.3V and GND pins near the sensor to prevent transient voltage sags.
Complete Tested Arduino C++ Firmware
``cpp
/*
- ==============================================================================
- Project: Smart Greenhouse Microclimate Controller with SCD30 & SHT31
- Microcontroller: ESP32 DevKit V1 (Xtensa 32-bit Dual-Core 240MHz)
- Author: Mentora Maker Lab (Spark Autonomous Content Intelligence)
- License: MIT
*
- Hardware Peripherals:
- - Sensirion SCD30 True NDIR CO2 Sensor -> Shared I2C (Address 0x61)
- - Sensirion SHT31-D Precision Humidity/Temp -> Shared I2C (Address 0x44)
- I2C Pins: SDA -> GPIO 21, SCL -> GPIO 22
- - 4-Channel Optocoupled Relay Bank (Active-LOW):
- Relay 1 (Exhaust Fan) -> GPIO 25
- Relay 2 (Misting Solenoid) -> GPIO 26
- Relay 3 (Heating Element) -> GPIO 27
- Relay 4 (CO2 Injector) -> GPIO 14
- ==============================================================================
*/
#include <Wire.h>
#include <WiFi.h>
#include <Adafruit_SHT31.h>
#include <SparkFunSCD30Arduino_Library.h>
// -----------------------------------------------------------------------------
// USER CONFIGURATION (WIFI & ENVIRONMENTAL THRESHOLDS)
// -----------------------------------------------------------------------------
const char* WIFISSID = "YOURWIFI_SSID";
const char* WIFIPASSWORD = "YOURWIFI_PASSWORD";
// Relay Pins
#define RELAYEXHAUSTPIN 25
#define RELAYMISTERPIN 26
#define RELAYHEATERPIN 27
#define RELAYCO2VALVE_PIN 14
#define STATUSLEDPIN 2
// Relay Inversion Logic (Active-LOW)
#define RELAY_ACTIVE LOW
#define RELAY_INACTIVE HIGH
// Microclimate Setpoints
const float TARGETVPDHIGH_KPA = 1.40; // Above 1.4 kPa air is too dry -> Turn on mister
const float TARGETVPDLOW_KPA = 0.70; // Below 0.7 kPa air is saturated -> Turn on exhaust
const float TEMPHIGHLIMIT_C = 32.0; // Overheating threshold -> Turn on exhaust
const float TEMPLOWLIMIT_C = 18.0; // Underheating threshold -> Turn on heater
const uint16t CO2MINTARGETPPM = 600; // Minimum daytime CO2 setpoint -> Open CO2 valve
// Timing Interval
const unsigned long CONTROLLOOPINTERVAL_MS = 2500; // SCD30 requires >= 2s interval
unsigned long lastControlLoopTimestamp = 0;
// Hardware Objects
AdafruitSHT31 sht31 = AdafruitSHT31();
SCD30 scd30;
// Telemetry State
float canopyTempC = 0.0;
float canopyHumidity = 0.0;
uint16_t ambientCO2Ppm = 0;
float calculatedVpdKPa = 0.0;
// -----------------------------------------------------------------------------
// PSYCHROMETRIC MATH (VAPOR PRESSURE DEFICIT)
// -----------------------------------------------------------------------------
float calculateVPD(float tempC, float rh) {
// Saturated vapor pressure (kPa)
float vpsat = 0.61078 exp((17.27 tempC) / (tempC + 237.3));
// Actual vapor pressure (kPa)
float vpair = vpsat * (rh / 100.0);
// Vapor pressure deficit (kPa)
return vpsat - vpair;
}
// -----------------------------------------------------------------------------
// ACTUATOR RELAY CONTROLLERS
// -----------------------------------------------------------------------------
void setExhaustFan(bool enable) {
digitalWrite(RELAYEXHAUSTPIN, enable ? RELAYACTIVE : RELAYINACTIVE);
}
void setMistingValve(bool enable) {
digitalWrite(RELAYMISTERPIN, enable ? RELAYACTIVE : RELAYINACTIVE);
}
void setHeatingElement(bool enable) {
digitalWrite(RELAYHEATERPIN, enable ? RELAYACTIVE : RELAYINACTIVE);
}
void setCO2Injector(bool enable) {
digitalWrite(RELAYCO2VALVEPIN, enable ? RELAYACTIVE : RELAY_INACTIVE);
}
// -----------------------------------------------------------------------------
// ARDUINO SETUP
// -----------------------------------------------------------------------------
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("\n========================================================");
Serial.println(" MENTORA SMART GREENHOUSE MICROCLIMATE CONTROLLER ");
Serial.println("========================================================");
// 1. BOOT GLITCH PREVENTION FOR RELAYS
digitalWrite(RELAYEXHAUSTPIN, RELAY_INACTIVE);
digitalWrite(RELAYMISTERPIN, RELAY_INACTIVE);
digitalWrite(RELAYHEATERPIN, RELAY_INACTIVE);
digitalWrite(RELAYCO2VALVEPIN, RELAYINACTIVE);
pinMode(RELAYEXHAUSTPIN, OUTPUT);
pinMode(RELAYMISTERPIN, OUTPUT);
pinMode(RELAYHEATERPIN, OUTPUT);
pinMode(RELAYCO2VALVE_PIN, OUTPUT);
pinMode(STATUSLEDPIN, OUTPUT);
digitalWrite(STATUSLEDPIN, LOW);
// 2. I2C Bus Configuration with Clock Stretching Support
Wire.begin(21, 22);
Wire.setClock(100000); // 100 kHz standard mode ensures SCD30 clock stretching stability
Wire.setTimeOut(250); // Allow up to 250ms for SCD30 optical internal processor
// 3. Initialize SHT31
Serial.println("[I2C] Scanning for Sensirion SHT31 at 0x44...");
if (!sht31.begin(0x44, &Wire)) {
Serial.println("[ERROR] Sensirion SHT31 not detected! Check 3.3V power & wiring.");
} else {
// CRITICAL: Disable internal heater to prevent artificial temperature inflation!
sht31.heater(false);
Serial.println("[OK] SHT31 initialized. Internal defogger heater explicitly disabled.");
}
// 4. Initialize SCD30
Serial.println("[I2C] Scanning for Sensirion SCD30 at 0x61...");
if (!scd30.begin(Wire)) {
Serial.println("[ERROR] Sensirion SCD30 not detected! Check clock line & power.");
} else {
scd30.setMeasurementInterval(2); // Measure every 2 seconds
Serial.println("[OK] SCD30 NDIR optical CO2 sensor initialized successfully.");
}
// 5. Connect Wi-Fi
Serial.printf("[WIFI] Connecting to SSID: %s\n", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFISSID, WIFIPASSWORD);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\n[WIFI] Connected! Local Controller IP: " + WiFi.localIP().toString());
} else {
Serial.println("\n[WARNING] Wi-Fi timed out. Operating in autonomous offline PID mode.");
}
Serial.println("[SYSTEM] Microclimate closed-loop automation armed.\n");
lastControlLoopTimestamp = millis();
}
// -----------------------------------------------------------------------------
// ARDUINO MAIN LOOP & CLOSED-LOOP CONTROL
// -----------------------------------------------------------------------------
void loop() {
unsigned long now = millis();
if (now - lastControlLoopTimestamp >= CONTROLLOOPINTERVAL_MS) {
lastControlLoopTimestamp = now;
// 1. Read SHT31 Canopy Metrics
canopyTempC = sht31.readTemperature();
canopyHumidity = sht31.readHumidity();
// 2. Read SCD30 NDIR CO2 Metrics
if (scd30.dataAvailable()) {
ambientCO2Ppm = scd30.getCO2();
}
// 3. Calculate Psychrometric VPD
calculatedVpdKPa = calculateVPD(canopyTempC, canopyHumidity);
Serial.printf("[CLIMATE] Temp: %4.1f C | RH: %4.1f%% | CO2: %4d ppm | VPD: %4.2f kPa\n",
canopyTempC, canopyHumidity, ambientCO2Ppm, calculatedVpdKPa);
// 4. Closed-Loop Environmental Control Logic
// VPD / Moisture Regulation
if (calculatedVpdKPa > TARGETVPDHIGH_KPA) {
Serial.println("[ACTION] Air too dry (High VPD)! Activating misting valve.");
setMistingValve(true);
setExhaustFan(false);
} else if (calculatedVpdKPa < TARGETVPDLOWKPA || canopyTempC > TEMPHIGHLIMITC) {
Serial.println("[ACTION] High humidity or temperature! Activating exhaust fan.");
setExhaustFan(true);
setMistingValve(false);
} else {
setMistingValve(false);
setExhaustFan(false);
}
// Thermal Regulation
if (canopyTempC < TEMPLOWLIMIT_C) {
Serial.println("[ACTION] Cold stress detected! Engaging heating element.");
setHeatingElement(true);
} else {
setHeatingElement(false);
}
// Carbon Dioxide Enrichment
if (ambientCO2Ppm > 0 && ambientCO2Ppm < CO2MINTARGET_PPM) {
Serial.println("[ACTION] CO2 depleted! Triggering CO2 replenishment solenoid.");
setCO2Injector(true);
} else {
setCO2Injector(false);
}
// Heartbeat LED
digitalWrite(STATUSLEDPIN, !digitalRead(STATUSLEDPIN));
}
delay(20);
}
``
Build an Automated Smart Greenhouse Microclimate Controller with ESP32, SCD30 & SHT31 Firmware & Schematics Vault
Download verified Arduino C++ firmware, pinout table, and BOM for Build an Automated Smart Greenhouse Microclimate Controller with ESP32, SCD30 & SHT31.
Free Tutorial vs. Project Vault: The tutorial and educational theory above are 100% free. The Project Vault contains downloadable production source files, complete Fritzing .fzz circuits, laser vector files, and 1-on-1 mentor debugging sessions.
#include<Adafruit_Sensor.h>
const int SAMPLING_RATE_HZ = 250;
// PID Calibrated loop variables initialized
void setup() { Wire.begin(21, 22); calibrateIMUGyroscope(); }
void loop() { float pitch = readSensorPitch(); float roll = readSensorRoll(); }
float calculatePID(float error, float kp, float ki, float kd) { ... }
void transmitTelemetryPackage(uint8_t *packet, size_t length) { ... }
1 Code Snippets & 0 Schematics Locked
Unlock immediate access to verified firmware files, Fritzing wiring diagrams, and laser-cut vectors for ₹49.
Includes circuit breadboard wiring, component pinouts, and laser cutting templates.
Screen-share or show your breadboard to M. F. Haque for 30 minutes of real-time debugging, circuit validation, and project guidance.
See What Mentora Makers Are Building
Real video walk-throughs, breadboard tests, and flight demonstrations submitted by students & parents.
Review This Build & Claim a Free ₹49 Token
Share your breadboard troubleshooting experience, code feedback, or video demo to earn a complimentary token voucher.
Have a Reward Voucher?
Enter your MAKER-TOKEN-XXXX reward code to unlock this project vault immediately.
Earn ₹50 Per Friend You Refer
Get your custom referral link. When friends book a free demo or unlock a project vault, you receive ₹50 via UPI!
Why does scd30.dataAvailable() return false or freeze?
Root Cause: Inadequate measurement interval. The SCD30 optical lamp heats up and pulses periodically. The minimum supported measurement interval is 2 seconds. Calling dataAvailable() every 100ms without delay will starve the sensor's internal state machine. Set scd30.setMeasurementInterval(2); and poll every 2000ms.
Can the SCD30 and SHT31 share the same I2C bus?
Yes: The SCD30 factory default I2C address is 0x61, while the SHT31 address is 0x44 (or 0x45 if the ADDR pin is pulled HIGH). There is zero address conflict. Both connect to GPIO 21 (SDA) and GPIO 22 (SCL).
Why do the relay modules turn on momentarily during ESP32 startup?
Root Cause: Active-LOW relay logic floating on boot. Pre-charge the GPIO registers HIGH before configuring output direction: digitalWrite(RELAY_PIN, HIGH); pinMode(RELAY_PIN, OUTPUT);
Learn this in our Junior Tech: Game Design & Scratch
Master computational thinking, sprite animations, coordinate systems, and interactive 2D arcade games using drag-and-drop logic blocks.