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:
# Clone ESPAsyncWebServer dan AsyncTCP ke folder Arduino/libraries cd ~/Arduino/libraries git clone https://github.com/me-no-dev/ESPAsyncWebServer.git git clone https://github.com/me-no-dev/AsyncTCP.git
Pastikan tidak ada konflik dengan library WebServer bawaan. Jika menggunakan Arduino IDE, hindari menginstall kedua library secara bersamaan dalam sketch yang sama karena bisa menyebabkan error kompilasi. Pilih salah satu sesuai kebutuhan proyek Anda.
3. Hello World Web Server
Mari mulai dengan contoh paling sederhana: ESP32 yang menampilkan halaman "Hello World" saat diakses dari browser. Kita akan menunjukkan kedua pendekatan.
Cara A: Menggunakan WebServer Bawaan
// Hello World Web Server - WebServer Bawaan ESP32
// IoTHub - https://iothub.id
#include <WiFi.h>
#include <WebServer.h>
// Konfigurasi WiFi
const char* ssid = "NamaWiFiAnda";
const char* password = "PasswordWiFiAnda";
// Buat instance server pada port 80 (HTTP default)
WebServer server(80);
// Handler untuk halaman utama
void handleRoot() {
String html = "<!DOCTYPE html>";
html += "<html lang='id'>";
html += "<head><meta charset='UTF-8'>";
html += "<title>Hello ESP32</title>";
html += "<style>";
html += "body{font-family:sans-serif;text-align:center;";
html += "margin-top:80px;background:#1a1a2e;color:#eee;}";
html += "h1{color:#00ff88;font-size:2.5em;}";
html += "</style></head>";
html += "<body>";
html += "<h1>Hello World dari ESP32!</h1>";
html += "<p>Web server berjalan pada port 80</p>";
html += "<p>Jam server: " + String(millis()/1000) + " detik</p>";
html += "</body></html>";
server.send(200, "text/html", html);
}
// Handler untuk halaman tidak ditemukan
void handleNotFound() {
server.send(404, "text/plain", "404: Halaman tidak ditemukan!");
}
void setup() {
Serial.begin(115200);
Serial.println("Memulai Web Server ESP32...");
// Koneksi WiFi
WiFi.begin(ssid, password);
Serial.print("Menghubungkan ke WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("WiFi terhubung! IP address: ");
Serial.println(WiFi.localIP());
// Daftarkan route handler
server.on("/", handleRoot);
server.onNotFound(handleNotFound);
// Mulai server
server.begin();
Serial.println("Web server dimulai!");
}
void loop() {
server.handleClient(); // Proses request dari klien
}
Cara B: Menggunakan ESPAsyncWebServer (Rekomendasi)
// Hello World Web Server - AsyncWebServer
// IoTHub - https://iothub.id
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
const char* ssid = "NamaWiFiAnda";
const char* password = "PasswordWiFiAnda";
AsyncWebServer server(80);
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<title>Hello ESP32 Async</title>
<style>
body {
font-family: 'Segoe UI', sans-serif;
text-align: center;
margin-top: 80px;
background: #0f0f1a;
color: #e0e0e0;
}
h1 { color: #3ecf8e; font-size: 2.5em; }
.uptime { font-size: 1.2em; color: #888; }
</style>
</head>
<body>
<h1>Hello World dari ESP32!</h1>
<p class="uptime">Server berjalan via AsyncWebServer</p>
</body>
</html>
)rawliteral";
void setup() {
Serial.begin(115200);
Serial.println("Memulai AsyncWebServer...");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi terhubung: " + WiFi.localIP().toString());
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send_P(200, "text/html", index_html);
});
server.begin();
Serial.println("AsyncWebServer dimulai!");
}
void loop() {
// Tidak perlu handleClient() di AsyncWebServer!
}
Setelah upload, buka Serial Monitor untuk melihat IP address ESP32 (misalnya 192.168.1.105). Kemudian buka browser di perangkat yang sama dengan jaringan WiFi, masukkan IP address tersebut di address bar. Anda akan melihat halaman "Hello World" yang dilayani oleh ESP32!
4. Serving HTML, CSS, JS dari ESP32
Untuk proyek web yang lebih serius, menulis seluruh HTML dalam string C++ bukanlah praktik terbaik. ESP32 mendukung beberapa pendekatan untuk menyajikan file statis:
Pendekatan 1: Inline HTML dengan PROGMEM
HTML, CSS, dan JavaScript ditulis langsung di dalam kode Arduino menggunakan PROGMEM (Program Memory) agar tidak memakan RAM. Cocok untuk halaman sederhana hingga sedang.
Pendekatan 2: SPIFFS / LittleFS
SPIFFS (SPI Flash File System) atau LittleFS memungkinkan Anda menyimpan file HTML, CSS, JS, dan gambar sebagai file terpisah di flash memory ESP32 (4MB). File di-upload menggunakan plugin Arduino IDE atau CLI.
// Serving Static Files dengan LittleFS
// IoTHub - https://iothub.id
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <LittleFS.h>
const char* ssid = "NamaWiFiAnda";
const char* password = "PasswordWiFiAnda";
AsyncWebServer server(80);
// CSS inline yang disajikan dari PROGMEM
const char style_css[] PROGMEM = R"rawliteral(
:root {
--bg: #0f0f1a;
--card: #1a1a2e;
--accent: #3ecf8e;
--text: #e0e0e0;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
text-align: center;
padding: 2rem;
background: var(--card);
border-radius: 16px;
border: 1px solid rgba(62, 207, 142, 0.2);
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
}
h1 { color: var(--accent); margin-bottom: 0.5rem; }
.status {
display: inline-block;
margin-top: 1rem;
padding: 0.5rem 1rem;
background: rgba(62, 207, 142, 0.15);
border-radius: 8px;
color: var(--accent);
}
)rawliteral";
// JavaScript untuk fetch status dari API
const char script_js[] PROGMEM = R"rawliteral(
async function getStatus() {
try {
const resp = await fetch('/api/status');
const data = await resp.json();
document.getElementById('uptime').textContent =
'Uptime: ' + data.uptime + ' detik';
document.getElementById('heap').textContent =
'Heap: ' + data.heap + ' bytes';
} catch(e) {
console.error('Gagal mengambil status:', e);
}
}
// Perbarui status setiap 2 detik
setInterval(getStatus, 2000);
getStatus();
)rawliteral";
void setup() {
Serial.begin(115200);
// Inisialisasi LittleFS
if (!LittleFS.begin(true)) {
Serial.println("[ERROR] LittleFS gagal di-mount!");
return;
}
Serial.println("LittleFS siap.");
// Koneksi WiFi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) delay(500);
Serial.println("\nIP: " + WiFi.localIP().toString());
// Sajikan CSS dari PROGMEM
server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *req) {
req->send_P(200, "text/css", style_css);
});
// Sajikan JavaScript dari PROGMEM
server.on("/app.js", HTTP_GET, [](AsyncWebServerRequest *req) {
req->send_P(200, "application/javascript", script_js);
});
// Sajikan file dari LittleFS (jika ada index.html)
server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html");
// API endpoint: status server
server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest *req) {
String json = "{\"uptime\":";
json += String(millis() / 1000);
json += ",\"heap\":";
json += String(ESP.getFreeHeap());
json += ",\"ip\":\"";
json += WiFi.localIP().toString();
json += "\"}";
req->send(200, "application/json", json);
});
server.begin();
}
void loop() {
// AsyncWebServer menangani semua di background
}
Untuk mengupload file ke LittleFS, buat folder data di dalam sketch folder Arduino Anda. Masukkan file HTML, CSS, dan JS ke dalamnya. Kemudian gunakan menu Tools โ ESP32 Sketch Data Upload (perlu plugin) atau gunakan CLI: python -m esptool --port COM3 write_flash 0x290000 littlefs.bin. Gunakan tools mkspiffs atau mkittlefs untuk membuat image file system.
5. Handling Form Input (LED ON/OFF, Slider)
Salah satu kekuatan utama web server di ESP32 adalah kemampuan mengontrol perangkat keras dari browser. Mari kita buat halaman web yang bisa mengontrol LED dan mengatur kecerahan menggunakan slider PWM.
Rangkaian Hardware
ESP32
โโโโโโโโโโโโโโโ
โ โ
โ 3.3V/5V โโโโโโโโโโโโโโ
โ โ โ
โ GPIO 2 โโโโโ[LED]โโโโค Anode (+)
โ โ [R] โ
โ GND โโโโโโโโโโโโโโค Cathode (-)
โ โ 220ฮฉ โ
โ GPIO 4 โโโโโ[BTN]โโโโค Push Button
โ โ โ โ
โ GND โโโโโโโโโดโโโโโ
โโโโโโโโโโโโโโโ
Catatan:
- LED anode ke GPIO 2, cathode ke GND via resistor 220ฮฉ
- Push button: satu sisi ke GPIO 4, sisi lain ke GND
(ESP32 GPIO 4 menggunakan internal pull-up)
// Web Server: Kontrol LED & Slider PWM
// IoTHub - https://iothub.id
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
const char* ssid = "NamaWiFiAnda";
const char* password = "PasswordWiFiAnda";
AsyncWebServer server(80);
#define LED_PIN 2
#define BUTTON_PIN 4
#define PWM_CHANNEL 0
#define PWM_FREQ 5000
#define PWM_RES 8
bool ledState = false;
int ledBrightness = 128;
// Halaman utama dengan form kontrol
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>Kontrol LED ESP32</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: sans-serif;
background: #0f0f1a;
color: #e0e0e0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.card {
background: #1a1a2e;
padding: 2rem;
border-radius: 16px;
border: 1px solid rgba(62, 207, 142, 0.2);
width: 350px;
text-align: center;
}
h1 { color: #3ecf8e; font-size: 1.5em; margin-bottom: 1rem; }
.btn-group { margin: 1.5rem 0; }
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
margin: 0 0.5rem;
transition: transform 0.15s;
}
.btn:hover { transform: scale(1.05); }
.btn-on { background: #3ecf8e; color: #111; }
.btn-off { background: #ef4444; color: #fff; }
.status {
margin-top: 1rem;
padding: 0.5rem;
background: rgba(62, 207, 142, 0.1);
border-radius: 8px;
}
.slider-group { margin-top: 1.5rem; }
.slider-group label { display: block; margin-bottom: 0.5rem; }
input[type="range"] {
width: 100%;
accent-color: #3ecf8e;
}
#brightness-val { color: #3ecf8e; font-weight: bold; }
</style>
</head>
<body>
<div class="card">
<h1>๐ก Kontrol LED ESP32</h1>
<div class="btn-group">
<button class="btn btn-on" onclick="setLED(1)">ON</button>
<button class="btn btn-off" onclick="setLED(0)">OFF</button>
</div>
<div class="slider-group">
<label>Kecerahan: <span id="brightness-val">128</span></label>
<input type="range" min="0" max="255" value="128"
oninput="setBrightness(this.value)">
</div>
<div class="status">
Status LED: <span id="led-status">OFF</span>
</div>
</div>
<script>
async function setLED(state) {
await fetch('/api/led?state=' + state);
document.getElementById('led-status').textContent =
state == 1 ? 'ON' : 'OFF';
}
async function setBrightness(val) {
document.getElementById('brightness-val').textContent = val;
await fetch('/api/brightness?value=' + val);
}
</script>
</body>
</html>
)rawliteral";
void setup() {
Serial.begin(115200);
// Setup LED PWM
ledcSetup(PWM_CHANNEL, PWM_FREQ, PWM_RES);
ledcAttachPin(LED_PIN, PWM_CHANNEL);
ledcWrite(PWM_CHANNEL, 0);
// Setup push button dengan internal pull-up
pinMode(BUTTON_PIN, INPUT_PULLUP);
// Koneksi WiFi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) delay(500);
Serial.println("\nIP: " + WiFi.localIP().toString());
// Route: halaman utama
server.on("/", HTTP_GET, [](AsyncWebServerRequest *req) {
req->send_P(200, "text/html", index_html);
});
// API: Kontrol ON/OFF LED
server.on("/api/led", HTTP_GET, [](AsyncWebServerRequest *req) {
if (req->hasParam("state")) {
String state = req->getParam("state")->value();
ledState = (state == "1");
digitalWrite(LED_PIN, ledState ? HIGH : LOW);
Serial.printf("LED: %s\n", ledState ? "ON" : "OFF");
req->send(200, "text/plain", ledState ? "ON" : "OFF");
} else {
req->send(400, "text/plain", "Parameter 'state' diperlukan");
}
});
// API: Atur kecerahan PWM
server.on("/api/brightness", HTTP_GET, [](AsyncWebServerRequest *req) {
if (req->hasParam("value")) {
int val = req->getParam("value")->value().toInt();
val = constrain(val, 0, 255);
ledcWrite(PWM_CHANNEL, val);
Serial.printf("Brightness: %d\n", val);
req->send(200, "text/plain", String(val));
} else {
req->send(400, "text/plain", "Parameter 'value' diperlukan");
}
});
server.begin();
}
void loop() {
// Cek tombol fisik sebagai fallback
if (digitalRead(BUTTON_PIN) == LOW) {
ledState = !ledState;
digitalWrite(LED_PIN, ledState ? HIGH : LOW);
Serial.println("Tombol ditekan! LED: " + String(ledState ? "ON" : "OFF"));
delay(300); // debounce
while (digitalRead(BUTTON_PIN) == LOW); // tunggu lepas
}
}
Ketika pengguna menekan tombol ON/OFF atau menggeser slider di browser, JavaScript akan mengirim request GET /api/led?state=1 atau GET /api/brightness?value=200 ke ESP32. Server kemudian mengontrol GPIO sesuai parameter yang diterima. Ini adalah pola dasar komunikasi browser โ ESP32 yang akan kita kembangkan lebih lanjut.
6. JSON API Endpoint
Dalam arsitektur IoT modern, pertukaran data biasanya dilakukan dalam format JSON (JavaScript Object Notation). ESP32 bisa berfungsi sebagai REST API server yang menyediakan data sensor dalam format JSON, sehingga bisa dikonsumsi oleh dashboard web, aplikasi mobile, atau layanan cloud.
Membangun API Sensor Suhu & Kelembaban
// JSON API Endpoint - ESP32 Web Server
// IoTHub - https://iothub.id
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <ArduinoJson.h>
#include "DHT.h"
const char* ssid = "NamaWiFiAnda";
const char* password = "PasswordWiFiAnda";
AsyncWebServer server(80);
DHT dht(4, DHT11);
// Data sensor global
float currentTemp = 0.0;
float currentHumidity = 0.0;
unsigned long lastRead = 0;
// Baca sensor setiap 2 detik
void readSensor() {
if (millis() - lastRead >= 2000) {
float t = dht.readTemperature();
float h = dht.readHumidity();
if (!isnan(t) && !isnan(h)) {
currentTemp = t;
currentHumidity = h;
}
lastRead = millis();
}
}
void setup() {
Serial.begin(115200);
dht.begin();
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) delay(500);
Serial.println("\nIP: " + WiFi.localIP().toString());
// === API ENDPOINTS ===
// Endpoint: GET /api/sensor
// Mengembalikan data sensor dalam JSON
server.on("/api/sensor", HTTP_GET,
[](AsyncWebServerRequest *req) {
StaticJsonDocument<200> doc;
doc["temperature"] = serialized(
String(currentTemp, 1).c_str());
doc["humidity"] = serialized(
String(currentHumidity, 1).c_str());
doc["unit_temp"] = "Celsius";
doc["unit_humidity"] = "percent";
doc["sensor"] = "DHT11";
doc["uptime_sec"] = millis() / 1000;
doc["free_heap"] = ESP.getFreeHeap();
String response;
serializeJson(doc, response);
req->send(200, "application/json", response);
}
);
// Endpoint: GET /api/device
// Informasi perangkat
server.on("/api/device", HTTP_GET,
[](AsyncWebServerRequest *req) {
StaticJsonDocument<300> doc;
doc["chip"] = ESP.getChipModel();
doc["cpu_freq_mhz"] = ESP.getCpuFreqMHz();
doc["flash_size"] = ESP.getFlashChipSize() / 1048576;
doc["free_heap"] = ESP.getFreeHeap();
doc["wifi_rssi"] = WiFi.RSSI();
doc["ip"] = WiFi.localIP().toString();
doc["mac"] = WiFi.macAddress();
String response;
serializeJson(doc, response);
req->send(200, "application/json", response);
}
);
// Halaman dokumentasi API (simple)
server.on("/", HTTP_GET,
[](AsyncWebServerRequest *req) {
String html = "<h1>ESP32 REST API</h1>";
html += "<ul>";
html += "<li><a href='/api/sensor'>/api/sensor</a> - Data sensor</li>";
html += "<li><a href='/api/device'>/api/device</a> - Info perangkat</li>";
html += "</ul>";
req->send(200, "text/html", html);
}
);
server.begin();
}
void loop() {
readSensor();
}
Contoh Response JSON
Ketika browser atau aplikasi mengakses GET /api/sensor, ESP32 akan mengembalikan response seperti berikut:
{
"temperature": "28.5",
"humidity": "72.0",
"unit_temp": "Celsius",
"unit_humidity": "percent",
"sensor": "DHT11",
"uptime_sec": 3621,
"free_heap": 245120
}
Library ArduinoJson memudahkan pembuatan dan parsing JSON di Arduino/ESP32. Gunakan StaticJsonDocument untuk ukuran tetap (hemat RAM) atau DynamicJsonDocument untuk ukuran dinamis. Ukuran document harus cukup besar untuk menampung seluruh JSON yang akan dibuat โ jika terlalu kecil, data akan terpotong.
7. WebSocket untuk Data Real-time
HTTP memiliki keterbatasan: client harus selalu mengirim request baru untuk mendapatkan data terbaru (polling). WebSocket menyelesaikan masalah ini dengan membuka koneksi dua-arah yang persisten antara browser dan ESP32. Setelah koneksi terbangun, ESP32 bisa mengirim data ke browser kapan saja tanpa diminta โ sempurna untuk dashboard real-time.
Perbandingan HTTP Polling vs WebSocket
HTTP Polling (Traditional): Browser โโGET /dataโโโบ ESP32 Browser โโโJSONโโโโโโ ESP32 Browser โโGET /dataโโโบ ESP32 (ulang terus...) Browser โโโJSONโโโโโโ 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
// IoTHub - https://iothub.id
#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: #3ecf8e; font-size: 1.3em; text-align: center; }
.value-display {
text-align: center;
font-size: 4em;
font-weight: bold;
color: #3ecf8e;
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: #3ecf8e; }
#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 = '#3ecf8e';
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
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ESP32 DevKit โ
โ โ
โ โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โ
โ โ DHT22 โ โ LDR โ โ Relay / LED โ โ
โ โ (GPIO 4) โ โ (GPIO 34)โ โ (GPIO 2) โ โ
โ โโโโโโฌโโโโโโ โโโโโโฌโโโโโโ โโโโโโโโฌโโโโโโโโ โ
โ โ โ โ โ
โ โโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโผโโโโโโโโโ โ
โ โ ESP32 Core (FreeRTOS) โ โ
โ โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ AsyncWebServer (Port 80) โ โ โ
โ โ โ โ โ โ
โ โ โ Routes: โ โ โ
โ โ โ GET / โ Home Page โ โ โ
โ โ โ GET /sensor โ Sensor Detail โ โ โ
โ โ โ GET /control โ Control Panel โ โ โ
โ โ โ WS /ws โ Real-time Data โ โ โ
โ โ โ GET /api/data โ JSON All Data โ โ โ
โ โ โ GET /api/relay โ Toggle Relay โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ WiFi โ โ WiFi โ
โผ โ โผ โ
โโโโโโโโโโ โ โโโโโโโโโโ โ
โ Browserโ โ โ Browserโ โ
โ PC โ โ โ Ponsel โ โ
โโโโโโโโโโ โ โโโโโโโโโโ
โ
WebSocket Connection
(real-time updates)
Source Code Utama
// Dashboard Sensor Multi-page - ESP32 Web Server
// IoTHub - https://iothub.id
// 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:#3ecf8e; text-decoration:none; margin:0 1rem;
font-weight:500; }
nav a.active { border-bottom:2px solid #3ecf8e; padding-bottom:4px; }
.content { max-width:900px; margin:2rem auto; padding:0 1rem; }
h1 { color:#3ecf8e; 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:#3ecf8e; }
.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:#3ecf8e; }
</style>
</head>
<body>
<nav>
<div><strong style="color:#3ecf8e">โก 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:#3ecf8e; text-decoration:none; margin:0 1rem;
font-weight:500; }
nav a.active { border-bottom:2px solid #3ecf8e; padding-bottom:4px; }
.content { max-width:900px; margin:2rem auto; padding:0 1rem; }
h1 { color:#3ecf8e; 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:#3ecf8e; font-weight:bold; }
</style>
</head>
<body>
<nav>
<div><strong style="color:#3ecf8e">โก 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:#3ecf8e; text-decoration:none; margin:0 1rem;
font-weight:500; }
nav a.active { border-bottom:2px solid #3ecf8e; padding-bottom:4px; }
.content { max-width:500px; margin:2rem auto; padding:0 1rem; }
h1 { color:#3ecf8e; 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:#3ecf8e; 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:#3ecf8e; }
</style>
</head>
<body>
<nav>
<div><strong style="color:#3ecf8e">โก 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: