Read a DHT11 and show live temperature & humidity on a page that refreshes every 10 seconds.
Project_9_ESP32_DHT11_Web_Server.ino, then open it in the Arduino IDE (setup: Foundation 3). Needs the DHT, Adafruit Unified Sensor, ESPAsyncWebServer + AsyncTCP libraries and your Wi‑Fi credentials.
By the end of this project, you will:
isnan() instead of publishing a nonsense number.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
The first two are in the Library Manager; the async pair install as ZIPs (as in Projects 7–8).
| Qty | Part | Notes |
|---|---|---|
| 1 | ESP32 · breadboard · jumper wires | - |
| 1 | DHT11 module | 3‑pin: −, +, S |

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.
The async server again, with a sensor behind it. Here is what is new.
#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.
dht.readTemperature() hides all of it.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.
<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.
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.
loop() againReadings 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.
| Code | What 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.responseText | Swaps 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. |

This is the first project that measures the world and publishes it, which is what most IoT devices spend their lives doing.
dht.readTemperature() replaces microsecond-accurate pulse timing.isnan()) and show something honest: a dash beats a wrong number.| Symptom | Likely cause | Fix |
|---|---|---|
Reads -- / "Failed to read" | Signal not on GPIO 4 / bad wiring | S → GPIO 4, + → 3.3 V, − → GND |
| Compile error (DHT/sensor) | Libraries missing | Install DHT + Adafruit Unified Sensor |
| Compile error (async) | Async libs missing | Install ESPAsyncWebServer + AsyncTCP |
| Icons missing on page | No internet on the phone | Numbers still work; give the device internet |
| Values never change | DHT11 is slow | Wait ~10 s between updates |