Indoor station: temperature, humidity, and OLED

Build a DHT11 indoor station with an OLED, historical temperature extremes, and a temperature indicator LED with hysteresis.

Sensors and measurement displaysBeginner90 min

What you will learn

  • Connect a DHT11, OLED, and LED using a shared 3.3 V wiring plan.
  • Separate 2.5-second sampling from display refreshes.
  • Track successful temperature minima and maxima since reset.
  • Control an LED using two temperature thresholds and its previous state.
  • Handle failed readings without displaying stale current values.

Before you start

Courses 02, 10, and 11: an LED with a resistor, DHT11 readings, and an SSD1306 I2C display.

Reference setup

Board in Arduino IDE
ESP32 Dev Module
Arduino-ESP32
3.3.12
Serial Monitor
115200 baud

This is a reference profile, not an identification of the pictured board. Adapt the GPIO mapping and verify the circuit before using ESP32-C3/S2/S3 or a differently labelled board.

What we will build

We will combine a DHT11, OLED, and LED into a small indoor station. The screen shows temperature, relative humidity, and the lowest and highest temperatures read so far. The LED indicates a selected temperature threshold. The device works locally without Wi-Fi or a computer as its display.

What you will learn

  • Combine a sensor, display, and indicator LED.
  • Separate measurement intervals from display refresh intervals.
  • Keep minimum and maximum values from successful readings.
  • Apply two temperature thresholds, known as hysteresis.
  • Show errors without presenting an old reading as current.

Prerequisites

Complete courses 02, 10, and 11. You should understand LED polarity, the DHT11 library, and I2C address checking. The reference board is a classic ESP32/ESP32-WROOM-32. GPIO labels are not numbered physical pin positions; other ESP32 families require adaptation.

Equipment and module requirements

ComponentQuantitySpecification and notes
Development board with a classic ESP32-WROOM-32 module1Match the reference GPIO labels; check physical header positions on the actual board.
USB data and power cable1Use the connector fitted to your board; the pictured kit lists Micro-USB.
Solderless breadboard1The kit lists 830 tie points. Check whether the power rails are split.
DHT11 temperature and relative humidity module1Three-terminal VCC/GND/DATA module verified for 3.3 V; DATA pull-up only to 3V3. Some variants are unreliable at 3.3 V.
10 kΩ resistor1R2: DATA pull-up to 3V3. The quantity per resistance value in the kit is unconfirmed.
0.96-inch OLED — reference I2C SSD1306 128×641VCC/GND/SDA/SCL, 3.3 V supply, onboard pull-ups to 3V3. The image does not confirm the controller, interface or resolution.
Red LED1Anode A and cathode K; identify polarity on the actual part.
220 Ω resistor1One current-limiting resistor per LED branch; use the kit’s 220 Ω resistors.
Jumper wires12Use male-to-male or female-to-male leads to suit the board headers.

Approximate quantity, depending on layout and connector types.

We use a three-pin DHT11 module rated for 3.3 V and a four-pin I2C SSD1306 OLED, 128 × 64, also rated for 3.3 V. Verify the actual modules: a photograph does not establish controller or pin order. An SH1106 requires a different driver; a bare four-pin DHT11 has a different physical wiring diagram. Adafruit: DHT connections, SSD1306

Measurements and history

When a reading succeeds, current temperature and humidity come from that cycle. Minimum and maximum describe successful temperature readings since reset. They are not retained after power loss and are not necessarily daily extremes.

This is an educational indoor station. The DHT11 and its placement limit the result; decimal digits do not increase sensor accuracy. Keep it away from a warm board when comparing room readings. Adafruit: DHT overview

Wiring and assembly

Wiring diagram

DHT11 and OLED use 3.3 V and common ground. R2, 10 kΩ, pulls DATA up; R1, 220 Ω, limits LED current. Apply the DHT11 and SSD1306 module checks from courses 10 and 11.

Wiring diagram — electrical connections listed in the table below
Wiring diagramEnlargeSVGPNG
Connections · Wiring diagram
FromToConnection
ESP32.3V3DHT1.VCCUse a DHT11 module that operates reliably at 3.3 V.
ESP32.GNDDHT1.GNDCommon ground.
DHT1.DATAESP32.GPIO26Bidirectional DHT11 data line.
ESP32.3V3R2.1R2 is a 10 kΩ pull-up to 3V3.
R2.2DHT1.DATAConnect the other end of R2 to the DATA/GPIO26 node.
ESP32.3V3OLED1.VCC128×64 I2C SSD1306 module with 3.3 V supply and pull-ups.
ESP32.GNDOLED1.GNDCommon ground.
ESP32.GPIO21OLED1.SDAI2C data; identify the SDA terminal.
ESP32.GPIO22OLED1.SCLI2C clock; identify the SCL terminal.
ESP32.GPIO25R1.1Digital output for the LED.
R1.2D1.AR1, 220 Ω, in series with the LED anode.
D1.KESP32.GNDCathode to common ground.
  1. Disconnect USB. Connect OLED VCC to 3V3, GND to GND, SDA to GPIO21, and SCL to GPIO22.
  2. Connect DHT11 VCC to 3V3, GND to GND, and DATA to GPIO26.
  3. Add R2, 10 kΩ, between DATA and 3V3. This reference includes it even when the module has a pull-up; no pull-up may go to 5 V.
  4. Connect GPIO25 through R1, 220 Ω, to the LED anode; connect its cathode to GND.
  5. Check the common ground, pin labels, and OLED pull-ups to 3.3 V. Connect USB.

If your DHT module is unreliable at 3.3 V, check its specification and wiring. Switching its supply to 5 V without checking DATA levels is not a suitable correction.

Arduino setup

Use Arduino IDE 2.x, esp32 by Espressif Systems 3.3.12, and ESP32 Dev Module. Install these versions through Library Manager:

LibraryVersion
DHT sensor library by Adafruit1.4.7
Adafruit Unified Sensor1.1.15
Adafruit SSD13062.5.17
Adafruit GFX Library1.12.6
Adafruit BusIO1.17.4

Wire comes with the ESP32 package. Extract the Arduino ZIP and open indoor_station.ino in its matching folder. Select the port, run Verify and Upload, and set Serial Monitor to 115200 baud.

Library Manager — documented versions and dependencies
Library and releaseVersionDependencies
DHT sensor library1.4.7Adafruit Unified Sensor 1.1.15
Adafruit Unified Sensor1.1.15None
Adafruit SSD13062.5.17Adafruit GFX Library 1.12.6
Adafruit GFX Library1.12.6Adafruit BusIO 1.17.4
Adafruit BusIO1.17.4None

Wire is included in the ESP32 core; do not install it separately through Library Manager.

Complete program

indoor_station.ino Arduino / C++
Download .ino

#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>
#include <math.h>

constexpr uint8_t SDA_PIN = 21;
constexpr uint8_t SCL_PIN = 22;
constexpr uint8_t DHT_PIN = 26;
constexpr uint8_t LED_PIN = 25;
constexpr uint8_t OLED_ADDRESS = 0x3C;
constexpr uint16_t OLED_WIDTH = 128;
constexpr uint16_t OLED_HEIGHT = 64;
constexpr uint32_t SAMPLE_MS = 2500;
constexpr uint32_t DISPLAY_MS = 250;
constexpr float HOT_ON_C = 28.0f;
constexpr float HOT_OFF_C = 27.0f;
Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, -1, 100000UL, 100000UL);
DHT dht(DHT_PIN, DHT11);

bool ready = false;
bool sampleAttempted = false;
bool currentValid = false;
bool hasHistory = false;
bool hotLed = false;
float currentTemperature = NAN;
float currentHumidity = NAN;
float minTemperature = NAN;
float maxTemperature = NAN;
uint32_t lastSampleAt = 0;
uint32_t lastDisplayAt = 0;

void scanI2CBus() {
  Serial.println("I2C scan (an ACK alone does not identify the display controller):");
  uint8_t found = 0;
  for (uint8_t address = 1; address < 127; ++address) {
    Wire.beginTransmission(address);
    if (Wire.endTransmission() == 0) {
      Serial.print("ACK at 0x"); Serial.println(address, HEX);
      ++found;
    }
  }
  if (found == 0) Serial.println("No I2C device acknowledged.");
}

bool initializeDisplay() {
  if (!Wire.begin(SDA_PIN, SCL_PIN)) {
    Serial.println("OLED ERROR: I2C bus initialization failed.");
    return false;
  }
  Wire.setClock(100000);
  Wire.setTimeOut(50);
  scanI2CBus();
  Wire.beginTransmission(OLED_ADDRESS);
  if (Wire.endTransmission() != 0) {
    Serial.println("OLED ERROR: configured address did not ACK. Check wiring/address; reset to retry.");
    return false;
  }
  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS, false, false)) {
    Serial.println("OLED ERROR: display buffer initialization failed; reset to retry.");
    return false;
  }
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setTextWrap(false);
  return true;
}

void drawStation() {
  display.clearDisplay();
  display.setCursor(0, 0); display.print("WB Maker Bridge");
  display.setCursor(0, 12);
  if (!sampleAttempted) {
    display.print("Waiting for DHT11");
    display.setCursor(0, 22); display.print("First read: 2.5 s");
  } else if (!currentValid) {
    display.print("SENSOR ERROR");
    display.setCursor(0, 22); display.print("No current reading");
  } else {
    display.print("T: "); display.print(currentTemperature, 1); display.print(" C");
    display.setCursor(0, 22);
    display.print("RH: "); display.print(currentHumidity, 1); display.print(" %");
  }
  display.setCursor(0, 32); display.print(hotLed ? "Hot LED: ON" : "Hot LED: OFF");
  display.setCursor(0, 44);
  if (hasHistory) {
    display.print("Hist min: "); display.print(minTemperature, 1); display.print(" C");
    display.setCursor(0, 54);
    display.print("Hist max: "); display.print(maxTemperature, 1); display.print(" C");
  } else {
    display.print("History: no data");
  }
  display.display();
}

void sampleEnvironment() {
  sampleAttempted = true;
  const float humidity = dht.readHumidity();
  const float temperature = dht.readTemperature();
  if (isnan(humidity) || isnan(temperature)) {
    currentValid = false;
    currentTemperature = NAN;
    currentHumidity = NAN;
    hotLed = false;
    digitalWrite(LED_PIN, LOW);
    Serial.println("SENSOR ERROR: no current reading; LED OFF; valid min/max history retained.");
    return;
  }
  currentValid = true;
  currentTemperature = temperature;
  currentHumidity = humidity;
  if (!hasHistory) {
    minTemperature = temperature;
    maxTemperature = temperature;
    hasHistory = true;
  } else {
    if (temperature < minTemperature) minTemperature = temperature;
    if (temperature > maxTemperature) maxTemperature = temperature;
  }
  if (temperature >= HOT_ON_C) hotLed = true;
  else if (temperature <= HOT_OFF_C) hotLed = false;
  digitalWrite(LED_PIN, hotLed ? HIGH : LOW);
  Serial.print("T_C="); Serial.print(temperature, 1);
  Serial.print(" RH_pct="); Serial.print(humidity, 1);
  Serial.print(" min_C="); Serial.print(minTemperature, 1);
  Serial.print(" max_C="); Serial.print(maxTemperature, 1);
  Serial.print(" LED="); Serial.println(hotLed ? "ON" : "OFF");
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);
  dht.begin();
  ready = initializeDisplay();
  if (!ready) return;
  lastSampleAt = millis();
  lastDisplayAt = lastSampleAt;
  drawStation();
  Serial.println("Indoor station: waiting 2.5 seconds for the first DHT11 sample.");
}

void loop() {
  if (!ready) return;
  const uint32_t now = millis();
  bool newSample = false;
  if (static_cast<uint32_t>(now - lastSampleAt) >= SAMPLE_MS) {
    lastSampleAt = now;
    sampleEnvironment();
    newSample = true;
  }
  if (newSample || static_cast<uint32_t>(now - lastDisplayAt) >= DISPLAY_MS) {
    lastDisplayAt = now;
    drawStation();
  }
}

Check that OLED_ADDRESS matches your scan from course 11: the default is 0x3C; use 0x3D only when confirmed. An ACK does not identify the controller. If OLED initialization fails, the program reports the reason and does not start normal operation.

Sampling and display updates

SAMPLE_MS is 2500 ms. After successful display setup, the program waits that interval before its first reading. Until then, it shows Waiting for DHT11, an unlit LED, and no history. The DHT library uses a two-second minimum interval; we allow 2.5 seconds. DHT source

sampleEnvironment() reads temperature and humidity. drawStation() redraws every 250 ms and immediately after a measurement attempt. More frequent drawing does not mean a new measurement: the value remains from the latest cycle. Scheduling uses elapsed millis() values.

Extremes and hysteresis

The first successful reading sets both minTemperature and maxTemperature. Later successful readings extend that range. hasHistory distinguishes real history from startup without data.

The LED uses two thresholds: a temperature at or above 28 °C sets hotLed on; at or below 27 °C sets it off. Between them, it retains its previous state. HOT_ON_C and HOT_OFF_C therefore avoid switching at every crossing of one single threshold. The initial state is off.

Handling failed readings

If either temperature or humidity is NaN, the whole current pair is rejected. currentValid becomes false, the display shows SENSOR ERROR and No current reading, and the LED turns off. Earlier extremes remain under Hist min and Hist max.

Sampling attempts continue. The first successful recovery restores current values; hysteresis resumes from an off LED. Recovery between the thresholds therefore leaves it off. An unlit LED during an error does not confirm a low temperature.

Running and checking behavior

Observe startup, several readings, and the recorded extremes. Write down your actual observations. To check error handling, disconnect USB, disconnect only DHT DATA from GPIO26, and power up again; expect an error after a reading attempt. Restore the connection with power disconnected. This test starts without history because of the restart. Retained history in the table refers to an error during the same run.

ConditionExpected result
Before the first attemptWaiting; LED OFF; no history
First successful sampleT/RH shown; minimum = maximum = temperature
Temperature reaches 28 °CLED ON
Temperature reaches 27 °CLED OFF
Reading failsError; LED OFF; any existing history retained

Troubleshooting

ProblemCheck and correction
No OLED responseCheck power, SDA/SCL, address, and controller type.
Persistent SENSOR ERRORCheck DHT11 type, GPIO26, ground, supply, and R2.
LED behaves unexpectedlyCheck polarity, GPIO25, R1, and both thresholds.
Minimum stays oldIt is history since reset, not the current reading.
Screen refreshes without a new temperatureSampling and drawing have separate intervals.

Independent challenge

Change the thresholds to 30 °C for switching on and 28 °C for switching off. On paper, trace successful temperatures of 27, 30, 29, and 28, then an error, then a successful 29 °C reading. Determine the LED and historical extremes after each step. This is an analysis sequence, not measured data.

Questions and worked answers

Why not store zero on an error? Zero would be an invented measurement and could corrupt the minimum. Why retain history? It still describes earlier successful samples and is clearly labelled as historical.

For the challenge, set HOT_ON_C = 30.0f and HOT_OFF_C = 28.0f. The LED sequence is OFF, ON, ON, OFF, OFF, OFF. The minimum is 27 °C; from the second sample onward, the maximum is 30 °C. The error changes neither extreme. Resetting clears history again.

Downloads

Keep your observations with the program. Compilation and physical verification are separate results.

Primary sources