Community project

Seed Drying Box Controller

ESP32
Photo of Seed Drying Box Controller
Generated with AI

ricky.marek

Published October 25, 2026

This seed drying box controller automatically manages humidity levels by modulating a 12V computer fan based on real-time sensor readings. The ESP32 microcontroller reads temperature and humidity from an SHT31-D sensor, displays the values on a small OLED screen, and adjusts fan speed via PWM control through a logic-level MOSFET to keep seeds drying at an optimal pace.

This guide provides a complete parts list, wiring diagram, and step-by-step assembly instructions for building the controller. You'll also get the Arduino firmware that implements proportional fan control—ramping speed from 30% at 45% humidity up to full speed at 65% humidity—along with details on configuring the 12V power supply and setting up the I2C sensor bus.

Wiring diagram

Wiring diagram for Seed Drying Box Controller

Gather all the parts

QtyComponent
1

Adafruit Sensirion SHT31-D Temperature & Humidity Breakout

I2C temperature and humidity sensor breakout (SHT31-D) with ±2% RH and ±0.3°C accuracy, PTFE-filtered housing, and 3.3 V/5 V compatibility. Good choice when DHT-series accuracy is insufficient.

1

0.96" OLED Display 128x64 I2C (Blue)

Monochrome 0.96 inch OLED display with 128 x 64 blue pixels, driven by an SSD1306 controller over I2C (address 0x3C). It runs from 3.3 to 5 V and connects through a four-pin VCC, GND, SCL, SDA header.

1

12 V 4-pin PWM computer fan

A four-wire 12 V brushless computer fan whose separate PWM wire sets its speed.

1

12V Barrel-Jack Adapter

12 V / 2 A wall adapter with a 5.5 mm / 2.1 mm barrel jack. Used to power motor drivers, LED strips, or boards that need a higher rail.

1

24v Buck Converter

LM2596-based adjustable step-down buck converter module. Commonly used to regulate a higher battery rail, such as a 2S 18650 pack, down to 5V for Arduino logic. It is a regulator, not a charger or battery protection board.

1

IRLZ44N Logic-Level N-Channel MOSFET

Logic-level N-channel MOSFET commonly used as a low-side switch for DC loads from 3.3V or 5V microcontroller GPIO. Use a gate resistor, a gate pulldown, and a flyback diode for inductive loads.

1

100 Ω gate resistor

100 Ω

A small resistor that protects the ESP32 output and controls fast switching into the MOSFET gate.

1

10 kΩ gate pulldown resistor

10 kΩ

A resistor that keeps the MOSFET switched off while the ESP32 is starting or disconnected.

Assemble it in 6 steps

1. Set the 5 V converter before connecting the ESP32

With the ESP32 unplugged, connect supply_12v positive to buck_5v VIN+ and supply_12v negative to buck_5v VIN-. Plug in the adapter and use a multimeter while turning the converter’s small adjustment screw until VOUT+ to VOUT- reads 5.0 V. Disconnect the adapter after setting it.

  • Turn the adjustment screw in very small amounts and re-measure each time.
  • Do not connect the ESP32 until the converter measures 5.0 V — a higher setting can damage the board.

2. Power the ESP32 and fan from the 12 V supply

Connect buck_5v VOUT+ to the ESP32 pin labelled 5V or VIN (power), and buck_5v VOUT- to an ESP32 GND pin (ground). Connect fan_1 +12V to supply_12v +12V (power), and fan_1 GND to supply_12v GND (ground).

  • On many computer fans the power wires are yellow for +12 V and black for GND, but follow the labels on your own fan.
  • Keep the 12 V fan positive wire away from the ESP32 3V3 pins — 12 V on a 3.3 V pin can permanently damage the board.
  • All grounds must meet at the shared GND connection or the fan speed control will not work reliably.

3. Wire the IRLZ44N fan-control switch

Hold fan_pwm_mosfet with its labelled flat face toward you and its three legs pointing down: on the usual TO-220 IRLZ44N, the left leg is GATE, middle is DRAIN, and right is SOURCE; the metal tab is also DRAIN. Connect SOURCE to supply_12v GND (ground). Connect DRAIN to fan_1 PWM (signal). Connect GPIO25 to one end of gate_resistor (signal) and its other end to GATE. Connect gate_pulldown between that same GATE connection and GND (off-at-startup safety). Leave the fan TACH wire unconnected and insulated.

  • The PWM wire is commonly blue and the unused TACH wire commonly green, but use the fan’s label or datasheet rather than color alone.
  • The IRLZ44N metal tab is electrically connected to DRAIN, so do not let it touch ground or a metal case unless it is insulated.
  • Do not connect the fan PWM wire straight to GPIO25 — this MOSFET makes the safe pull-to-ground control signal the fan expects.
  • Do not short the unused TACH wire to power or ground; cover its bare end with tape or heat-shrink.

4. Wire the humidity sensor

Connect sht31_1 VCC to ESP32 3V3 (power), GND to ESP32 GND (ground), SDA to GPIO21 (data), and SCL to GPIO22 (clock). Connect the sensor’s ADR pin to GND; that makes it answer at the address used by this project.

  • Keep the sensor away from the fan motor and mount it where air from the seed tray passes it, not where it touches wet seeds.
  • Make sure VCC and GND are not swapped — swapped power can damage the sensor.

5. Wire the small display

Connect oled_1 VCC to ESP32 3V3 (power), GND to ESP32 GND (ground), SDA to GPIO21 (data), and SCL to GPIO22 (clock). The screen and sensor intentionally share the two GPIO21 and GPIO22 wires.

  • Read the labels on the OLED header; look-alike modules can put the four pins in a different physical order.
  • Make sure VCC and GND are not swapped — swapped power can damage the screen.

6. Mount the parts inside the drying box

Mount fan_1 so it moves air through the seed tray rather than directly blasting the sensor. Place sht31_1 in the moving air but shield it from drips and direct wet-seed contact. Keep the ESP32, buck converter, and all bare connections outside the humid air path or in a ventilated electronics compartment.

  • Use cable ties or screw terminals so vibration from the fan cannot pull out a wire.
  • Keep mains-powered adapter wiring outside the box; only the low-voltage 12 V output should enter the enclosure.

Review all connections

1. Connections between "sht31_1" and "ESP32"

Functionsht31_1ESP32
powerVCC → 3.3 V sensor power3V3
groundGND → shared groundGND
i2cSDA → shared I2C dataGPIO 21
i2cSCL → shared I2C clockGPIO 22
groundADR → sets SHT31 I2C address to 0x44GND

2. Connections between "oled_1" and "ESP32"

Functionoled_1ESP32
powerVCC → 3.3 V display power3V3
groundGND → shared groundGND
i2cSDA → shared I2C dataGPIO 21
i2cSCL → shared I2C clockGPIO 22

3. Connections between "fan_1" and "ESP32"

Functionfan_1ESP32
power+12V → 12V Barrel-Jack Adapter +12VEXT
groundGND → 12V Barrel-Jack Adapter GNDEXT

4. Connections between "supply_12v" and "ESP32"

Functionsupply_12vESP32
power+12V → 24v Buck Converter VIN+EXT
groundGND → 24v Buck Converter VIN-EXT

5. Connections between "buck_5v" and "ESP32"

Functionbuck_5vESP32
powerVOUT+ → set module to 5.0 V before connecting to ESP32 5V/VIN5V
groundVOUT- → ESP32 shared groundGND

6. Connections between "gate_resistor" and "ESP32"

Functiongate_resistorESP32
digitalA → 25 kHz fan PWM control signalGPIO 25
digitalB → IRLZ44N Logic-Level N-Channel MOSFET GATEEXT

7. Connections between "gate_pulldown" and "ESP32"

Functiongate_pulldownESP32
digitalA → IRLZ44N Logic-Level N-Channel MOSFET GATEEXT
groundB → shared groundGND

8. Connections between "fan_pwm_mosfet" and "ESP32"

Functionfan_pwm_mosfetESP32
dataDRAIN → 12 V 4-pin PWM computer fan PWMEXT
groundSOURCE → 12V Barrel-Jack Adapter GNDEXT

Deploy the firmware

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_SHT31.h>

#define I2C_SDA_PIN 21
#define I2C_SCL_PIN 22
#define FAN_PWM_PIN 25

constexpr uint8_t SCREEN_WIDTH = 128;
constexpr uint8_t SCREEN_HEIGHT = 64;
constexpr uint8_t FAN_PWM_CHANNEL = 0;
constexpr uint16_t FAN_PWM_FREQUENCY = 25000;
constexpr uint8_t FAN_PWM_RESOLUTION = 8;
constexpr uint8_t FAN_MIN_PERCENT = 30;
constexpr float DRY_TARGET_RH = 45.0f;
constexpr float HIGH_RH = 65.0f;
constexpr unsigned long SAMPLE_INTERVAL_MS = 2000;

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
Adafruit_SHT31 sht31 = Adafruit_SHT31();

float shownTemperature = NAN;
float shownHumidity = NAN;
int shownFanPercent = -1;
bool shownSensorFault = false;
unsigned long lastSampleMs = 0;

int fanPercentForHumidity(float humidity) {
  if (humidity <= DRY_TARGET_RH) {
    return FAN_MIN_PERCENT;
  }
  if (humidity >= HIGH_RH) {
    return 100;
  }
  float fraction = (humidity - DRY_TARGET_RH) / (HIGH_RH - DRY_TARGET_RH);
  return FAN_MIN_PERCENT + (int)((100 - FAN_MIN_PERCENT) * fraction + 0.5f);
}

void setFanPercent(int percent) {
  percent = constrain(percent, FAN_MIN_PERCENT, 100);
  uint32_t maxDuty = (1UL << FAN_PWM_RESOLUTION) - 1;
  // A 4-pin fan internally pulls PWM high. The MOSFET makes a GPIO HIGH pull it LOW,
  // so invert the ESP32 duty cycle to give the fan the requested high-time percentage.
  uint32_t mosfetDuty = (maxDuty * (100 - percent)) / 100;
  ledcWrite(FAN_PWM_PIN, mosfetDuty);
}

void drawReadings(float temperature, float humidity, int fanPercent) {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println(F("SEED DRYER"));
  display.drawFastHLine(0, 11, SCREEN_WIDTH, SSD1306_WHITE);

  display.setTextSize(2);
  display.setCursor(0, 18);
  display.print(humidity, 1);
  display.println(F("%RH"));

  display.setTextSize(1);
  display.setCursor(0, 43);
  display.print(F("Temp: "));
  display.print(temperature, 1);
  display.println(F(" C"));
  display.setCursor(0, 54);
  display.print(F("Fan:  "));
  display.print(fanPercent);
  display.print(F("%  Target: "));
  display.print(DRY_TARGET_RH, 0);
  display.print(F("%"));
  display.display();
}

void drawSensorFault() {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println(F("SEED DRYER"));
  display.drawFastHLine(0, 11, SCREEN_WIDTH, SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 25);
  display.println(F("SHT31 not found"));
  display.setCursor(0, 40);
  display.println(F("Check SDA / SCL"));
  display.display();
}

void takeSample() {
  float temperature = sht31.readTemperature();
  float humidity = sht31.readHumidity();

  if (isnan(temperature) || isnan(humidity)) {
    setFanPercent(100);
    if (!shownSensorFault) {
      drawSensorFault();
      shownSensorFault = true;
    }
    return;
  }

  int fanPercent = fanPercentForHumidity(humidity);
  setFanPercent(fanPercent);

  bool changed = shownSensorFault || isnan(shownTemperature) ||
                 fabsf(temperature - shownTemperature) >= 0.1f ||
                 fabsf(humidity - shownHumidity) >= 0.1f ||
                 fanPercent != shownFanPercent;
  if (changed) {
    drawReadings(temperature, humidity, fanPercent);
    shownTemperature = temperature;
    shownHumidity = humidity;
    shownFanPercent = fanPercent;
    shownSensorFault = false;
  }
}

void setup() {
  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);

  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  display.display();

  ledcAttach(FAN_PWM_PIN, FAN_PWM_FREQUENCY, FAN_PWM_RESOLUTION);
  setFanPercent(100);

  sht31.begin(0x44);
  takeSample();
}

void loop() {
  unsigned long now = millis();
  if (now - lastSampleMs >= SAMPLE_INTERVAL_MS) {
    lastSampleMs = now;
    takeSample();
  }

  unsigned long elapsed = millis() - lastSampleMs;
  unsigned long remaining = elapsed < SAMPLE_INTERVAL_MS ? SAMPLE_INTERVAL_MS - elapsed : 1;
  delay(remaining > 20 ? 20 : remaining);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik