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
| Component | Quantity | Specification and notes |
|---|---|---|
| Development board with a classic ESP32-WROOM-32 module | 1 | Match the reference GPIO labels; check physical header positions on the actual board. |
| USB data and power cable | 1 | Use the connector fitted to your board; the pictured kit lists Micro-USB. |
| Solderless breadboard | 1 | The kit lists 830 tie points. Check whether the power rails are split. |
| DHT11 temperature and relative humidity module | 1 | Three-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Ω resistor | 1 | R2: DATA pull-up to 3V3. The quantity per resistance value in the kit is unconfirmed. |
| 0.96-inch OLED — reference I2C SSD1306 128×64 | 1 | VCC/GND/SDA/SCL, 3.3 V supply, onboard pull-ups to 3V3. The image does not confirm the controller, interface or resolution. |
| Red LED | 1 | Anode A and cathode K; identify polarity on the actual part. |
| 220 Ω resistor | 1 | One current-limiting resistor per LED branch; use the kit’s 220 Ω resistors. |
| Jumper wires | 12 | Use 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.
| From | To | Connection |
|---|---|---|
ESP32.3V3 | DHT1.VCC | Use a DHT11 module that operates reliably at 3.3 V. |
ESP32.GND | DHT1.GND | Common ground. |
DHT1.DATA | ESP32.GPIO26 | Bidirectional DHT11 data line. |
ESP32.3V3 | R2.1 | R2 is a 10 kΩ pull-up to 3V3. |
R2.2 | DHT1.DATA | Connect the other end of R2 to the DATA/GPIO26 node. |
ESP32.3V3 | OLED1.VCC | 128×64 I2C SSD1306 module with 3.3 V supply and pull-ups. |
ESP32.GND | OLED1.GND | Common ground. |
ESP32.GPIO21 | OLED1.SDA | I2C data; identify the SDA terminal. |
ESP32.GPIO22 | OLED1.SCL | I2C clock; identify the SCL terminal. |
ESP32.GPIO25 | R1.1 | Digital output for the LED. |
R1.2 | D1.A | R1, 220 Ω, in series with the LED anode. |
D1.K | ESP32.GND | Cathode to common ground. |
- Disconnect USB. Connect OLED VCC to 3V3, GND to GND, SDA to GPIO21, and SCL to GPIO22.
- Connect DHT11 VCC to 3V3, GND to GND, and DATA to GPIO26.
- 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.
- Connect GPIO25 through R1, 220 Ω, to the LED anode; connect its cathode to GND.
- 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:
| Library | Version |
|---|---|
| DHT sensor library by Adafruit | 1.4.7 |
| Adafruit Unified Sensor | 1.1.15 |
| Adafruit SSD1306 | 2.5.17 |
| Adafruit GFX Library | 1.12.6 |
| Adafruit BusIO | 1.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 and release | Version | Dependencies |
|---|---|---|
| DHT sensor library | 1.4.7 | Adafruit Unified Sensor 1.1.15 |
| Adafruit Unified Sensor | 1.1.15 | None |
| Adafruit SSD1306 | 2.5.17 | Adafruit GFX Library 1.12.6 |
| Adafruit GFX Library | 1.12.6 | Adafruit BusIO 1.17.4 |
| Adafruit BusIO | 1.17.4 | None |
Wire is included in the ESP32 core; do not install it separately through Library Manager.
Complete program
#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.
| Condition | Expected result |
|---|---|
| Before the first attempt | Waiting; LED OFF; no history |
| First successful sample | T/RH shown; minimum = maximum = temperature |
| Temperature reaches 28 °C | LED ON |
| Temperature reaches 27 °C | LED OFF |
| Reading fails | Error; LED OFF; any existing history retained |
Troubleshooting
| Problem | Check and correction |
|---|---|
| No OLED response | Check power, SDA/SCL, address, and controller type. |
Persistent SENSOR ERROR | Check DHT11 type, GPIO26, ground, supply, and R2. |
| LED behaves unexpectedly | Check polarity, GPIO25, R1, and both thresholds. |
| Minimum stays old | It is history since reset, not the current reading. |
| Screen refreshes without a new temperature | Sampling 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.
