1. Web Server di ESP32: AsyncWebServer vs WebServer
ESP32 memiliki keunggulan besar dalam proyek IoT: ia bisa berfungsi sebagai web server mandiri tanpa memerlukan komputer atau perangkat tambahan. Cukup dengan menghubungkan ESP32 ke jaringan WiFi, perangkat ini bisa melayani halaman web yang diakses dari browser di komputer, tablet, atau ponsel.
Di dunia Arduino/ESP32, terdapat dua library utama untuk membuat web server:
ESP32 WebServer (Bawaan)
Library ini disertakan secara default dalam core ESP32 Arduino. Sangat cocok untuk proyek-proyek sederhana yang hanya memerlukan beberapa endpoint. Kelebihannya adalah tidak memerlukan instalasi library tambahan, sehingga langsung bisa digunakan. Namun library ini bersifat synchronous â satu request harus selesai diproses sebelum request berikutnya diterima.
ESPAsyncWebServer
Library pihak ketiga yang sangat populer di komunitas ESP32. Menggunakan arsitektur asynchronous sehingga mampu menangani banyak request secara bersamaan tanpa memblokir loop utama. Sangat direkomendasikan untuk proyek yang melibatkan WebSocket, banyak klien, atau streaming data real-time.
Perbandingan WebServer vs AsyncWebServer
| Fitur | ESP32 WebServer | ESPAsyncWebServer |
|---|---|---|
| Tipe | Synchronous (blokir) | Asynchronous (non-blokir) |
| Multi-klien | Terbatas | Banyak klien simultan |
| WebSocket | Tidak didukung | â Didukung penuh |
| EventSource | Tidak didukung | â Didukung |
| Simplicity | â Sangat mudah | Sedikit lebih kompleks |
| RAM Usage | Lebih ringan | Sedikit lebih besar |
| Cocok untuk | Proyek sederhana, few endpoint | Dashboard real-time, banyak klien |
| Library | Bawaan ESP32 core | Perlu install: AsyncWebServer + AsyncTCP |
Untuk tutorial ini, kita akan menggunakan ESPAsyncWebServer karena lebih serbaguna dan didukung oleh komunitas yang luas. Namun untuk contoh paling awal (Section 3), kita juga menunjukkan cara menggunakan WebServer bawaan agar Anda memahami perbedaannya.
2. Setup & Instalasi Library
Sebelum membangun web server, pastikan Anda sudah menginstal ESP32 board di Arduino IDE (ikuti tutorial Panduan Lengkap ESP32 jika belum). Selanjutnya, kita perlu menginstal library tambahan.
Langkah 1: Instalasi Library via Arduino IDE
Buka menu Sketch â Include Library â Manage Libraries, kemudian cari dan instal library berikut:
| Library | Pengarang | Fungsi |
|---|---|---|
| ESPAsyncWebServer | me-no-dev (lacamera) | Server web asynchronous utama |
| AsyncTCP | me-no-dev | TCP asynchronous (dependency AsyncWebServer) |
| ArduinoJson | Benoit Blanchon | Pembuatan & parsing JSON |
| DHT sensor library | Adafruit | Support untuk sensor DHT11/DHT22 |
Langkah 2: Instalasi via Library Manager di Arduino IDE 2.x
- Buka Arduino IDE, kemudian buka menu Tools â Manage Libraries
- Pada kolom pencarian, ketik "ESPAsyncWebServer"
- Pilih "ESPAsyncWebServer by lacamera", klik Install
- Jika diminta, konfirmasi instalasi dependency (AsyncTCP akan terinstall otomatis)
- Ulangi langkah di atas untuk "ArduinoJson" oleh Benoit Blanchon
Langkah 3: Instalasi Manual (Alternatif)
Jika Library Manager bermasalah, Anda bisa clone langsung dari GitHub:
/"HTTP Polling (Traditional):"/
/"Browser"/
/"GET /data"/
/"ESP32"/
/"Browser"/
/"ESP32"/
/"Browser"/
/"GET /data"/
/"ESP32 (ulang terus...)"/
/"Browser"/
/"ESP32"/
/"WebSocket (Real-time):"/
/"Browser"/
/"WS Connect"/
/"ESP32 (koneksi dibuka)"/
/"Browser"/
/"Data Push"/
/"ESP32 (kapan saja)"/
/"Browser"/
/"Data Push"/
/"ESP32 (kapan saja)"/
/"Browser"/
/"Control Cmd"/
/"ESP32 (dua arah!)"/
/"Browser"/
/"Data Push"/
/"ESP32"/
/"... (koneksi tetap terbuka)"/
// WebSocket Real-time: Sensor Push ke Browser
// BeebaneLabs - https://beebanelabs.pages.dev
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <ArduinoJson.h>
const char* ssid = "NamaWiFiAnda";
const char* password = "PasswordWiFiAnda";
AsyncWebServer server(80);
AsyncWebSocket ws("/ws"); // Endpoint WebSocket
// Simulasi sensor (ganti dengan sensor asli)
float sensorValue = 25.0;
unsigned long lastUpdate = 0;
// Callback saat menerima pesan WebSocket dari klien
void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client,
AwsEventType type, void *arg, uint8_t *data, size_t len) {
switch (type) {
case WS_EVT_CONNECT:
Serial.printf("[WS] Klien #%u terhubung dari %s\n",
client->id(), client->remoteIP().toString().c_str());
break;
case WS_EVT_DISCONNECT:
Serial.printf("[WS] Klien #%u terputus\n", client->id());
break;
case WS_EVT_DATA: {
AwsFrameInfo *info = (AwsFrameInfo *)arg;
if (info->opcode == WS_TEXT) {
// Parse pesan dari klien
String msg = "";
for (size_t i = 0; i < len; i++) {
msg += (char)data[i];
}
Serial.printf("[WS] Pesan dari #%u: %s\n", client->id(), msg.c_str());
// Contoh: klien minta data sekarang
if (msg == "get_data") {
StaticJsonDocument<128> doc;
doc["type"] = "sensor_data";
doc["value"] = serialized(String(sensorValue, 1).c_str());
doc["timestamp"] = millis();
String response;
serializeJson(doc, response);
client->text(response);
}
}
break;
}
default:
break;
}
}
// Kirim data ke SEMUA klien WebSocket yang terhubung
void broadcastSensorData() {
if (ws.count() == 0) return; // Tidak ada klien, skip
StaticJsonDocument<128> doc;
doc["type"] = "sensor_data";
doc["value"] = serialized(String(sensorValue, 1).c_str());
doc["timestamp"] = millis();
doc["clients"] = ws.count();
String json;
serializeJson(doc, json);
ws.textAll(json.c_str()); // Kirim ke semua klien
}
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Real-time Sensor WebSocket</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: sans-serif;
background: #0a0a14;
color: #e0e0e0;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.monitor {
background: #141422;
padding: 2rem;
border-radius: 16px;
width: 400px;
border: 1px solid rgba(62, 207, 142, 0.2);
}
h1 { color: #ffeb00; font-size: 1.3em; text-align: center; }
.value-display {
text-align: center;
font-size: 4em;
font-weight: bold;
color: #ffeb00;
margin: 1rem 0;
font-family: monospace;
}
.info { font-size: 0.85em; color: #888; text-align: center; }
.dot {
display: inline-block;
width: 10px; height: 10px;
border-radius: 50%;
margin-right: 6px;
background: #ef4444;
}
.dot.connected { background: #ffeb00; }
#chart {
width: 100%;
height: 120px;
margin-top: 1rem;
background: #0a0a14;
border-radius: 8px;
position: relative;
overflow: hidden;
}
</style>
</head>
<body>
<div class="monitor">
<h1>đĄ Sensor Real-time</h1>
<div class="value-display" id="sensorVal">--.-</div>
<div class="info">
<span class="dot" id="statusDot"></span>
<span id="statusText">Terputus</span>
<br><span id="updateTime"></span>
</div>
<canvas id="chart"></canvas>
</div>
<script>
let ws;
let chartData = [];
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
function connectWS() {
ws = new WebSocket(
'ws://' + location.host + '/ws'
);
ws.onopen = () => {
document.getElementById('statusDot').classList.add('connected');
document.getElementById('statusText').textContent = 'Terhubung';
ws.send('get_data');
};
ws.onclose = () => {
document.getElementById('statusDot').classList.remove('connected');
document.getElementById('statusText').textContent = 'Terputus';
setTimeout(connectWS, 2000); // Reconnect
};
ws.onmessage = (evt) => {
const data = JSON.parse(evt.data);
if (data.type === 'sensor_data') {
document.getElementById('sensorVal').textContent =
data.value + '°C';
document.getElementById('updateTime').textContent =
'Update: ' + new Date().toLocaleTimeString();
// Simpan untuk chart
chartData.push(parseFloat(data.value));
if (chartData.length > 100) chartData.shift();
drawChart();
}
};
}
function drawChart() {
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (chartData.length < 2) return;
const min = Math.min(...chartData) - 2;
const max = Math.max(...chartData) + 2;
const range = max - min || 1;
ctx.strokeStyle = '#ffeb00';
ctx.lineWidth = 2;
ctx.beginPath();
chartData.forEach((v, i) => {
const x = (i / (chartData.length - 1)) * canvas.width;
const y = canvas.height - ((v - min) / range) * canvas.height;
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
}
connectWS();
</script>
</body>
</html>
)rawliteral";
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) delay(500);
Serial.println("\nIP: " + WiFi.localIP().toString());
// Daftarkan WebSocket event handler
ws.onEvent(onWsEvent);
server.addHandler(&ws);
// Route halaman utama
server.on("/", HTTP_GET, [](AsyncWebServerRequest *req) {
req->send_P(200, "text/html", index_html);
});
// Cleanup WebSocket lama secara berkala
server.on("/cleanup", HTTP_GET, [](AsyncWebServerRequest *req) {
ws.cleanupClients();
req->send(200, "text/plain", "Cleaned");
});
server.begin();
}
void loop() {
// Simulasi perubahan sensor (ganti dengan pembacaan asli)
if (millis() - lastUpdate > 1000) {
sensorValue += random(-5, 6) / 10.0;
sensorValue = constrain(sensorValue, 15.0, 45.0);
broadcastSensorData();
lastUpdate = millis();
}
// Cleanup koneksi WebSocket yang sudah tidak aktif
ws.cleanupClients();
}
Dengan WebSocket, ESP32 secara aktif mengirim data terbaru ke semua browser yang terhubung. Tidak perlu refresh halaman atau polling berulang. Ini menghemat bandwidth WiFi, mengurangi beban CPU ESP32, dan memberikan pengalaman real-time yang mulus bagi pengguna. WebSocket juga mendukung komunikasi dua arah â browser bisa mengirim perintah kontrol ke ESP32 melalui koneksi yang sama.
8. Proyek: Dashboard Sensor Multi-page
Di bagian ini, kita akan menggabungkan semua konsep yang sudah dipelajari menjadi proyek lengkap: Dashboard Sensor Multi-halaman dengan beberapa halaman (home, sensor detail, kontrol), WebSocket real-time, dan JSON API. Ini adalah proyek production-ready yang bisa Anda kembangkan lebih lanjut.
Arsitektur Proyek
Source Code Utama
// Dashboard Sensor Multi-page - ESP32 Web Server
// BeebaneLabs - https://beebanelabs.pages.dev
// Menggunakan AsyncWebServer + WebSocket + ArduinoJson
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <ArduinoJson.h>
#include "DHT.h"
// === KONFIGURASI ===
const char* ssid = "NamaWiFiAnda";
const char* password = "PasswordWiFiAnda";
#define DHT_PIN 4
#define DHT_TYPE DHT22
#define LDR_PIN 34
#define RELAY_PIN 2
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");
DHT dht(DHT_PIN, DHT_TYPE);
// Data sensor global
struct SensorData {
float temperature;
float humidity;
int lightLevel;
bool relayState;
unsigned long lastUpdate;
} sensor;
// === HTML PAGES ===
// --- HOME PAGE ---
const char home_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dashboard IoT - Beranda</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:sans-serif; background:#0a0a14; color:#e0e0e0; }
nav { background:#141422; padding:1rem 2rem; display:flex;
justify-content:space-between; align-items:center;
border-bottom:1px solid #222; }
nav a { color:#ffeb00; text-decoration:none; margin:0 1rem;
font-weight:500; }
nav a.active { border-bottom:2px solid #ffeb00; padding-bottom:4px; }
.content { max-width:900px; margin:2rem auto; padding:0 1rem; }
h1 { color:#ffeb00; margin-bottom:1rem; }
.cards { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }
.card { background:#141422; padding:1.5rem; border-radius:12px;
border:1px solid #222; }
.card h3 { color:#888; font-size:0.85em; margin-bottom:0.5rem; }
.card .value { font-size:2.5em; font-weight:bold;
font-family:monospace; color:#ffeb00; }
.card .unit { font-size:0.5em; color:#666; }
.status-dot { width:8px; height:8px; border-radius:50%;
display:inline-block; background:#ef4444; }
.status-dot.on { background:#ffeb00; }
</style>
</head>
<body>
<nav>
<div><strong style="color:#ffeb00">⥠IoT Dashboard</strong></div>
<div>
<a href="/" class="active">Beranda</a>
<a href="/sensor">Sensor</a>
<a href="/control">Kontrol</a>
</div>
</nav>
<div class="content">
<h1>Selamat Datang di Dashboard IoT</h1>
<div class="cards">
<div class="card">
<h3>đĄī¸ SUHU</h3>
<div class="value" id="temp">--.-<span class="unit">°C</span></div>
</div>
<div class="card">
<h3>đ§ KELEMBABAN</h3>
<div class="value" id="hum">--.-<span class="unit">%</span></div>
</div>
<div class="card">
<h3>âī¸ CAHAYA</h3>
<div class="value" id="light">---<span class="unit"></span></div>
</div>
<div class="card">
<h3>đ RELAY</h3>
<div class="value"><span class="status-dot" id="relayDot"></span>
<span id="relayText">OFF</span></div>
</div>
</div>
<p style="margin-top:1rem;color:#666;text-align:center">
WebSocket: <span id="wsStatus">Menghubungkan...</span></p>
</div>
<script>
function connectWS() {
const ws = new WebSocket('ws://'+location.host+'/ws');
ws.onopen = () => {
document.getElementById('wsStatus').textContent = 'đĸ Terhubung';
};
ws.onclose = () => {
document.getElementById('wsStatus').textContent = 'đ´ Terputus';
setTimeout(connectWS, 2000);
};
ws.onmessage = (e) => {
const d = JSON.parse(e.data);
document.getElementById('temp').innerHTML =
d.temperature + '<span class="unit">°C</span>';
document.getElementById('hum').innerHTML =
d.humidity + '<span class="unit">%</span>';
document.getElementById('light').innerHTML = d.light;
const rd = document.getElementById('relayDot');
d.relay ? rd.classList.add('on') : rd.classList.remove('on');
document.getElementById('relayText').textContent =
d.relay ? 'ON' : 'OFF';
};
}
connectWS();
</script>
</body>
</html>
)rawliteral";
// --- SENSOR DETAIL PAGE ---
const char sensor_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Detail Sensor - Dashboard IoT</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:sans-serif; background:#0a0a14; color:#e0e0e0; }
nav { background:#141422; padding:1rem 2rem; display:flex;
justify-content:space-between; align-items:center;
border-bottom:1px solid #222; }
nav a { color:#ffeb00; text-decoration:none; margin:0 1rem;
font-weight:500; }
nav a.active { border-bottom:2px solid #ffeb00; padding-bottom:4px; }
.content { max-width:900px; margin:2rem auto; padding:0 1rem; }
h1 { color:#ffeb00; margin-bottom:1.5rem; }
table { width:100%; border-collapse:collapse; }
td, th { padding:0.75rem 1rem; text-align:left; border-bottom:1px solid #222; }
th { color:#888; font-size:0.85em; }
td { font-family:monospace; font-size:1.1em; }
.val { color:#ffeb00; font-weight:bold; }
</style>
</head>
<body>
<nav>
<div><strong style="color:#ffeb00">⥠IoT Dashboard</strong></div>
<div>
<a href="/">Beranda</a>
<a href="/sensor" class="active">Sensor</a>
<a href="/control">Kontrol</a>
</div>
</nav>
<div class="content">
<h1>đ Detail Sensor</h1>
<table>
<tr><th>Parameter</th><th>Nilai</th></tr>
<tr><td>Suhu (DHT22)</td><td class="val" id="sTemp">--.- °C</td></tr>
<tr><td>Kelembaban (DHT22)</td><td class="val" id="sHum">--.- %</td></tr>
<tr><td>Cahaya (LDR/ADC)</td><td class="val" id="sLight">---</td></tr>
<tr><td>Relay</td><td class="val" id="sRelay">OFF</td></tr>
<tr><td>Heap Bebas</td><td class="val" id="sHeap">--- bytes</td></tr>
<tr><td>WiFi RSSI</td><td class="val" id="sRSSI">--- dBm</td></tr>
<tr><td>Uptime</td><td class="val" id="sUptime">--:--:--</td></tr>
</table>
</div>
<script>
function connectWS() {
const ws = new WebSocket('ws://'+location.host+'/ws');
ws.onclose = () => setTimeout(connectWS, 2000);
ws.onmessage = (e) => {
const d = JSON.parse(e.data);
document.getElementById('sTemp').textContent = d.temperature+' °C';
document.getElementById('sHum').textContent = d.humidity+' %';
document.getElementById('sLight').textContent = d.light;
document.getElementById('sRelay').textContent = d.relay?'ON':'OFF';
document.getElementById('sHeap').textContent = d.freeHeap+' bytes';
document.getElementById('sRSSI').textContent = d.rssi+' dBm';
const s = Math.floor(d.uptime/1000);
const h = Math.floor(s/3600);
const m = Math.floor((s%3600)/60);
const sec = s%60;
document.getElementById('sUptime').textContent =
String(h).padStart(2,'0')+':'+String(m).padStart(2,'0')+':'+
String(sec).padStart(2,'0');
};
}
connectWS();
</script>
</body>
</html>
)rawliteral";
// --- CONTROL PAGE ---
const char control_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Kontrol - Dashboard IoT</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:sans-serif; background:#0a0a14; color:#e0e0e0; }
nav { background:#141422; padding:1rem 2rem; display:flex;
justify-content:space-between; align-items:center;
border-bottom:1px solid #222; }
nav a { color:#ffeb00; text-decoration:none; margin:0 1rem;
font-weight:500; }
nav a.active { border-bottom:2px solid #ffeb00; padding-bottom:4px; }
.content { max-width:500px; margin:2rem auto; padding:0 1rem; }
h1 { color:#ffeb00; margin-bottom:1.5rem; text-align:center; }
.ctrl-card { background:#141422; padding:2rem; border-radius:12px;
border:1px solid #222; text-align:center; margin-bottom:1rem; }
.toggle-btn {
display:inline-block; padding:1rem 3rem; font-size:1.5em;
font-weight:bold; border:none; border-radius:12px; cursor:pointer;
transition:all 0.2s;
}
.toggle-btn.on { background:#ffeb00; color:#111; }
.toggle-btn.off { background:#ef4444; color:#fff; }
.toggle-btn:hover { transform:scale(1.05); }
.api-link { color:#666; font-size:0.85em; margin-top:1rem; display:block; }
.api-link a { color:#ffeb00; }
</style>
</head>
<body>
<nav>
<div><strong style="color:#ffeb00">⥠IoT Dashboard</strong></div>
<div>
<a href="/">Beranda</a>
<a href="/sensor">Sensor</a>
<a href="/control" class="active">Kontrol</a>
</div>
</nav>
<div class="content">
<h1>đŽ Panel Kontrol</h1>
<div class="ctrl-card">
<h3 style="margin-bottom:1rem">Relay / LED</h3>
<button class="toggle-btn off" id="relayBtn"
onclick="toggleRelay()">OFF</button>
<span class="api-link">API: <a href="/api/relay?state=1">?state=1</a>
/ <a href="/api/relay?state=0">?state=0</a></span>
</div>
</div>
<script>
async function toggleRelay() {
const btn = document.getElementById('relayBtn');
const isOn = btn.classList.contains('on');
const newState = isOn ? 0 : 1;
await fetch('/api/relay?state=' + newState);
}
</script>
</body>
</html>
)rawliteral";
// === WEBSOCKET EVENT HANDLER ===
void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client,
AwsEventType type, void *arg, uint8_t *data, size_t len) {
if (type == WS_EVT_CONNECT) {
Serial.printf("[WS] Klien #%u terhubung\n", client->id());
} else if (type == WS_EVT_DISCONNECT) {
Serial.printf("[WS] Klien #%u terputus\n", client->id());
}
}
// === KIRIM DATA KE SEMUA KLIENT ===
void broadcastData() {
if (ws.count() == 0) return;
StaticJsonDocument<256> doc;
doc["temperature"] = serialized(
String(sensor.temperature, 1).c_str());
doc["humidity"] = serialized(
String(sensor.humidity, 1).c_str());
doc["light"] = sensor.lightLevel;
doc["relay"] = sensor.relayState;
doc["freeHeap"] = ESP.getFreeHeap();
doc["rssi"] = WiFi.RSSI();
doc["uptime"] = millis();
String json;
serializeJson(doc, json);
ws.textAll(json.c_str());
}
void setup() {
Serial.begin(115200);
dht.begin();
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, LOW);
// Inisialisasi data
sensor.relayState = false;
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) delay(500);
Serial.println("\nIP: " + WiFi.localIP().toString());
// WebSocket
ws.onEvent(onWsEvent);
server.addHandler(&ws);
// === ROUTES ===
server.on("/", HTTP_GET, [](AsyncWebServerRequest *req) {
req->send_P(200, "text/html", home_html);
});
server.on("/sensor", HTTP_GET, [](AsyncWebServerRequest *req) {
req->send_P(200, "text/html", sensor_html);
});
server.on("/control", HTTP_GET, [](AsyncWebServerRequest *req) {
req->send_P(200, "text/html", control_html);
});
// API: Semua data sensor
server.on("/api/data", HTTP_GET, [](AsyncWebServerRequest *req) {
StaticJsonDocument<256> doc;
doc["temperature"] = sensor.temperature;
doc["humidity"] = sensor.humidity;
doc["light"] = sensor.lightLevel;
doc["relay"] = sensor.relayState;
doc["uptime"] = millis() / 1000;
String json;
serializeJson(doc, json);
req->send(200, "application/json", json);
});
// API: Kontrol relay
server.on("/api/relay", HTTP_GET, [](AsyncWebServerRequest *req) {
if (req->hasParam("state")) {
int st = req->getParam("state")->value().toInt();
sensor.relayState = (st == 1);
digitalWrite(RELAY_PIN, sensor.relayState ? HIGH : LOW);
req->send(200, "application/json",
"{\"relay\":" + String(sensor.relayState ? "true" : "false") + "}");
} else {
req->send(400, "application/json",
"{\"error\":\"state parameter needed\"}");
}
});
server.begin();
Serial.println("Dashboard server dimulai!");
}
void loop() {
// Baca sensor setiap 2 detik
static unsigned long lastRead = 0;
if (millis() - lastRead >= 2000) {
float t = dht.readTemperature();
float h = dht.readHumidity();
if (!isnan(t) && !isnan(h)) {
sensor.temperature = t;
sensor.humidity = h;
}
sensor.lightLevel = analogRead(LDR_PIN);
sensor.lastUpdate = millis();
broadcastData();
lastRead = millis();
}
ws.cleanupClients();
}
Proyek ini bisa dikembangkan dengan menambahkan: (1) data logging ke SPIFFS/LittleFS untuk menyimpan histori sensor, (2) Chart.js untuk visualisasi grafik historis, (3) autentikasi sederhana untuk keamanan halaman kontrol, (4) NTP time sync untuk timestamp akurat, dan (5) OTA update untuk update firmware tanpa kabel USB.
9. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang web server di ESP32: