If you've ever wanted a live, browser-based dashboard that updates sensor values without page refresh, WebSockets are the answer. Unlike polling (where the browser asks "any news?" every N seconds), WebSocket keeps a persistent two-way connection open — the ESP32 pushes data the moment it's ready.
// What you'll build: ESP32 reads a DHT22 sensor every 2 seconds and broadcasts temperature, humidity, and uptime to every connected browser over WebSocket. The dashboard updates live with animated gauges — no frameworks, pure HTML+JS.
// AD SLOT — IN-CONTENT RESPONSIVE
Prerequisites
- ESP32 development board (any variant)
- DHT22 sensor + 10kΩ pull-up resistor
- Arduino IDE 2.x with ESP32 board support
- Libraries: ESPAsyncWebServer, AsyncTCP, DHT sensor library, ArduinoJson
| Library | Author | Install via |
|---|---|---|
| ESPAsyncWebServer | me-no-dev | GitHub ZIP |
| AsyncTCP | me-no-dev | GitHub ZIP |
| ArduinoJson | Benoît Blanchon | Library Manager |
| DHT sensor library | Adafruit | Library Manager |
Wiring — DHT22 to ESP32
| DHT22 Pin | ESP32 Pin | Notes |
|---|---|---|
| VCC | 3.3V | |
| DATA | GPIO 4 | 10kΩ pull-up to 3.3V |
| GND | GND |
Full Arduino Sketch
// main.ino#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <AsyncTCP.h>
#include <DHT.h>
#include <ArduinoJson.h>
const char* SSID = "YOUR_SSID";
const char* PASSWORD = "YOUR_PASSWORD";
#define DHT_PIN 4
#define DHT_TYPE DHT22
DHT dht(DHT_PIN, DHT_TYPE);
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");
unsigned long lastBroadcast = 0;
const unsigned long INTERVAL = 2000;
// Minimal dashboard HTML served from PROGMEM
const char INDEX_HTML[] PROGMEM = R"rawliteral(
<!DOCTYPE html><html><head>
<title>ESP32 Dashboard</title>
<style>
body{background:#020810;color:#c8e6f0;font-family:monospace;display:flex;flex-direction:column;align-items:center;padding:40px;}
.card{background:#071525;border:1px solid rgba(0,245,255,.2);border-radius:8px;padding:24px 40px;margin:12px;min-width:200px;text-align:center;}
.val{font-size:3rem;color:#00f5ff;font-weight:bold;}
.lbl{color:#6a9bb5;font-size:.85rem;margin-top:6px;}
h1{color:#00ff88;letter-spacing:4px;}
</style></head><body>
<h1>ESP32 LIVE DASHBOARD</h1>
<div style="display:flex;gap:20px;flex-wrap:wrap;justify-content:center;">
<div class="card"><div class="val" id="temp">--</div><div class="lbl">TEMPERATURE (°C)</div></div>
<div class="card"><div class="val" id="hum">--</div><div class="lbl">HUMIDITY (%)</div></div>
<div class="card"><div class="val" id="uptime">--</div><div class="lbl">UPTIME (s)</div></div>
</div>
<script>
const ws = new WebSocket('ws://' + location.hostname + '/ws');
ws.onmessage = e => {
const d = JSON.parse(e.data);
document.getElementById('temp').textContent = d.temp.toFixed(1);
document.getElementById('hum').textContent = d.hum.toFixed(1);
document.getElementById('uptime').textContent = d.uptime;
};
</script></body></html>
)rawliteral";
void broadcastSensorData() {
float t = dht.readTemperature();
float h = dht.readHumidity();
if (isnan(t) || isnan(h)) return;
StaticJsonDocument<128> doc;
doc["temp"] = t;
doc["hum"] = h;
doc["uptime"] = millis() / 1000;
String payload;
serializeJson(doc, payload);
ws.textAll(payload);
}
void onWsEvent(AsyncWebSocket* s, AsyncWebSocketClient* c,
AwsEventType type, void* arg, uint8_t* data, size_t len) {
// handle connect/disconnect if needed
}
void setup() {
Serial.begin(115200);
dht.begin();
WiFi.begin(SSID, PASSWORD);
while (WiFi.status() != WL_CONNECTED) delay(500);
Serial.println("IP: " + WiFi.localIP().toString());
ws.onEvent(onWsEvent);
server.addHandler(&ws);
server.on("/", HTTP_GET, [](AsyncWebServerRequest* req) {
req->send_P(200, "text/html", INDEX_HTML);
});
server.begin();
}
void loop() {
ws.cleanupClients();
if (millis() - lastBroadcast >= INTERVAL) {
lastBroadcast = millis();
broadcastSensorData();
}
}
Testing
- Flash the sketch and open the Serial Monitor at 115200 baud
- Note the IP address printed — e.g.
192.168.1.105 - Open
http://192.168.1.105in any browser on the same WiFi network - Values update live every 2 seconds — open multiple tabs to confirm multi-client broadcast
// Tip: Use
ws.cleanupClients() in loop() to automatically remove disconnected clients and prevent memory leaks.Going Further
- Add chart.js for live graphing of historical readings
- Send commands back to ESP32 via
ws.onmessageto toggle relays or LEDs - Secure the dashboard with HTTP Basic Auth using
request->authenticate() - Log readings to SD card using the AsyncWebServer SD File Manager