Breadboard demo with ESP32-C3 SuperMini, AHT20 sensor, OLED display showing target temperature, and a 40mm fan spinning

Intermediate

Build a Digital Thermostat Demo

SensorsAutomationActuators

Read a real temperature, compare it to a threshold, and physically switch a fan — closed-loop control you can actually see and feel.

8 real parts included

Total for this kit: 1,895.00 ฿

Doesn't include any order discount you may get at checkout.

What you'll need

Project Goal

This project builds a small closed-loop control system: read a real temperature, compare it to a threshold, and physically switch a fan on or off in response — control you can actually see and feel, not just read as a number on a screen. It's a step up from projects that only sense or only display, because here the controller's decision changes something in the physical world.

What the Controller Does

The ESP32-C3 continuously reads the current temperature from the sensor, compares it against a threshold defined in code, updates the display with the current reading, and switches the fan on or off based on that comparison. It is the only part of the system that makes decisions — the sensor, display, and fan simply report and respond.

What the Sensor Does

The AHT20 provides the current temperature reading (along with humidity, if you choose to use it), which the controller reads over the same I2C bus used for the display. This is the input half of the control loop — without a real reading, the controller has nothing meaningful to compare against the threshold.

What the Display Does

The OLED shows both the current temperature and the threshold it's being compared against, which makes the system's behavior visible and easy to verify — you can watch the displayed temperature approach the threshold and confirm the fan responds at the right moment, rather than guessing whether the logic is working correctly.

What the MOSFET and Fan Do

The logic-level MOSFET module switches the fan on or off based on the controller's comparison. A MOSFET is used here rather than a mechanical relay specifically because the fan is a DC load, and a MOSFET switches DC silently, with no moving parts and no audible click, and no contact wear over time. This particular module is confirmed logic-level, meaning it fully switches at 3.3-5V — the same voltage range the ESP32-C3's GPIO pins output directly — rather than needing a higher gate voltage that would require extra driving circuitry.

Why a MOSFET Instead of a Relay

Beyond the silence and lack of moving parts, a logic-level MOSFET switches directly from a GPIO pin without an intermediate driver stage, because its gate fully turns on within the controller's own output voltage range. A standard mechanical relay, by contrast, needs a coil current the GPIO pin usually can't supply directly, plus flyback protection for the coil, and wears out mechanically over repeated switching. For a small DC fan switched frequently by a temperature loop, the logic-level MOSFET is the simpler and more durable choice.

Software and Library Concepts

The core logic is a threshold comparison, but a naive implementation — fan on above X°, off below X° — causes a real problem: hysteresis. If the temperature hovers right at the exact threshold, tiny fluctuations cause the fan to rapidly flick on and off as the reading crosses back and forth over that single point. The fix is a small buffer gap between the on and off thresholds — for example, turn the fan on above 28° but only turn it back off below 27°. That one-degree gap means a fluctuation right around either number doesn't cause rapid cycling, because the fan has to clear the full gap in one direction before it changes state again. This is a genuinely important control-systems concept, not just a minor implementation detail — without it, the fan's relay-like switching will wear noticeably faster and the behavior will look glitchy even when the underlying logic is correct.

Testing and Troubleshooting

  • Fan never turns on even when the temperature is clearly above the threshold — check the MOSFET module's control-signal wiring, and confirm the code is actually comparing against the right threshold value rather than a leftover default.
  • Fan won't turn off or flickers rapidly around the threshold — almost always a missing hysteresis gap; add separate on and off thresholds a degree or two apart.
  • The displayed temperature and the fan's behavior don't seem to match — double-check the units and threshold value used in code against what the sensor actually reports (the AHT20 reports in Celsius by default).
  • Temperature reading looks stuck or unreasonable — confirm the AHT20 initialized successfully over I2C and isn't conflicting with the OLED on the same bus; a failed sensor init often silently returns a fixed or garbage value instead of an error.

Optional Future Upgrade: Proportional Fan Speed Control

Rather than switching the fan fully on or off, a future version could drive it with PWM and scale the fan's speed proportionally to how far the temperature is above the threshold — gentle airflow just above the line, full speed further above it. This builds directly on the same MOSFET wiring already in place, since PWM-capable logic-level MOSFETs handle a varying duty cycle the same way they handle a simple on/off signal.

Wiring

  • AHT20 sensor VCC → ESP32-C3 3V3
  • AHT20 sensor GND → ESP32-C3 GND
  • AHT20 sensor SDA → ESP32-C3 GPIO8 — shared I2C bus (default SDA on ESP32-C3/S3 Arduino core)
  • AHT20 sensor SCL → ESP32-C3 GPIO9 — shared I2C bus (default SCL)
  • OLED display VCC → ESP32-C3 3V3
  • OLED display GND → ESP32-C3 GND
  • OLED display SDA → ESP32-C3 GPIO8 — same bus as AHT20 (different I2C addresses: AHT20 = 0x38, OLED = 0x3C, so no conflict)
  • OLED display SCL → ESP32-C3 GPIO9 — same bus as AHT20
  • MOSFET module SIG → ESP32-C3 GPIO3 — digital control signal for the fan switch
  • MOSFET module VCC → ESP32-C3 3V3 — logic supply for the module's gate-drive circuitry
  • MOSFET module GND → ESP32-C3 GND — must share ground with both the ESP32 and the fan's power source
  • Fan red wire (+) → 5V (board's 5V pin, present when powered via USB-C)
  • Fan black wire (−) → MOSFET module load output terminal, with the module's other load terminal → GND — this routes the fan's return current through the MOSFET switch

Bring the AHT20 and OLED up first and confirm both respond on the shared I2C bus before wiring the MOSFET/fan stage — an I2C address conflict or bad wiring is easier to debug without a spinning fan in the loop. The ±0.5°C hysteresis band is what keeps the fan from rapidly clicking on/off right at the threshold; widen it if the fan still chatters in your setup. Never power the fan directly from a GPIO pin — it must always be switched through the MOSFET module.

Wiring Diagram

VCC → 3V3 GND SDA → GPIO8 SCL → GPIO9 VCC → 3V3 GND SDA → GPIO8 SCL → GPIO9 SIG → GPIO3 VCC → 3V3 GND ESP32-C3 AHT20 sensor OLED display MOSFET module 5V Fan red wire MOSFET module load output… Fan black wire

Starter Code

// Digital Thermostat Demo — ESP32-C3 SuperMini + AHT20 + OLED + MOSFET fan
#include <Wire.h>
#include <Adafruit_AHTX0.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// --- Pins — adjust to match your board's silkscreen if different ---
#define SDA_PIN 8
#define SCL_PIN 9
#define FAN_MOSFET_PIN 3

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_ADDR 0x3C

Adafruit_AHTX0 aht;
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

const float TEMP_THRESHOLD_C = 27.0; // fan turns on above this
const float HYSTERESIS_C = 0.5;      // fan turns off once temp drops this far below threshold

bool fanOn = false;

void setup() {
  Serial.begin(115200);
  Wire.begin(SDA_PIN, SCL_PIN);

  pinMode(FAN_MOSFET_PIN, OUTPUT);
  digitalWrite(FAN_MOSFET_PIN, LOW); // fan off at boot

  if (!aht.begin()) {
    Serial.println("AHT20 not found — check wiring");
    while (1) delay(1000);
  }

  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
    Serial.println("SSD1306 not found — check wiring");
    while (1) delay(1000);
  }
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
}

void updateFan(float tempC) {
  if (!fanOn && tempC >= TEMP_THRESHOLD_C) {
    fanOn = true;
  } else if (fanOn && tempC <= (TEMP_THRESHOLD_C - HYSTERESIS_C)) {
    fanOn = false;
  }
  digitalWrite(FAN_MOSFET_PIN, fanOn ? HIGH : LOW);
}

void loop() {
  sensors_event_t humidity, temp;
  aht.getEvent(&humidity, &temp);

  float tempC = temp.temperature;
  float rh = humidity.relative_humidity;

  updateFan(tempC);

  display.clearDisplay();
  display.setTextSize(2);
  display.setCursor(0, 0);
  display.print(tempC, 1);
  display.println(" C");

  display.setTextSize(1);
  display.setCursor(0, 20);
  display.print("Humidity: ");
  display.print(rh, 0);
  display.println(" %");

  display.setCursor(0, 35);
  display.print("Threshold: ");
  display.print(TEMP_THRESHOLD_C, 1);
  display.println(" C");

  display.setCursor(0, 50);
  display.print("Fan: ");
  display.println(fanOn ? "ON" : "OFF");

  display.display();

  Serial.print("Temp: "); Serial.print(tempC);
  Serial.print(" C  Humidity: "); Serial.print(rh);
  Serial.print(" %  Fan: "); Serial.println(fanOn ? "ON" : "OFF");

  delay(1000);
}

Not sure about a step? Ask us