PROJECT 9 · SENSOR + WEB

DHT11 Temperature & Humidity Web Server

Read a DHT11 and show live temperature & humidity on a page that refreshes every 10 seconds.

Builds on: Project 8's polling pattern, now fetching sensor readings instead of a button state.

⏱ ~25 min 📊 Intermediate 🔖 Prerequisite: Project 5
⬇ Download the sketch Save Project_9_ESP32_DHT11_Web_Server.ino, then open it in the Arduino IDE (setup: Foundation 3). Needs the DHT, Adafruit Unified Sensor, ESP Async WebServer + AsyncTCP libraries and your Wi-Fi credentials.
🎲 Try it without hardware 📄 diagram.json Open the Wokwi simulator, then open or download diagram.json above and paste its wiring in to load the circuit.

🎯 Objective

By the end of this project, you will:

  • Read a real environmental sensor and get physical units out of it, not raw counts.
  • Use a sensor library and understand what it does for you underneath.
  • Handle a failed reading with isnan() instead of publishing a nonsense number.
  • Fill placeholders in a page with live values using a template processor.
  • Update parts of a page in the background, so figures refresh without a reload.
  • Compare this with Project 2's ADC: a digital sensor speaks numbers, an analog one gives a voltage you must interpret.

1The DHT11 sensor & auto‑updating pages

The DHT11 is a small, slow, cheap sensor that reports temperature (°C) and relative humidity (%) over one data wire. Read it every couple of seconds: no faster.

To refresh without reloading, the browser polls (like Project 8): two timers fetch /temperature and /humidity every 10 s and drop the numbers into the page:

setInterval(function () {
  xhttp.open("GET", "/temperature", true);   // fetch just the number
  ...
}, 10000);                                     // every 10 seconds

2Install the required libraries

  • DHT sensor library (Adafruit)
  • Adafruit Unified Sensor (dependency)
  • ESP Async WebServer
  • AsyncTCP (ESP32)

The first two are in the Library Manager; the async pair install as ZIPs (as in Projects 7–8).

3Parts & wiring

QtyPartNotes
1ESP32 · breadboard · jumper wires-
1DHT11 module3‑pin: −, +, S
DHT11: S to GPIO 4, + to 3.3 V, − to GND
S → GPIO 4 · + → 3.3 V · − → GND.
This kit's DHT11 is a 3‑pin module with the pull‑up built in, so it runs straight off 3.3 V: no extra resistor.

4Upload & use

Set your Wi‑Fi credentials, upload, open Serial Monitor at 115200, press EN/RESET:

==============================================
 Project 9: ESP32 DHT11 Web Server
==============================================
DHT11 data -> GPIO 4
Connecting to Wi-Fi: MyNetwork
....
Wi-Fi connected!
Open this address in your browser:  http://192.168.1.42
----------------------------------------------
[DHT] Temperature: 21.4 C
[DHT] Humidity: 63.9 %

The improved logging prints labeled readings with units (the original printed a bare number), so you can confirm the sensor works before opening the page.

Internet note: the page pulls its thermometer/drop icons from a font CDN, so the viewing device needs internet for the icons (the numbers work regardless).

5Code Walkthrough: Understanding the Sketch

The async server again, with a sensor behind it. Here is what is new.

Step 1: Telling the library which sensor, on which pin

#include <Adafruit_Sensor.h>
#include <DHT.h>

#define DHTPIN 4          // data pin
#define DHTTYPE DHT11     // the family member we have

DHT dht(DHTPIN, DHTTYPE);

DHT dht(DHTPIN, DHTTYPE) creates an object: a bundle of the pin number, the sensor type, and the code that talks to it. From here on you ask dht for readings rather than handling the pin yourself.

That library is doing real work. The DHT11 speaks a one-wire protocol where bits are encoded as pulses of different lengths, so reading it by hand means timing pulses to the microsecond. dht.readTemperature() hides all of it.

Step 2: Reading safely, with a fallback

String readDHTTemperature() {
  float t = dht.readTemperature();       // Celsius by default
  if (isnan(t)) {
    Serial.println("[DHT] Failed to read temperature!");
    return "--";                         // show a dash, not a wrong number
  }
  return String(t);
}

isnan() asks "is this Not a Number?" A DHT11 read fails now and then, from a loose wire or from being asked too often, and the library reports that by returning NaN. Checking for it is what keeps -- on the page instead of a meaningless value.

Both readers return String rather than float, because their output goes straight into a web page. dht.readTemperature(true) returns Fahrenheit.

Step 3: Placeholders in the page, filled at send time

<span id="temperature">%TEMPERATURE%</span>
<span id="humidity">%HUMIDITY%</span>
String processor(const String& var){
  if (var == "TEMPERATURE") return readDHTTemperature();
  else if (var == "HUMIDITY")  return readDHTHumidity();
  return String();
}

Same mechanism as Project 7, used for data instead of controls. The first page you load therefore already shows real values, with no wait.

Step 4: Two endpoints, and a page that refreshes itself

server.on("/temperature", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send_P(200, "text/plain", readDHTTemperature().c_str());
});
setInterval(function () {
  ...
  document.getElementById("temperature").innerHTML = this.responseText;
  xhttp.open("GET", "/temperature", true);
}, 10000);

Exactly Project 8's polling pattern, with two differences: each value has its own endpoint returning plain text, and the interval is 10 seconds rather than 1. The DHT11 produces a fresh sample only about once a second and is not a fast-moving quantity, so polling harder would gain nothing. innerHTML replaces just the contents of one <span>, which is why the figures change while the rest of the page sits still.

This pattern, a script fetching data in the background and updating part of the page, is what people usually mean by AJAX (Asynchronous JavaScript and XML, even though nothing here is XML). It's the opposite of Project 5's approach: there, clicking a button loaded an entirely new page (a full page reload, complete with a network round-trip and a visible flash), because the button was a plain link to a new URL. Here, only the two <span> contents change, and the rest of the page, its layout, styles, and icons, never re-downloads. The tradeoff is complexity: Project 5's link needs no JavaScript at all, while this page needs setInterval and XMLHttpRequest to pull off a partial update.

Step 5: an empty loop() again

Readings are taken on demand, when a browser asks. If you wanted logging every minute regardless of visitors, that is where a millis() timer from Project 4 would go.

Key concepts: what each line really does

CodeWhat it does
DHT dht(DHTPIN, DHTTYPE)Creates the sensor object holding the pin, the type, and the protocol code.
dht.begin()Prepares the pin and the library. Like Serial.begin(), it belongs in setup().
dht.readTemperature()Returns degrees Celsius as a float, already converted. Pass true for Fahrenheit.
dht.readHumidity()Returns relative humidity in percent.
isnan(t)Detects a failed reading. The library returns NaN on error, and NaN equals nothing, so it needs its own test.
String(t)Turns the number into text for the web page.
%TEMPERATURE% + processor()Placeholder and the function that fills it, so the first page load already carries real values.
send_P(200, "text/plain", ...)Replies with a bare number, no HTML: all the JavaScript needs.
innerHTML = this.responseTextSwaps the text inside one element, leaving the rest of the page untouched.
setInterval(..., 10000)Ten seconds between polls, matched to how fast a DHT11 can actually produce new data.

Going further: DHT11 vs. DHT22

DHT11 (this kit)DHT22
Temperature range0 to 50 °C−40 to 80 °C
Temperature accuracy±2 °C±0.5 °C
Humidity range20 to 90 %RH0 to 100 %RH
Humidity accuracy±5 %RH±2 %RH
Sampling rate~1 reading/s~1 reading/2 s
PriceCheaperMore expensive

The code barely changes: swap the commented #define DHTTYPE DHT22 line for the active one, rewire (a DHT22 module uses the same three pins), and everything else in this sketch, from dht.readTemperature() to the web page, works unchanged. The DHT22's wider range and tighter accuracy matter for anything outdoors or near freezing; for a room‑temperature demo like this one, the DHT11 is plenty.

Predict, then check. Predict: if you breathe gently near the sensor, does the humidity reading change faster or slower than the temperature reading? Try it and watch both numbers on the page.

6Demonstration

Animated demo: temperature and humidity readings update live on the phone
Readings update on their own: breathe near the sensor and watch humidity rise.

7Knowledge Check

Test your understanding.

1. What two values does the DHT11 sensor measure?

2. Why should you check `isnan()` before publishing a DHT11 reading?

3. How does the web page update the temperature and humidity without the user refreshing?

4. What happens if you use ADC2 pins while Wi-Fi is active?

8Wrapping Up: What You've Learned

This is the first project that measures the world and publishes it, which is what most IoT devices spend their lives doing.

  • A digital sensor hands you finished numbers over a protocol. Contrast Project 2, where the ADC gave a voltage and you did the interpreting.
  • Libraries exist because that protocol is fiddly: dht.readTemperature() replaces microsecond-accurate pulse timing.
  • Sensors fail sometimes. Always check (isnan()) and show something honest: a dash beats a wrong number.
  • Sensors have a sampling rate. The DHT11 manages roughly one reading per second, accurate to about ±2 °C and ±5 % RH, so a 10-second refresh is generous.
  • Template placeholders put live values into the first page load; polling keeps them fresh afterwards.
  • The page is now a dashboard. Project 11 sends the same readings to a cloud service so you can watch them from outside the building.

Now try

  • Temperature warning LED: Wire an LED to GPIO 26 and turn it on when the temperature exceeds a threshold (e.g., 30°C). The web page should also show the warning.
  • CSV data logging: Format the Serial output as comma-separated values (timestamp, temperature, humidity) so you can paste it into a spreadsheet for analysis.

9Troubleshooting

SymptomLikely causeFix
Reads -- / "Failed to read"Signal not on GPIO 4 / bad wiringS → GPIO 4, + → 3.3 V, − → GND
Compile error (DHT/sensor)Libraries missingInstall DHT + Adafruit Unified Sensor
Compile error (async)Async libs missingInstall ESP Async WebServer + AsyncTCP
Icons missing on pageNo internet on the phoneNumbers still work; give the device internet
Values never changeDHT11 is slowWait ~10 s between updates