â˜€ī¸Siang
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

Artikel: Web Server Esp32 Artikel: Web Server Esp32


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
TipeSynchronous (blokir)Asynchronous (non-blokir)
Multi-klienTerbatasBanyak klien simultan
WebSocketTidak didukung✅ Didukung penuh
EventSourceTidak didukung✅ Didukung
Simplicity✅ Sangat mudahSedikit lebih kompleks
RAM UsageLebih ringanSedikit lebih besar
Cocok untukProyek sederhana, few endpointDashboard real-time, banyak klien
LibraryBawaan ESP32 corePerlu 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
ESPAsyncWebServerme-no-dev (lacamera)Server web asynchronous utama
AsyncTCPme-no-devTCP asynchronous (dependency AsyncWebServer)
ArduinoJsonBenoit BlanchonPembuatan & parsing JSON
DHT sensor libraryAdafruitSupport untuk sensor DHT11/DHT22

Langkah 2: Instalasi via Library Manager di Arduino IDE 2.x

  1. Buka Arduino IDE, kemudian buka menu Tools → Manage Libraries
  2. Pada kolom pencarian, ketik "ESPAsyncWebServer"
  3. Pilih "ESPAsyncWebServer by lacamera", klik Install
  4. Jika diminta, konfirmasi instalasi dependency (AsyncTCP akan terinstall otomatis)
  5. 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

(LED) Anode (+)

Cathode (-)

(BTN) 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)"/
C++ — websocket_realtime.ino

// 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

Diagram

Relay / LED (GPIO 2)

DHT22 (GPIO 4)

LDR (GPIO 34)

Browser PC

Browser Ponsel

Source Code Utama

C++ — dashboard_multi_page.ino

// 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
🔍 Zoom
100%
🎨 Tema