Skip to content
Buy 10+ on select items — save 10% auto-applied
Free US shipping on orders $35+
Order by 3pm ET — ships same-day from the US
Skip to main content

ESP32 WebSocket Dashboard: Live Browser Sensor Chart | ShillehTek

September 13, 2026 8 views

ESP32 WebSocket Dashboard: Live Browser Sensor Chart | ShillehTek
Project

Build an ESP32 WebSocket dashboard that streams KY-018 and DHT11 sensor readings to a live browser chart twice per second, self-hosted with ShillehTek parts.

45 min Intermediate5 parts

Project Overview

ESP32 WebSocket Dashboard: Build an ESP32 live dashboard that streams KY-018 light readings plus DHT11 temperature and humidity to a browser chart twice a second using WebSockets, with a button that toggles the onboard LED.

A normal ESP32 web page shows a number when you load it; a WebSocket page shows the number changing while you watch. The board keeps a two-way connection open to every browser on the page and pushes a JSON packet every 500 ms, and the page draws a scrolling chart and updates the readouts instantly. Everything is served from the ESP32: no server, no internet, no libraries loaded from the web.

  • Time: ~45 minutes
  • Skill level: Intermediate
  • What you will build: A self-hosted live dashboard with a canvas chart, three sensor readouts, and a browser-to-board control using ESPAsyncWebServer's built-in WebSocket support.
ESP32 WebSocket live dashboard in a browser showing light, temperature, and humidity with a scrolling chart
Readings stream to the browser as they happen.

Parts List

From ShillehTek

External

  • None - the page is served by the ESP32 itself

Note: On the ESP32, analogRead() only works on ADC1 pins (GPIO32-39) while Wi-Fi is on because ADC2 pins are taken over by the radio. That's why the light sensor goes on GPIO34.

Step-by-Step Guide

Step 1 - Wire the Sensors

Goal: Connect the KY-018 and DHT11 to the ESP32 so the sketch can read light, temperature, and humidity.

What to do: Wire the KY-018: S to GPIO34, + to 3V3, and - to GND. Wire the DHT11: DATA to GPIO4, VCC to 3V3, and GND to GND. The onboard LED on GPIO2 is the control target.

ESP32 wired to a KY-018 photoresistor module on a breadboard with signal on GPIO34 and power on 3V3 and GND
The original used a bare LDR with a divider resistor; the KY-018 module has the divider built in.

Expected result: Hardware is wired and ready for code upload.

Step 2 - Install the Libraries

Goal: Set up the async web server and sensor libraries required by the sketch.

What to do: In the Arduino IDE Library Manager, install "ESP Async WebServer" (the ESP32Async release) and its companion "Async TCP", plus the Adafruit DHT sensor library and Adafruit Unified Sensor.

Expected result: The sketch in the next step compiles successfully.

Step 3 - Upload the Sketch (Board + Web Page)

Goal: Flash the ESP32 with firmware that serves the dashboard and streams data over WebSockets.

What to do: Paste the sketch below into the Arduino IDE.

Code:

#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <DHT.h>

const char* SSID = "YourNetwork";
const char* PASS = "YourPassword";
const int LDR_PIN = 34, LED_PIN = 2;
DHT dht(4, DHT11);
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");

// The whole web page lives in flash. No external files, no CDN.
const char PAGE[] PROGMEM = R"rawliteral(
<!DOCTYPE html><html><head><meta name="viewport" content="width=device-width,initial-scale=1">
<title>ESP32 Live</title>
<style>body{font-family:sans-serif;background:#111;color:#eee;text-align:center;margin:0;padding:16px}
.v{font-size:2em;margin:0 8px}canvas{width:100%;max-width:640px;background:#222;border-radius:8px}
button{font-size:1.1em;padding:10px 24px;border:0;border-radius:6px;background:#2a7;color:#fff}</style></head>
<body><h2>ESP32 Live Dashboard</h2>
<div>Light <span class="v" id="L">--</span> Temp <span class="v" id="T">--</span>C  Hum <span class="v" id="H">--</span>%</div>
<canvas id="c" width="640" height="200"></canvas>
<button onclick="ws.send('toggle')">Toggle LED</button>
<script>
const c=document.getElementById('c'),g=c.getContext('2d'),hist=[];
const ws=new WebSocket('ws://'+location.host+'/ws');
ws.onmessage=e=>{const d=JSON.parse(e.data);
  document.getElementById('L').textContent=d.light;
  document.getElementById('T').textContent=d.temp.toFixed(1);
  document.getElementById('H').textContent=d.hum.toFixed(0);
  hist.push(d.light); if(hist.length>128) hist.shift(); draw();};
function draw(){g.clearRect(0,0,640,200);g.strokeStyle='#2a7';g.lineWidth=2;g.beginPath();
  hist.forEach((v,i)=>{const x=i*5,y=195-v/4095*190; i?g.lineTo(x,y):g.moveTo(x,y);});g.stroke();}
</script></body></html>)rawliteral";

void onWsEvent(AsyncWebSocket* s, AsyncWebSocketClient* client, AwsEventType type,
               void* arg, uint8_t* data, size_t len) {
  if (type == WS_EVT_CONNECT) Serial.printf("client %u connected\n", client->id());
  if (type == WS_EVT_DATA) {                                  // a message from the browser
    String msg; for (size_t i = 0; i < len; i++) msg += (char)data[i];
    if (msg == "toggle") digitalWrite(LED_PIN, !digitalRead(LED_PIN));
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT); dht.begin();
  WiFi.begin(SSID, PASS);
  while (WiFi.status() != WL_CONNECTED) delay(250);
  Serial.println("Open http://" + WiFi.localIP().toString());
  ws.onEvent(onWsEvent);
  server.addHandler(&ws);
  server.on("/", HTTP_GET, [](AsyncWebServerRequest* r) { r->send(200, "text/html", PAGE); });
  server.begin();
}

void loop() {
  static unsigned long last = 0; static int tick = 0; static float t = 0, h = 0;
  if (millis() - last >= 500) {                               // push twice a second
    last = millis();
    if (++tick % 4 == 0) {                                    // DHT11 only every 2 s
      float nt = dht.readTemperature(), nh = dht.readHumidity();
      if (!isnan(nt)) { t = nt; h = nh; }
    }
    int light = analogRead(LDR_PIN);                          // 0..4095
    String json = "{\"light\":" + String(light) + ",\"temp\":" + String(t, 1) + ",\"hum\":" + String(h, 0) + "}";
    ws.textAll(json);                                         // to every connected browser
    ws.cleanupClients();
  }
}

What to do: Fill in your Wi-Fi details, upload, open the Serial Monitor for the IP address, and open it in a browser (phone or laptop on the same network).

Expected result: The readouts appear within a second and the light trace scrolls across the chart. Cover the KY-018 with your hand and the line dives immediately. Tap "Toggle LED" and the blue LED on the board flips. Open the page on a second device and both update in step.

Step 4 - Understand the WebSocket Flow

Goal: Understand how the browser and ESP32 communicate without refreshing.

What to do: The browser loads the page once over plain HTTP, then the script opens a WebSocket to /ws. From then on, the connection stays open: ws.textAll() on the board pushes to every client with no request, and ws.send() in the page pushes to the board. Compare that with polling (reloading a URL every second), which costs a full TCP handshake and headers per update. WebSockets carry a few bytes per message and react in milliseconds. The async server means loop() never blocks on a client either.

Expected result: You understand why the dashboard feels instant compared to refresh-based updates.

Step 5 - Make It Yours

Goal: Adapt the dashboard pattern to your own sensors and controls.

What to do: Add fields to the JSON and spans to the page for any sensor (a BME280, soil probe, or the JSN-SR04T). Send commands the other way ("fan:1", "set:24.5") and parse them in onWsEvent to drive relays or servos. Draw a second trace for temperature, or log the readings to an SD card while streaming them. For access away from home, keep the local dashboard and add MQTT for the cloud side, as in our Home Assistant guide.

Expected result: A live view of any device you build, on any phone in the house.

Conclusion

With one page stored in flash and one WebSocket connection, your ESP32 streams live KY-018 and DHT11 readings to every browser on your network while also accepting a button command to toggle the onboard LED. WebSockets are the difference between a web page about your sensor and a live instrument.

Want the exact parts used in this build? Grab them from ShillehTek.com. If you want help customizing this project or building something for your product, check out our IoT consulting services.

Photo credit: Original reference guide and images from donskytech on Hackster.io (MIT license).

Parts for this build

Everything used in this tutorial. Uncheck what you already have.

All 5 in stock
0 parts selected $0.00