Web Server Asinkron ESP32 dengan WebSocket: Panel Kontrol Real-Time
Web server asinkron ESP32 dengan WebSocket: nyalakan relay dan pantau data sensor secara langsung tanpa reload. Instalasi, PROGMEM vs LittleFS, esp32.local, kata sandi, dan solusi error.
Web server asinkron ESP32 (ESP32 async web server) yang dibangun dengan library ESPAsyncWebServer mengubah ESP32-mu menjadi web server cepat yang bisa melayani beberapa ponsel dan komputer sekaligus. Dengan dukungan WebSocket yang sudah ada di library itu, browser dan board menjaga satu jalur yang terus terbuka: tekan tombol di halaman dan relay langsung berbunyi klik, dan ketika suhu berubah, angka di halaman ikut diperbarui sendiri tanpa reload. Setiap perangkat lain yang membuka panel yang sama melihat perubahanmu dalam detik yang sama. Singkatnya, pasangan ini adalah cara paling praktis untuk membuat panel rumah pintarmu sendiri di dalam jaringan rumah, tanpa layanan cloud apa pun.
Di panduan ini kamu akan melihat dulu apa bedanya server asinkron dengan library WebServer klasik, dan kenapa WebSocket lebih cepat daripada request HTTP biasa. Setelah itu kamu akan menginstal library yang tepat, menjalankan server yang paling sederhana, lalu membuat panel kontrol real-time dengan relay dan sensor DHT22. Berikutnya kamu akan belajar menyimpan HTML di PROGMEM atau LittleFS, memakai esp32.local alih-alih alamat IP, menangani beberapa klien, memberi panel kata sandi sederhana, dan memperbaiki error kompilasi dan koneksi yang paling sering muncul. Kodenya ditulis agar berjalan di core ESP32 versi 2.x maupun 3.x di Arduino IDE.

Apa itu web server asinkron ESP32 dan bagaimana cara kerjanya?
Library WebServer.h yang ikut terpasang bersama core ESP32 bersifat sinkron: request yang masuk baru diproses, satu per satu, ketika server.handleClient() dipanggil di dalam loop(). Kalau loop() berisi delay() 2 detik, halaman juga menunggu 2 detik; kalau dua orang tersambung bersamaan, yang satu menunggu yang lain selesai. Sebaliknya, ESPAsyncWebServer berjalan di task FreeRTOS terpisah (async_tcp) yang dibuat oleh library AsyncTCP. Ketika request datang, header-nya diurai di latar belakang, fungsi yang kamu tulis (callback) dipanggil begitu request siap, dan respons juga dikirim di latar belakang. Sementara itu loop() tetap mengerjakan tugasnya sendiri. Dengan cara ini beberapa koneksi bisa terbuka sekaligus, dan koneksi yang selalu terbuka seperti WebSocket dan Server-Sent Events (SSE) menjadi mungkin. Kalau kamu penasaran dengan dua inti dan task di ESP32, lihat artikel FreeRTOS ESP32.
| Fitur | WebServer.h (sinkron) | ESPAsyncWebServer (asinkron) |
|---|---|---|
| Instalasi | Sudah ikut bersama core ESP32 | 2 library dari Library Manager |
| Penanganan request | Satu per satu lewat handleClient() di loop() | Di task async_tcp di latar belakang, beberapa sekaligus |
| delay() di dalam loop() | Menunda respons halaman | Tidak memengaruhi server |
| WebSocket | Tidak ada; perlu library terpisah (mis. library WebSockets karya Markus Sattler) | Bawaan (AsyncWebSocket) |
| Server-Sent Events | Tidak ada | Bawaan (AsyncEventSource) |
| Menyajikan file statis | Dengan serveStatic | serveStatic, dukungan .gz otomatis, dan pengaturan cache |
| Yang perlu diperhatikan | Paling mudah dipelajari | Tidak boleh ada delay() atau pekerjaan panjang di dalam callback |
Apa itu WebSocket dan apa bedanya dengan request HTTP?
Pada request HTTP biasa, browser membuka koneksi, berkata “berikan halaman ini”, menerima respons, lalu percakapan selesai. Server tidak bisa memberi tahu browser apa pun atas inisiatifnya sendiri; untuk melihat nilai baru, browser harus bertanya lagi. WebSocket (RFC 6455) adalah jabat tangan (handshake) yang dimulai sebagai HTTP: browser menambahkan header Upgrade: websocket ke request-nya, ESP32 menerima dengan respons 101 Switching Protocols, dan koneksi TCP yang sama tetap terbuka. Sejak itu kedua sisi bisa mengirim pesan kapan pun mereka mau (full-duplex). Setiap pesan hanya membawa overhead 2 sampai 14 byte, sedangkan setiap request HTTP membawa ratusan byte header. Karena itu pesan WebSocket tiba dalam hitungan milidetik, dan WebSocket menjadi metode paling efisien untuk panel yang mengirim beberapa pembaruan per detik. Begini perbandingan empat cara menjaga halaman tetap mutakhir dengan nilai sensor:
| Metode | Cara kerja | Latensi dan overhead | Kapan dipakai |
|---|---|---|---|
| Refresh halaman (meta refresh) | Browser mengunduh ulang seluruh halaman setiap beberapa detik | Tinggi; halaman berkedip setiap kali | Hindari |
| Polling dengan fetch | JavaScript meminta JSON kecil setiap 1–2 detik | Sedang; setiap request membawa header baru | Satu nilai yang jarang berubah |
| Server-Sent Events (SSE) | Server mendorong event satu arah lewat koneksi yang terbuka | Rendah; hanya dari server ke browser | Dasbor yang hanya untuk memantau |
| WebSocket | Pesan dua arah lewat koneksi yang terbuka | Paling rendah; frame berukuran beberapa byte | Tombol, slider, dan data langsung sekaligus |
Library yang dibutuhkan: menginstal ESPAsyncWebServer dan AsyncTCP
- Pastikan paket board ESP32 (Espressif Systems) sudah terinstal di Arduino IDE; langkah pengaturannya ada di Apa Itu ESP32?
- Buka Library Manager (Ctrl+Shift+I) dan cari ESPAsyncWebServer. Instal library ESP Async WebServer yang penulisnya ESP32Async.
- Library ini tidak mendeklarasikan dependensinya ke Library Manager, jadi IDE tidak akan menawarkan library tambahan: instal sendiri library Async TCP, yang juga diterbitkan oleh ESP32Async. Di ESP8266 kamu justru perlu library ESP Async TCP dari ESP32Async (ESPAsyncTCP.h).
- Untuk contoh panel, instal DHT sensor library dari Adafruit dan dependensi Adafruit Unified Sensor yang dimintanya. Kalau ini pertama kalinya kamu menginstal library, lihat panduan Cara Instal Library Arduino.
- Kalau folder Documents/Arduino/libraries-mu berisi folder ESPAsyncWebServer-master, AsyncTCP-master, atau ESPAsyncWebSrv yang dulu ditambahkan dari ZIP, hapus folder itu. Dua salinan library yang sama membuat compiler memilih yang salah.
Langkah pertama: server paling sederhana dengan AsyncWebServer
Sebelum masuk ke panel, mari lihat cara kerja library ini dengan contoh kecil. Kode di bawah menyambungkan ESP32 ke jaringan rumahmu, menampilkan sapaan di halaman utama, dan menyalakan serta mematikan LED bawaan lewat alamat /led?durum=1 dan /led?durum=0 (durum adalah bahasa Turki untuk “status”). Upload kodenya, buka Serial Monitor di 115200 baud, lalu ketik alamat yang ditampilkannya ke browser ponsel yang terhubung ke Wi-Fi yang sama. Kalau board tidak mau masuk ke jaringan, pakai kode diagnosis di panduan ESP32 tidak mau terhubung ke Wi-Fi.
1#include <WiFi.h>
2#include <AsyncTCP.h>
3#include <ESPAsyncWebServer.h>
4
5const char* ssid = "AgAdi"; // Wi-Fi adın / your Wi-Fi name
6const char* password = "Sifre123"; // Wi-Fi şifren / your Wi-Fi password
7const int LED_PIN = 2; // çoğu DevKit'te dahili LED / onboard LED on most DevKits
8
9AsyncWebServer server(80); // 80 = standart HTTP portu / standard HTTP port
10bool ledAcik = false;
11
12void setup() {
13 Serial.begin(115200);
14 pinMode(LED_PIN, OUTPUT);
15
16 WiFi.mode(WIFI_STA);
17 WiFi.begin(ssid, password);
18 while (WiFi.status() != WL_CONNECTED) {
19 delay(500);
20 Serial.print(".");
21 }
22 Serial.println();
23 Serial.print("Adres / address: http://");
24 Serial.println(WiFi.localIP());
25
26 // Ana sayfa / home page
27 server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
28 request->send(200, "text/plain", "Merhaba, ben ESP32 / Hello from ESP32");
29 });
30
31 // /led?durum=1 yakar, /led?durum=0 söndürür / turns the LED on or off
32 server.on("/led", HTTP_GET, [](AsyncWebServerRequest *request) {
33 if (request->hasParam("durum")) {
34 ledAcik = request->getParam("durum")->value() == "1";
35 digitalWrite(LED_PIN, ledAcik ? HIGH : LOW);
36 }
37 request->send(200, "text/plain", ledAcik ? "LED: ON" : "LED: OFF");
38 });
39
40 // Tanımsız adresler / unknown addresses
41 server.onNotFound([](AsyncWebServerRequest *request) {
42 request->send(404, "text/plain", "Sayfa yok / Not found");
43 });
44
45 server.begin();
46}
47
48void loop() {
49 // Sunucu arka planda çalışır, loop() boş kalabilir / the server runs in the background
50}| Perintah | Fungsinya |
|---|---|
| AsyncWebServer server(80) | Membuat server yang mendengarkan di port HTTP 80 |
| server.on("/led", HTTP_GET, ...) | Mendaftarkan fungsi (callback) yang dijalankan untuk suatu alamat dan jenis request |
| request->hasParam("durum") | Mengecek apakah alamatnya berisi parameter ?durum=... |
| request->getParam("durum")->value() (membaca nilai) | Mengembalikan nilai parameter sebagai String |
| request->send(kode, tipe, isi) | Mengirim respons: 200 OK, 404 tidak ditemukan; tipe text/plain, text/html, atau application/json |
| server.onNotFound(...) | Dijalankan untuk setiap alamat yang tidak didefinisikan |
| server.begin() | Menjalankan server; panggil setelah semua baris server.on() |
Komponen dan pengkabelan untuk panel kontrol
Di contoh panel kamu akan menyalakan dan mematikan lampu atau kipas dengan modul relay dan mengukur suhu serta kelembapan dengan DHT22. Kamu butuh ESP32 DevKit, modul relay 5 V satu kanal, modul sensor DHT22 (atau DHT11), breadboard, dan kabel jumper. Ketika memilih relay, utamakan modul dengan optocoupler. DHT22 mengukur dari −40 sampai 80 °C dengan akurasi ±0,5 °C dan paling cepat bisa dibaca sekali setiap 2 detik; DHT11 lebih murah tetapi akurasinya ±2 °C di rentang 0–50 °C. Untuk detail sensornya, lihat artikel sensor suhu dan kelembapan DHT11, dan untuk peta pin board-mu, alat Pinout.
| Pin modul | ESP32 DevKit | ESP8266 NodeMCU | Catatan |
|---|---|---|---|
| VCC relay | VIN (5 V) | VIN atau VU (5 V) | Kumparan relay butuh 5 V; dicatu dari USB |
| GND relay | GND | GND | Ground bersama |
| IN relay | GPIO 26 | D1 (GPIO 5) | Kebanyakan modul aktif dengan sinyal LOW |
| VCC DHT22 (+) | 3V3 | 3V3 | Dicatu dari 3,3 V, jalur datanya juga 3,3 V |
| DATA DHT22 (OUT) | GPIO 4 | D2 (GPIO 4) | Sensor tanpa modul: resistor 10 kΩ di antara DATA dan VCC |
| GND DHT22 (−) | GND | GND | Ground bersama |
Panel kontrol real-time: relay, DHT22, dan WebSocket
Sekarang proyek utamanya. Kode di bawah hanya satu file: HTML, CSS, dan JavaScript tinggal di dalamnya sebagai array PROGMEM. Ketika halaman terbuka, browser membuka koneksi WebSocket ke /ws. Setiap 2 detik ESP32 membaca DHT22 lalu mengirim suhu, kelembapan, status relay, dan jumlah perangkat yang tersambung ke setiap browser dalam satu pesan JSON. Ketika kamu menekan tombol, browser mengirim pesan toggle, ESP32 membalik relay, lalu kembali memberi tahu semua orang status barunya. Ganti nama jaringan dan kata sandinya, upload, lalu buka http://esp32.local atau alamat IP dari Serial Monitor di browsermu.
1#include <WiFi.h>
2#include <AsyncTCP.h>
3#include <ESPAsyncWebServer.h>
4#include <ESPmDNS.h>
5#include <DHT.h>
6
7const char* ssid = "AgAdi"; // Wi-Fi adın / your Wi-Fi name
8const char* password = "Sifre123"; // Wi-Fi şifren / your Wi-Fi password
9
10const int ROLE_PIN = 26; // röle modülü IN / relay module IN
11const bool ROLE_AKTIF_LOW = true; // çoğu modül LOW ile çeker / most modules switch on LOW
12const int DHT_PIN = 4; // DHT22 veri pini / DHT22 data pin
13
14DHT dht(DHT_PIN, DHT22); // DHT11 için DHT11 yaz / write DHT11 for a DHT11
15AsyncWebServer server(80);
16AsyncWebSocket ws("/ws"); // WebSocket adresi / WebSocket path
17
18bool roleAcik = false;
19float sicaklik = NAN;
20float nem = NAN;
21volatile bool gonder = false; // loop() yayın yapsın mı? / should loop() broadcast?
22unsigned long sonOlcum = 0;
23unsigned long sonTemizlik = 0;
24
25// Tarayıcıya giden sayfa / the page sent to the browser
26const char index_html[] PROGMEM = R"rawliteral(
27<!DOCTYPE html>
28<html lang="tr">
29<head>
30<meta charset="utf-8">
31<meta name="viewport" content="width=device-width, initial-scale=1">
32<title>ESP32 Panel</title>
33<style>
34body{font-family:sans-serif;background:#0f172a;color:#e2e8f0;text-align:center;margin:0;padding:16px}
35.kart{background:#1e293b;border-radius:12px;padding:16px;margin:12px auto;max-width:320px}
36.deger{font-size:2.4em;font-weight:bold}
37button{font-size:1.2em;padding:14px 32px;border:0;border-radius:8px;color:#fff;width:100%}
38.acik{background:#16a34a}.kapali{background:#475569}
39</style>
40</head>
41<body>
42<h2>ESP32 Panel</h2>
43<p id="baglanti">Bağlanıyor / Connecting...</p>
44<div class="kart">Sıcaklık / Temperature<div class="deger"><span id="sicaklik">--</span> °C</div></div>
45<div class="kart">Nem / Humidity<div class="deger"><span id="nem">--</span> %</div></div>
46<div class="kart">Röle / Relay<p><button id="dugme" class="kapali" onclick="degistir()">KAPALI / OFF</button></p></div>
47<p>Bağlı cihaz / Clients: <span id="istemci">0</span></p>
48<script>
49let ws;
50const $ = (id) => document.getElementById(id);
51function baglan() {
52 ws = new WebSocket('ws://' + location.host + '/ws');
53 ws.onopen = () => { $('baglanti').textContent = 'Bağlı / Connected'; };
54 ws.onclose = () => {
55 $('baglanti').textContent = 'Koptu, yeniden deneniyor / Reconnecting...';
56 setTimeout(baglan, 2000);
57 };
58 ws.onmessage = (e) => {
59 const d = JSON.parse(e.data);
60 $('sicaklik').textContent = d.sicaklik === null ? '--' : d.sicaklik.toFixed(1);
61 $('nem').textContent = d.nem === null ? '--' : d.nem.toFixed(0);
62 $('istemci').textContent = d.istemci;
63 $('dugme').textContent = d.role ? 'AÇIK / ON' : 'KAPALI / OFF';
64 $('dugme').className = d.role ? 'acik' : 'kapali';
65 };
66}
67function degistir() {
68 if (ws.readyState === WebSocket.OPEN) ws.send('toggle');
69}
70baglan();
71</script>
72</body>
73</html>
74)rawliteral";
75
76void roleAyarla(bool acik) {
77 roleAcik = acik;
78 digitalWrite(ROLE_PIN, ROLE_AKTIF_LOW ? !acik : acik);
79}
80
81// Durumu JSON metnine çevir / build the state as JSON text
82String durumJson() {
83 String s = "{\"role\":";
84 s += roleAcik ? "true" : "false";
85 s += ",\"sicaklik\":";
86 s += isnan(sicaklik) ? String("null") : String(sicaklik, 1);
87 s += ",\"nem\":";
88 s += isnan(nem) ? String("null") : String(nem, 0);
89 s += ",\"istemci\":";
90 s += (unsigned long)ws.count();
91 s += "}";
92 return s;
93}
94
95// WebSocket olayları (async_tcp görevinde çalışır) / WebSocket events (run in the async_tcp task)
96void wsOlay(AsyncWebSocket *sunucu, AsyncWebSocketClient *client, AwsEventType type,
97 void *arg, uint8_t *data, size_t len) {
98 switch (type) {
99 case WS_EVT_CONNECT:
100 Serial.printf("Istemci #%u baglandi / connected: %s\n",
101 (unsigned)client->id(), client->remoteIP().toString().c_str());
102 gonder = true; // yeni gelen güncel durumu alsın / newcomer gets the state
103 break;
104 case WS_EVT_DISCONNECT:
105 Serial.printf("Istemci #%u ayrildi / disconnected\n", (unsigned)client->id());
106 gonder = true;
107 break;
108 case WS_EVT_DATA: {
109 AwsFrameInfo *info = (AwsFrameInfo *)arg;
110 // Tek parça, tam bir metin mesajı mı? / a complete single-frame text message?
111 if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
112 if (len == 6 && memcmp(data, "toggle", 6) == 0) {
113 roleAyarla(!roleAcik);
114 gonder = true; // tüm ekranlar eşitlensin / sync every screen
115 }
116 }
117 break;
118 }
119 default: // PONG, ERROR vb. / PONG, ERROR etc.
120 break;
121 }
122}
123
124void setup() {
125 Serial.begin(115200);
126 pinMode(ROLE_PIN, OUTPUT);
127 roleAyarla(false); // açılışta röle kapalı / relay off at start
128 dht.begin();
129
130 WiFi.mode(WIFI_STA);
131 WiFi.begin(ssid, password);
132 Serial.print("Baglaniyor / connecting");
133 while (WiFi.status() != WL_CONNECTED) {
134 delay(500);
135 Serial.print(".");
136 }
137 WiFi.setSleep(false); // daha hızlı tepki / lower latency
138 Serial.println();
139 Serial.print("http://");
140 Serial.println(WiFi.localIP());
141
142 if (MDNS.begin("esp32")) { // http://esp32.local
143 MDNS.addService("http", "tcp", 80);
144 Serial.println("http://esp32.local");
145 }
146
147 ws.onEvent(wsOlay);
148 server.addHandler(&ws);
149
150 server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
151 request->send(200, "text/html", index_html);
152 });
153 server.onNotFound([](AsyncWebServerRequest *request) {
154 request->send(404, "text/plain", "Sayfa yok / Not found");
155 });
156 server.begin();
157}
158
159void loop() {
160 if (millis() - sonOlcum >= 2000) { // DHT22 en fazla 2 sn'de bir / DHT22: once per 2 s
161 sonOlcum = millis();
162 float t = dht.readTemperature();
163 float h = dht.readHumidity();
164 if (!isnan(t) && !isnan(h)) { // hatalı okumayı atla / skip failed reads
165 sicaklik = t;
166 nem = h;
167 }
168 gonder = true;
169 }
170
171 if (gonder) {
172 gonder = false;
173 ws.textAll(durumJson()); // tüm tarayıcılara yolla / send to every browser
174 }
175
176 if (millis() - sonTemizlik >= 1000) {
177 sonTemizlik = millis();
178 ws.cleanupClients(); // kopuk istemcileri temizle / drop stale clients
179 }
180}Bagaimana kode ini bekerja?
- AsyncWebSocket ws("/ws") membuat endpoint WebSocket dan server.addHandler(&ws) memasangnya ke server. Alamat di JavaScript harus sama persis dengan path ini.
- Fungsi wsOlay() (olay adalah bahasa Turki untuk “event”) dipanggil di setiap koneksi, pemutusan, dan pesan masuk. WS_EVT_CONNECT berarti klien baru, WS_EVT_DISCONNECT klien yang pergi, dan WS_EVT_DATA pesan yang masuk.
- AwsFrameInfo membawa informasi tentang frame yang masuk. Pengecekan final, index == 0, dan len memastikan pesannya tiba utuh dalam satu frame, dan WS_TEXT memastikan isinya teks. Perintah pendek selalu muat dalam satu frame.
- Data yang masuk dibandingkan bersama panjangnya dengan memcmp(), jadi kamu tidak bergantung pada ada atau tidaknya karakter null di akhir data.
- Karena callback berjalan di task async_tcp, callback hanya membalik relay dan menaikkan flag gonder (bahasa Turki untuk “kirim”). Pengiriman yang sebenarnya terjadi di loop() dengan ws.textAll(), jadi setiap pesan keluar dari satu tempat, secara berurutan.
- ws.cleanupClients() dipanggil sekali per detik: fungsi ini membebaskan memori koneksi yang terputus dan menutup koneksi tertua ketika jumlah klien melebihi batas.
- Di sisi JavaScript, event onclose menyambung ulang setelah 2 detik. Bahkan kalau ESP32 restart atau Wi-Fi putus sebentar, halaman pulih dengan sendirinya. Karena alamatnya dibentuk dari location.host, halaman bekerja baik dengan IP maupun dengan esp32.local.
- WiFi.setSleep(false) mematikan penghematan daya Wi-Fi. Modem sleep bawaan bisa menambah jeda sampai beberapa ratus milidetik pada respons; mematikannya di panel yang dicatu listrik dari stopkontak membuat tombol terasa instan.
Memilih JSON sebagai format pesan membuat semuanya mudah: browser mengubah teks yang masuk menjadi objek dalam satu baris dengan JSON.parse(). Contoh pesannya seperti ini: {"role":true,"sicaklik":23.4,"nem":48,"istemci":2} (kuncinya bahasa Turki: role = relay, sicaklik = suhu, nem = kelembapan, istemci = klien). Ketika sensor tidak bisa dibaca, nilainya dikirim sebagai null dan halaman menampilkan “--”; kalau pembacaan yang gagal dikirim sebagai NaN, JSON yang tidak valid akan membuat halaman berhenti diperbarui sama sekali. Karena setiap pesan hanya sekitar 60 byte, beberapa pembaruan per detik pun tidak membebani jaringan. Untuk pesan yang lebih rumit kamu bisa beralih ke library ArduinoJson, tetapi untuk panel seukuran ini, menggabungkan String sudah lebih dari cukup. Kamu juga bisa mencoba kode yang sama tanpa diubah dengan LED atau buzzer sebagai pengganti relay.
Di mana HTML sebaiknya disimpan: PROGMEM atau LittleFS?
Di kode panel, halamannya berada di dalam raw string literal yang ditulis sebagai R"rawliteral( ... )rawliteral". Penulisan ini memungkinkan kamu memakai tanda kutip dan baris baru tanpa karakter escape. Di Arduino berbasis AVR, PROGMEM dibutuhkan untuk menyimpan teks di flash, bukan di RAM; di ESP32, array konstan memang dibaca dari flash, jadi PROGMEM hanya ditulis demi kompatibilitas dan tidak merugikan. Ketika halaman makin besar, mengedit HTML di dalam kode jadi merepotkan; pada titik itu lebih rapi memindahkan file ke sistem file LittleFS di flash.
| Kriteria | PROGMEM (di dalam kode) | LittleFS (sistem file) |
|---|---|---|
| Persiapan | Tanpa langkah tambahan, satu file | Perlu folder data dan plugin upload |
| Mengedit | HTML di dalam array C++ | File index.html, style.css, dan app.js terpisah |
| Memperbarui | Kamu meng-upload ulang sketch setiap ada perubahan | Kamu hanya meng-upload ulang sistem file |
| Ukuran | Ideal untuk halaman beberapa KB | Ruang sekitar 1,5 MB dengan partisi default |
| Gambar dan font | Tidak praktis | Ikon, gambar, dan file .gz bisa disajikan |
| Kapan? | Uji coba pertama dan panel kecil | Antarmuka multi-halaman atau yang terus berkembang |
Menyajikan file HTML, CSS, dan JavaScript dengan LittleFS
- Di dalam folder sketch (folder yang berisi file .ino), buat folder bernama data dan simpan HTML yang ada di antara R"rawliteral( dan )rawliteral" di kode panel sebagai data/index.html. Kalau mau, kamu bisa memisahkan CSS dan JavaScript ke file tersendiri.
- Arduino IDE 2 tidak mendukung menu ESP32 Sketch Data Upload yang lama. Sebagai gantinya, unduh file .vsix dari plugin arduino-littlefs-upload dan taruh di C:\Users\(nama penggunamu)\.arduinoIDE\plugins di Windows atau ~/.arduinoIDE/plugins di macOS dan Linux (buat foldernya kalau belum ada), lalu restart IDE. Plugin ini butuh IDE 2.2.1 atau yang lebih baru.
- Tutup Serial Monitor kalau sedang terbuka, lalu buka command palette dengan Ctrl+Shift+P (Cmd+Shift+P di macOS) dan pilih Upload LittleFS to Pico/ESP8266/ESP32. File-file ditulis ke partisi sistem file di flash.
- Pastikan skema partisi yang punya partisi sistem file sudah dipilih di Tools > Partition Scheme (misalnya Default 4MB with spiffs). LittleFS memakai partisi ini, yang bernama spiffs.
- Upload kode di bawah seperti biasa. Selama opsi Erase All Flash di menu Tools nonaktif, meng-upload sketch tidak menghapus sistem file; ketika kamu mengubah file-filenya, cukup ulangi langkah 3.
1#include <WiFi.h>
2#include <AsyncTCP.h>
3#include <ESPAsyncWebServer.h>
4#include <LittleFS.h>
5
6const char* ssid = "AgAdi"; // Wi-Fi adın / your Wi-Fi name
7const char* password = "Sifre123"; // Wi-Fi şifren / your Wi-Fi password
8
9AsyncWebServer server(80);
10
11void setup() {
12 Serial.begin(115200);
13
14 if (!LittleFS.begin(true)) { // true: bağlanamazsa biçimlendir / format if mount fails
15 Serial.println("LittleFS hatasi / LittleFS error");
16 return;
17 }
18
19 WiFi.mode(WIFI_STA);
20 WiFi.begin(ssid, password);
21 while (WiFi.status() != WL_CONNECTED) {
22 delay(500);
23 }
24 Serial.println(WiFi.localIP());
25
26 // data/ klasörünün içeriğini sun / serve the contents of the data/ folder
27 server.serveStatic("/", LittleFS, "/")
28 .setDefaultFile("index.html") // varsayılan index.htm'dir / default is index.htm
29 .setCacheControl("max-age=600"); // tarayıcı 10 dk önbelleğe alsın / cache for 10 min
30
31 server.onNotFound([](AsyncWebServerRequest *request) {
32 request->send(404, "text/plain", "Sayfa yok / Not found");
33 });
34 server.begin();
35}
36
37void loop() {
38}mDNS: esp32.local alih-alih alamat IP
Router-mu bisa memberi ESP32 alamat IP yang berbeda setiap kali menyala, dan angka seperti 192.168.1.47 sulit diingat. mDNS (multicast DNS) memungkinkan sebuah perangkat mengumumkan namanya sendiri di jaringan lokal. Berkat baris MDNS.begin("esp32") di kode panel, perangkat di jaringan yang sama bisa membuka halaman di http://esp32.local, dan MDNS.addService("http", "tcp", 80) memberi tahu aplikasi pemindai jaringan bahwa di sini ada web server. Jangan tambahkan “.local” ke namanya, dan pakai hanya huruf kecil, angka, dan tanda hubung. Kalau kamu memakai beberapa ESP32, beri masing-masing nama yang berbeda (dapur, ruang-tamu, dan seterusnya), karena dua perangkat dengan nama yang sama saling mengganggu. mDNS tidak didukung sama baiknya di semua sistem:
| Sistem | Dukungan .local | Kalau tidak berhasil |
|---|---|---|
| iPhone, iPad, macOS | Bawaan (Bonjour) | Pastikan kamu berada di jaringan Wi-Fi yang sama |
| Windows 10 dan 11 | Biasanya berfungsi di versi terbaru | Instal layanan Bonjour atau pakai IP |
| Linux | Berfungsi kalau avahi-daemon terinstal | Instal paket avahi-daemon dan nss-mdns |
| Android | Tergantung versi dan browser | Pakai alamat IP atau atur reservasi DHCP di router |
Banyak klien: ponsel, tablet, dan komputer sekaligus
Yang paling disukai dari WebSocket adalah semua orang yang membuka panel melihat status yang sama. Ketika kamu menyalakan relay dari ponsel, ESP32 mengirim status baru ke setiap koneksi dengan textAll(), dan tab di komputermu berubah hijau pada momen yang sama. Setiap tab adalah klien tersendiri: buka tiga tab di browser yang sama dan penghitungnya menunjukkan 3. Secara default library ini mengizinkan 8 klien WebSocket bersamaan di ESP32 dan 4 di ESP8266 (DEFAULT_MAX_WS_CLIENTS). Batas ini tidak diterapkan otomatis; ketika cleanupClients() dipanggil, koneksi tertua yang melebihi batas ditutup. Berikut hal lain yang perlu kamu ingat:
- Ketika ponsel masuk mode tidur: ponsel yang layarnya mati mungkin tidak menutup koneksi dengan benar. Koneksi setengah terbuka seperti ini lama-lama dibersihkan berkat cleanupClients() dan pengiriman data yang rutin.
- Kecepatan kirim: untuk setiap klien, maksimal 32 pesan bisa menunggu di antrean (8 di ESP8266). Kalau kamu memanggil textAll() di setiap putaran loop(), antreannya penuh dan pesan baru dibuang diam-diam; atur Tools > Core Debug Level ke Warn atau level yang lebih detail, dan kamu akan melihat peringatan Too many messages queued di Serial Monitor. Kirim nilai hanya ketika berubah, atau beberapa kali per detik.
- Klien yang baru tersambung: kirim status terkini ke setiap halaman begitu halaman itu tersambung; kalau tidak, pengguna menatap panel kosong sampai pembacaan pertama. Di kode panel, flag yang dinaikkan di WS_EVT_CONNECT yang mengerjakan ini.
- Membalas satu klien saja: untuk menjawab hanya browser yang bertanya, bukan semua orang, pakai client->text("..."); client->id() memberimu nomor klien itu.
- Memori: setiap koneksi yang terbuka memakai satu socket TCP dan memori buffer. RAM ESP32 sebesar 520 KB lebih dari cukup untuk beberapa klien, tetapi mencetak memori kosong sesekali dengan ESP.getFreeHeap() membantumu menangkap kebocoran lebih awal.
Proteksi kata sandi sederhana
Siapa pun di jaringan rumahmu yang tahu alamat panel bisa menyalakan dan mematikan relay. Dengan fungsi request->authenticate() dan request->requestAuthentication() dari library ini, kamu bisa memasang nama pengguna dan kata sandi di halaman; browser membuka dialog login-nya sendiri dan mengingat datanya selama sesi. Metode default-nya adalah autentikasi Digest, artinya kata sandi tidak dikirim lewat jaringan sebagai teks biasa. Namun melindungi halaman saja tidak cukup: alamat WebSocket adalah pintu terpisah, dan seseorang bisa tersambung langsung ke ws://ip/ws. Contoh di bawah juga menutup pintu itu: setiap kali boot, ESP32 membuat kunci acak, memberikannya hanya ke browser yang sudah login lewat /anahtar (bahasa Turki untuk “kunci”), dan memakai setFilter() untuk menolak koneksi WebSocket yang tidak membawa kunci yang benar.
1#include <WiFi.h>
2#include <AsyncTCP.h>
3#include <ESPAsyncWebServer.h>
4
5const char* ssid = "AgAdi"; // Wi-Fi adın / your Wi-Fi name
6const char* password = "Sifre123"; // Wi-Fi şifren / your Wi-Fi password
7const char* KULLANICI = "admin"; // panel kullanıcı adı / panel username
8const char* PAROLA = "Gizli-2026"; // panel parolası / panel password
9const int LED_PIN = 2;
10
11AsyncWebServer server(80);
12AsyncWebSocket ws("/ws");
13String wsAnahtar; // her açılışta yeni anahtar / a new key on every boot
14bool ledAcik = false;
15
16const char index_html[] PROGMEM = R"rawliteral(
17<!DOCTYPE html>
18<html><head><meta charset="utf-8">
19<meta name="viewport" content="width=device-width, initial-scale=1"></head>
20<body style="font-family:sans-serif;text-align:center">
21<h2>Korumali panel / Protected panel</h2>
22<button style="font-size:1.4em;padding:12px 32px" onclick="ws && ws.send('toggle')">LED</button>
23<p id="durum">...</p>
24<script>
25let ws;
26fetch('/anahtar').then(r => r.text()).then(k => {
27 ws = new WebSocket('ws://' + location.host + '/ws?k=' + k);
28 ws.onmessage = (e) => { document.getElementById('durum').textContent = e.data; };
29});
30</script>
31</body></html>
32)rawliteral";
33
34bool girisVar(AsyncWebServerRequest *request) {
35 return request->authenticate(KULLANICI, PAROLA);
36}
37
38void setup() {
39 Serial.begin(115200);
40 pinMode(LED_PIN, OUTPUT);
41
42 WiFi.mode(WIFI_STA);
43 WiFi.begin(ssid, password);
44 while (WiFi.status() != WL_CONNECTED) {
45 delay(500);
46 }
47 Serial.println(WiFi.localIP());
48
49 // Wi-Fi açıkken random() donanım üretecini kullanır / random() uses the hardware RNG
50 wsAnahtar = String(random(0x7FFFFFFF), HEX) + String(random(0x7FFFFFFF), HEX);
51
52 // Sayfa: tarayıcı kullanıcı adı ve parola sorar / page: the browser asks for login
53 server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
54 if (!girisVar(request)) return request->requestAuthentication();
55 request->send(200, "text/html", index_html);
56 });
57
58 // Anahtar yalnız giriş yapana verilir / the key is given only after login
59 server.on("/anahtar", HTTP_GET, [](AsyncWebServerRequest *request) {
60 if (!girisVar(request)) return request->requestAuthentication();
61 request->send(200, "text/plain", wsAnahtar);
62 });
63
64 // Doğru anahtarı getirmeyen bağlantıyı reddet / reject sockets without the right key
65 ws.setFilter([](AsyncWebServerRequest *request) {
66 return request->hasParam("k") && request->getParam("k")->value() == wsAnahtar;
67 });
68
69 ws.onEvent([](AsyncWebSocket *sunucu, AsyncWebSocketClient *client, AwsEventType type,
70 void *arg, uint8_t *data, size_t len) {
71 if (type == WS_EVT_DATA && len == 6 && memcmp(data, "toggle", 6) == 0) {
72 ledAcik = !ledAcik;
73 digitalWrite(LED_PIN, ledAcik ? HIGH : LOW);
74 sunucu->textAll(ledAcik ? "LED: ON" : "LED: OFF");
75 }
76 });
77
78 server.addHandler(&ws);
79 server.begin();
80}
81
82void loop() {
83 ws.cleanupClients(); // saniyede bir yeterli / once per second is enough
84 delay(1000);
85}Catatan untuk ESP8266 dan Raspberry Pi Pico W
Library yang sama juga berjalan di ESP8266 (NodeMCU, Wemos D1 mini). Yang perlu diubah: instal library ESP Async TCP dari ESP32Async alih-alih Async TCP; sertakan ESP8266WiFi.h alih-alih WiFi.h, ESPAsyncTCP.h alih-alih AsyncTCP.h, dan ESP8266mDNS.h alih-alih ESPmDNS.h, lalu tambahkan baris MDNS.update() di dalam loop(). Ganti WiFi.setSleep(false) dengan WiFi.setSleepMode(WIFI_NONE_SLEEP) dan atur pin sesuai kolom NodeMCU di tabel pengkabelan. ESP8266 hanya punya sekitar 80 KB RAM yang bisa dipakai, jadi buat halamannya tetap kecil dan ingat bahwa batas kliennya 4. Kalau kamu memakai Raspberry Pi Pico W, cara membuat web server dengan MicroPython ada di panduan Wi-Fi dan web server Pico W; versi ESPAsyncWebServer yang lebih baru juga mendukung chip RP2040 dan RP2350 di sisi Arduino.
Ide proyek web server ESP32
- Stopkontak pintar multi-kanal: pakai board relay 4 kanal untuk membuat tombol terpisah bagi lampu, kipas, dan stopkontak; bedakan perintahnya dengan teks pendek seperti “r1” dan “r2”. Untuk memilih relay, lihat artikel modul relay.
- Panel penyiraman tanaman: tampilkan nilai sensor kelembapan tanah secara langsung, atur ambang penyiraman dengan slider di halaman, dan jalankan pompa secara manual kapan pun kamu mau.
- Stasiun cuaca: pakai BME280 sebagai pengganti DHT22 untuk menambahkan tekanan udara, dan tampilkan 60 pembacaan terakhir dalam grafik garis kecil di halaman.
- Lampu dengan pemilih warna: kirim nilai pemilih warna di halaman lewat WebSocket dan warnai LED strip WS2812B NeoPixel seketika.
- Servo dengan slider: kirim nilai slider di setiap gerakan dan putar motor servo; ideal untuk mengarahkan kamera atau kait pintu.
- Pencatatan dengan cap waktu: tambahkan waktu yang diambil lewat NTP ke setiap pembacaan, dan perbarui kodemu tanpa kabel lewat OTA, bukan lewat USB setiap kali.
Error umum dan cara memperbaikinya
| Gejala | Penyebab | Solusi |
|---|---|---|
| “'mbedtls_md5_starts_ret' was not declared in this scope” | Library lama me-no-dev dengan core ESP32 3.x | Instal ESP Async WebServer dan Async TCP dari ESP32Async, hapus folder lama |
| “Multiple libraries were found for "AsyncTCP.h"” | Ada dua salinan library yang sama | Hapus salinan -master yang lama di folder libraries |
| Peringatan deprecated untuk send_P | send_P sedang dihapus di versi baru | Pakai request->send(200, "text/html", index_html) |
| Halaman sama sekali tidak terbuka | Ponsel di jaringan tamu atau data seluler, atau alamat IP berubah | Masuk ke Wi-Fi yang sama dan pakai IP terkini dari Serial Monitor |
| Halaman terbuka tetapi nilainya tetap -- | WebSocket gagal tersambung: path berbeda atau addHandler terlupa | Buka console F12 dan samakan ws("/ws") dengan alamat di JavaScript |
| “Task watchdog got triggered” dengan async_tcp | delay(), perulangan panjang, atau request HTTP di dalam callback | Buat callback tetap singkat dan pindahkan pekerjaannya ke loop() dengan flag |
| “Too many messages queued” | textAll() dipanggil terlalu sering | Kirim hanya ketika berubah, atau beberapa kali per detik |
| Halaman LittleFS memberi 404 | Folder data belum di-upload atau setDefaultFile tidak ada | Upload file dengan plugin dan tambahkan setDefaultFile("index.html") |
| “does not exist, no permits for creation” di Serial Monitor | File yang diminta tidak ada di LittleFS | Cek nama file serta huruf besar-kecilnya; baris yang menyebut .gz tidak berbahaya |
| Relay tidak pernah mati | Modul 5 V tidak mengenali HIGH 3,3 V | Pakai modul yang kompatibel 3,3 V, JD-VCC, atau transistor |
| ESP32 reset ketika relay berbunyi klik | Arus kumparan menjatuhkan tegangan catu (brownout) | Pakai adaptor 5 V yang andal dan kapasitor 470 µF |
| esp32.local tidak bisa dibuka | Perangkat tidak mendukung mDNS atau namanya ditulis dengan .local | Tulis MDNS.begin("esp32") dan pakai alamat IP kalau perlu |
| Tombol bereaksi terlambat | Modem sleep Wi-Fi menyala | Panggil WiFi.setSleep(false) setelah tersambung |
Kombinasi web server asinkron ESP32 dan WebSocket adalah jalan terpendek menuju panel kontrol cepat yang berjalan tanpa layanan cloud atau menulis aplikasi ponsel, dan bisa dipakai bersama oleh beberapa perangkat. Mulailah dengan panel PROGMEM satu file, pindah ke LittleFS ketika antarmukanya makin besar, dan beri setiap perangkat baru di jaringanmu nama mDNS sendiri. Kalau kamu baru mengenal ESP32, dasar-dasarnya ada di Apa Itu ESP32? Di aplikasi BlueGrays kamu juga bisa menjelajahi proyek ESP32 seperti Web Server Asinkron ESP32, Penghitung WebSocket ESP32, akses lewat mDNS, jam NTP, dan pembaruan OTA langkah demi langkah, lengkap dengan skema rangkaian dan kode siap pakai.
Pertanyaan umum
Apa bedanya ESPAsyncWebServer dan WebServer di ESP32?
Library WebServer memproses request satu per satu lewat panggilan handleClient() di loop(); kalau loop() melambat, halaman ikut melambat. ESPAsyncWebServer memproses request di task latar belakang yang terpisah, sanggup menangani beberapa koneksi sekaligus, dan sudah membawa dukungan WebSocket dan SSE. Untuk panel real-time, versi asinkron lebih cocok.
Berapa banyak klien yang bisa tersambung ke web server ESP32 sekaligus?
Secara default ESPAsyncWebServer mengizinkan 8 klien WebSocket di ESP32 dan menutup kelebihannya ketika cleanupClients() dipanggil; di ESP8266 batasnya 4. Karena request halaman biasa berlangsung singkat, lebih banyak perangkat bisa membuka halaman secara bergantian. Dalam praktiknya, 3–5 perangkat bersamaan berjalan lancar di panel rumah.
Bisakah aku mengakses web server ESP32 dari internet?
Secara teknis bisa dengan port forwarding di router, tetapi tidak disarankan: library ini tidak menyediakan HTTPS di ESP32, dan perangkatnya tetap terbuka terhadap serangan dari internet. Untuk kendali jarak jauh, bot Telegram, sistem berbasis MQTT, atau fitur VPN di router-mu jauh lebih aman.
Kenapa esp32.local tidak berfungsi?
Penyebab yang paling umum adalah ponsel atau komputer tidak bisa me-resolve nama mDNS; ini terjadi terutama di sebagian ponsel Android dan versi Windows lama. Menulis MDNS.begin("esp32.local") di kode juga keliru, karena namanya harus diberikan tanpa .local. Kalau namanya tetap tidak ter-resolve, pakai alamat IP dari Serial Monitor atau reservasikan IP tetap untuk ESP32 di router-mu.
Bisakah web server ESP32 berjalan tanpa router?
Bisa. Dengan WiFi.softAP("ESP32-Panel", "12345678") ESP32 membuat jaringan Wi-Fi sendiri; sambungkan ponselmu ke jaringan itu lalu buka 192.168.4.1 di browser. Tidak ada internet dalam mode access point ini, tetapi panel dan WebSocket tetap berjalan seperti biasa. Kata sandi jaringannya minimal harus 8 karakter.
Library ESPAsyncWebServer mana yang harus aku instal?
Di Library Manager Arduino, instal library ESP Async WebServer dan Async TCP yang penulisnya ESP32Async. Versi asli di akun me-no-dev sudah tidak dirawat lagi dan tidak bisa dikompilasi dengan core ESP32 3.x. Hapus juga salinan lama yang dulu ditambahkan dari ZIP di folder libraries-mu.
Proyek untuk dibuat setelah membaca ini
Lihat semua proyek
Web Server Asinkron ESP32 (dengan Tampilan IP di LCD)
Di proyek ini, kita mengubah ESP32 menjadi web server asinkron yang bisa kamu kendalikan dari ponsel atau komputer di jaringan Wi-Fi yang sama. Berkat library ESPAsyncWebServer, server bisa memproses beberapa permintaan bersamaan (berbeda dengan WebServer.h klasik, yang memblokir pada satu permintaan). Kamu bisa membuka alamat IP ESP32 di browser dan mengklik tombol ON/OFF (rute /on dan /off) untuk menyalakan dan mematikan LED yang tersambung ke GPIO 5.

Penghitung WebSocket ESP32 (2 Tombol + LCD + Web Real-Time)
Di proyek ini, dua tombol yang tersambung ke ESP32 (GPIO 5 menghitung naik, GPIO 4 menghitung turun) mengubah sebuah penghitung. Nilainya langsung diperbarui di LCD I2C 16x2 sekaligus, lewat dukungan WebSocket ESPAsyncWebServer, di setiap browser yang tersambung tanpa memuat ulang halaman. Alih-alih polling HTTP, dipakai satu koneksi yang tetap terbuka, dan ESP32 mengirim nilai baru ke semua orang dengan ws.textAll().

mDNS ESP32: Mengakses Perangkat lewat bluegrays-esp.local (dengan Layar LCD)
Di proyek ini, kita menambahkan layanan mDNS (Multicast DNS) ke ESP32, sehingga perangkat bisa diakses dengan nama host seperti bluegrays-esp.local, bukan alamat IP. Meskipun IP dari DHCP berubah, perangkat selalu bisa dijangkau dengan nama yang sama. Web server kecil di perangkat menerima perintah kendali LED dari browser lewat alamat http://bluegrays-esp.local/. Layar LCD I2C menampilkan nama host, alamat IP, dan status LED.

Koneksi Wi-Fi STA ESP32 (dengan Layar LCD)
Di proyek ini, kita menyambungkan ESP32 ke jaringan Wi-Fi rumah dalam mode Station (STA) dengan cara yang aman dan tahan gangguan. Kita menyimpan SSID dan kata sandi di file secrets.h terpisah agar tidak bocor ke Git, menyiapkan mekanisme sambung ulang otomatis, dan menampilkan tingkat sinyal RSSI dan alamat IP di layar LCD I2C. LED bawaan bekerja sebagai indikator status. Ini adalah pengelola koneksi kelas produksi.

Suhu dan Kelembapan DHT22 ESP32 (dengan Layar LCD)
Proyek ini mengukur suhu dan kelembapan sekitar dengan sensor DHT22 dan menampilkan datanya secara real-time di LCD I2C 16x2. Data dibaca dari sensor dengan protokol satu kabel lewat library Adafruit DHT. Proyek ini menjadi dasar untuk stasiun cuaca IoT, otomasi rumah pintar, atau sistem pemantauan lingkungan di rumah kaca. Komunikasi dengan DHT22 dilakukan lewat pin GPIO 4 ESP32.

Lampu Pintar ESP32 (PIR + LDR + Relay + OLED)
Ubah lorong, kamar mandi, atau gudangmu menjadi lampu pintar dengan ESP32! Dengan sensor gerak PIR, sensor cahaya LDR, dan modul relay, kita membuat skenario klasik "lampu menyala ketika ada gerakan dalam gelap"; jika sudah terang, gerakan TIDAK menyalakan lampu, jadi energi hemat di siang hari. Lampu menyala 30 detik; jika gerakan berlanjut, timer diulang. OLED SSD1306 menampilkan status dengan font besar (NYALA / SIANG / MENUNGGU) dan hitung mundur.

Pembaruan OTA ESP32: Instal Firmware Tanpa Kabel (dengan Layar LCD)
Di proyek ini, kamu belajar cara meng-upload firmware baru ke ESP32 lewat Wi-Fi tanpa kabel USB. Dengan library ArduinoOTA, ESP32 terlihat di jaringan dan otomatis tercantum sebagai bluegrays-ota di tab "Network port" Arduino IDE. Upload pertama dilakukan lewat USB, dan semua pembaruan berikutnya tanpa kabel; fitur penting untuk perangkat di lapangan. Layar LCD I2C menampilkan status Wi-Fi, alamat IP, nama host, persentase upload OTA, dan pesan error.

Jam Real-Time dari Internet lewat NTP ESP32
ESP32 tersambung ke Wi-Fi dan mengambil waktu nyata dengan presisi milidetik dari server NTP (Network Time Protocol). Berkat fungsi configTzTime(), zona waktu Turki (TRT, UTC+3) otomatis diterapkan, dan pergantian DST di negara yang memakai waktu musim panas otomatis diatur dengan string POSIX TZ. Waktunya ditampilkan di Serial Monitor sekaligus di LCD I2C opsional. configTzTime() memulai layanan SNTP; sinkronisasi ulang berkala terjadi selama koneksi tersedia.
Artikel lainnya
Aplikasi BlueGrays
Rakit rangkaian ini langkah demi langkah di ponselmu
Skema pengkabelan, kode, dan daftar komponen dalam satu layar. 200+ proyek Arduino, ESP32, dan Pico siap dibuat.
Pindai dengan ponselmu
Arahkan kamera ke kode ini dan toko aplikasi di ponselmu akan terbuka.
atau buka toko aplikasi

