ESP32 & ESP8266
Membangun Web Server di ESP32: Panduan Lengkap
TOKEN
Pelajari cara membuat ESP32 menjadi web server mandiri â dari hello world hingga dashboard sensor multi-halaman dengan WebSocket real-time
đ
26 Juni 2026
đ 18 menit baca
Menengah
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
đĄ Rekomendasi
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:
Diagram: Rangkaian ESP32 + LED + Push Button
Rangkaian ESP32 + LED + Push Button
Diagram: Perbandingan HTTP Polling vs WebSocket
/"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();
}
âšī¸ Keunggulan WebSocket
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();
}
đĄ Pengembangan Lebih Lanjut
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:
Pertanyaan 1: Perbedaan utama ESP32 WebServer (bawaan) dan ESPAsyncWebServer adalah...
a) WebServer lebih cepat dari AsyncWebServer
b) AsyncWebServer tidak mendukung HTTP GET
c) WebServer bersifat synchronous (blokir), AsyncWebServer bersifat asynchronous (non-blokir)
d) Keduanya sama persis dalam cara kerjanya
Pertanyaan 2: Library apa yang HARUS diinstal sebagai dependency untuk menggunakan ESPAsyncWebServer?
a) AsyncTCP
b) ESP8266WiFi
c) Ethernet
d) HTTPClient
Pertanyaan 3: Protokol WebSocket berbeda dari HTTP karena...
a) WebSocket hanya bisa mengirim data satu arah (server ke client)
b) WebSocket menggunakan port 443 (HTTPS)
c) WebSocket membuka koneksi dua-arah yang persisten tanpa perlu poll ulang
d) WebSocket tidak bisa digunakan di ESP32
Pertanyaan 4: Untuk menyimpan file HTML statis di flash memory ESP32 agar bisa dilayani oleh web server, sistem file yang digunakan adalah...
a) FAT32
b) NTFS
c) ext4
d) SPIFFS / LittleFS
Pertanyaan 5: Dalam contoh proyek dashboard multi-page, untuk mengirim data real-time dari ESP32 ke semua browser yang terhubung, method yang digunakan adalah...
a) Kirim HTTP POST ke semua klien
b) Menulis ke file SPIFFS lalu klien membacanya
c) Broadcast data menggunakan ws.textAll() melalui WebSocket
d) Menggunakan serial monitor sebagai antarmuka
đ Periksa Jawaban
đŦ Dapatkan Tutorial Terbaru
Subscribe untuk mendapatkan tutorial IoT terbaru langsung di inbox kamu. Gratis, tanpa spam.
â âī¸