Senin, 02 November 2026

SISTEM PERINGATAN DINI BANJIR DAN KENDALI PINTU AIR OTOMATIS BERBASIS ESP32 MENGGUNAKAN PROTOKOL MQTT MELALUI WEBSOCKET

Tugas Modul 6 Kelompok 9 SISTEM PERINGATAN DINI BANJIR DAN KENDALI PINTU AIR OTOMATIS BERBASIS ESP32 MENGGUNAKAN PROTOKOL MQTT MELALUI WEBSOCKET

Disusun Oleh:

1. Fatkhur Rozaq(24051204026)
2. Ahmad Arkanul Falah Al Khalimi(24051204033)
3. Abdan Isa Ghaffara(24051204235)
4. Muhammad Naufal Muzaky(24051204233)
5. Orlinezar Raveda Ar-Rizky(24051204222)

BAB I
PENDAHULUAN

1.1 Latar Belakang

Banjir merupakan salah satu bencana yang sering terjadi di Indonesia. Kerugian akibat banjir dapat ditekan apabila kenaikan muka air diketahui lebih awal dan tindakan pengendalian, misalnya membuka pintu air, dilakukan tepat waktu. Pemantauan secara manual membutuhkan petugas yang berjaga terus-menerus, sehingga informasi sering terlambat sampai kepada pihak yang membutuhkan. Internet of Things (IoT) menawarkan cara yang lebih praktis, yaitu sensor mengukur ketinggian air secara otomatis, mikrokontroler memutuskan tingkat kewaspadaan, dan hasilnya dikirim ke layar pemantau melalui jaringan.

Laporan ini membahas Modul 6, yaitu penggunaan protokol MQTT melalui WebSocket untuk membangun aplikasi IoT berbasis HTML dan JavaScript (Modul 6, t.t.). Contoh pada modul, seperti pengiriman data GPS dan palang parkir berbasis QR Code, memperlihatkan alur perintah dari halaman web menuju ESP32 (ahocool-aisi555, 2025). Pada proyek ini alur tersebut dikembangkan menjadi komunikasi dua arah. ESP32 berperan sebagai publisher data sensor sekaligus subscriber perintah, sedangkan dashboard web berperan sebagai subscriber data sekaligus publisher perintah.

Sistem yang dirancang memakai sensor ultrasonik HC-SR04 untuk mengukur jarak permukaan air dari sensor. Tinggi muka air dihitung dari selisih tinggi pemasangan sensor dan jarak terukur, lalu diklasifikasikan menjadi tiga status, yaitu AMAN, WASPADA, dan BAHAYA. Setiap status memicu indikator lokal berupa LED tiga warna, buzzer, dan LCD I2C, serta menggerakkan servo yang mewakili pintu air. Dashboard web menampilkan tangki ketinggian air, grafik, status, dan tombol kontrol untuk berpindah antara mode otomatis dan manual, mengatur sudut pintu air, serta membisukan alarm. Seluruh rangkaian disimulasikan pada platform Wokwi sehingga dapat diuji tanpa perangkat keras fisik.

Dari sisi arsitektur komputer, program pada ESP32 sengaja dibuat tanpa fungsi delay() yang panjang. Pembacaan sensor, pengiriman data, dan bunyi alarm diatur dengan millis() agar fungsi loop() terus berjalan dan koneksi MQTT tetap terlayani. Pola ini memperlihatkan bagaimana pembagian waktu prosesor pada mikrokontroler memengaruhi ketanggapan sistem.

1.2 Rumusan Masalah

1.Bagaimana merancang rangkaian ESP32 dengan sensor ultrasonik, servo, LCD I2C, LED, dan buzzer pada platform simulasi Wokwi?

2.Bagaimana ESP32 menentukan status AMAN, WASPADA, dan BAHAYA dari ketinggian air serta mengendalikan pintu air secara otomatis?

3.Bagaimana membangun dashboard web yang menerima data sensor dan mengirim perintah menggunakan pustaka Paho MQTT melalui WebSocket?

4.Bagaimana menguji sistem dan apa saja keterbatasannya?

1.3 Tujuan Laporan

1.Merancang dan mensimulasikan rangkaian sistem peringatan dini banjir berbasis ESP32 pada platform Wokwi.

2.Menjelaskan logika penentuan status ketinggian air, termasuk penggunaan histeresis, dan pengendalian pintu air otomatis.

3.Menjelaskan mekanisme komunikasi dua arah antara ESP32 dan dashboard web melalui MQTT dan WebSocket.

4.Menguji sistem berdasarkan skenario ketinggian air dan menganalisis kelebihan serta keterbatasannya.

1.4 Manfaat Penulisan

Secara akademis, laporan ini memperjelas penerapan protokol MQTT melalui WebSocket pada kasus nyata, terutama pola komunikasi dua arah dan penulisan program mikrokontroler yang tidak memblokir. Secara praktis, rancangan ini dapat menjadi acuan untuk membuat prototipe pemantauan tinggi muka air pada sungai, waduk, atau saluran drainase, serta sebagai bahan pembelajaran integrasi aplikasi web dan perangkat tertanam.

BAB II
TINJAUAN PUSTAKA

2.1 Dasar Teori

2.1.1 Internet of Things (IoT)

Internet of Things adalah konsep yang menghubungkan perangkat fisik seperti sensor dan aktuator ke jaringan sehingga datanya dapat dipantau dan perangkatnya dapat dikendalikan dari jarak jauh. Pada proyek ini sensor ketinggian air, mikrokontroler, broker, dan dashboard web membentuk satu rantai IoT sederhana dari pengukuran hingga tindakan.

2.1.2 Mikrokontroler ESP32

ESP32 adalah mikrokontroler 32-bit dengan modul Wi-Fi internal sehingga dapat terhubung ke jaringan tanpa modul tambahan. Pada sistem ini ESP32 membaca sensor, menentukan status, menggerakkan aktuator, dan berkomunikasi dengan broker MQTT. Program ditulis dengan Arduino IDE menggunakan bahasa C++.

2.1.3 Sensor Ultrasonik HC-SR04

HC-SR04 mengukur jarak dengan memancarkan gelombang ultrasonik lalu menghitung lama waktu pantulan kembali. Mikrokontroler memberi pulsa 10 mikrodetik pada pin TRIG, kemudian mengukur lebar pulsa pada pin ECHO. Pada program, jarak dihitung dengan rumus jarak (cm) = durasi (µs) × 0,0343 ÷ 2, dengan 0,0343 cm/µs sebagai kecepatan suara dan pembagi dua karena gelombang menempuh jarak pergi dan pulang. Fungsi pulseIn() diberi batas waktu 30 ms agar program tidak menunggu terlalu lama bila tidak ada pantulan.

2.1.4 Servo Motor

Servo adalah aktuator yang sudut porosnya dapat ditentukan oleh program melalui sinyal PWM. Pada sistem ini servo disambungkan ke GPIO 13 dan berperan sebagai pintu air. Sudut 0° berarti pintu tertutup, 45° terbuka setengah, dan 90° terbuka penuh. Pustaka ESP32Servo diatur pada frekuensi 50 Hz dengan lebar pulsa 500 sampai 2400 mikrodetik.

2.1.5 LCD I2C, LED, dan Buzzer

LCD I2C berukuran 20 kolom × 4 baris dengan alamat 0x27 menampilkan level air, status, sudut pintu, dan mode kerja. Antarmuka I2C hanya memerlukan dua jalur data, yaitu SDA (GPIO 21) dan SCL (GPIO 22). Tiga LED hijau, kuning, dan merah menunjukkan status secara visual, sedangkan buzzer pada GPIO 14 memberi peringatan suara dengan pola yang berbeda untuk setiap tingkat bahaya.

2.1.6 WebSocket

WebSocket adalah protokol komunikasi yang memungkinkan browser dan server bertukar data secara dua arah. Menurut materi Modul 6 (t.t.), WebSocket memiliki empat karakteristik utama. Pertama, bersifat full duplex sehingga data dapat mengalir dua arah secara bersamaan. Kedua, memakai koneksi persisten yang tetap terbuka hingga ditutup secara eksplisit, berbeda dengan HTTP yang biasanya ditutup setelah setiap pasangan permintaan dan respons. Ketiga, mendukung data real-time tanpa menunggu permintaan klien. Keempat, beroperasi di atas koneksi TCP. Contoh penerapannya pada ESP32 dapat dilihat pada Random Nerd Tutorials (t.t.).

2.1.7 MQTT

MQTT (Message Queuing Telemetry Transport) adalah protokol pesan ringan berpola publish/subscribe. Komunikasi berlangsung melalui perantara bernama broker. Publisher mengirim pesan ke sebuah topic, sedangkan subscriber mendaftar pada topic tersebut untuk menerima pesan. Tiga fitur MQTT dimanfaatkan pada proyek ini. Pertama, wildcard # yang membuat ESP32 cukup berlangganan satu pola cmd/# untuk menerima semua perintah. Kedua, retained message yang menyimpan pesan terakhir pada broker, dipakai untuk status online. Ketiga, Last Will and Testament (LWT), yaitu pesan yang diterbitkan broker secara otomatis bila klien terputus tiba-tiba, dipakai untuk menandai ESP32 offline.

2.1.8 MQTT melalui WebSocket dan Pustaka Paho

Browser tidak dapat membuka koneksi TCP mentah ke broker MQTT, sehingga halaman web memakai MQTT melalui WebSocket. Pada proyek ini browser terhubung ke broker.hivemq.com melalui port 8884 dengan SSL, sedangkan ESP32 terhubung ke broker yang sama melalui port 1883. Kedua sisi bertemu pada topic yang sama. Cara ini juga membantu bila port MQTT biasa diblokir firewall karena WebSocket memakai port HTTP (Modul 6, t.t.).

Paho adalah proyek Eclipse Foundation yang menyediakan pustaka klien MQTT open source. MQTT dibuat oleh Andy Stanford-Clark (IBM) dan Arlen Nipper pada tahun 1999 untuk aplikasi industri. IBM menyumbangkan kode klien MQTT ke Eclipse pada tahun 2013, dan pustaka Paho tersedia untuk Java, C, JavaScript, serta Python (Modul 6, t.t.). Pada sisi ESP32 dipakai pustaka PubSubClient (O'Leary, t.t.). Pola penulisan klien Paho JavaScript dengan fungsi callback onSuccess, onFailure, dan onMessageArrived mengikuti contoh pada Steve's Internet Guide (t.t.).

2.1.9 Platform Simulasi Wokwi

Wokwi adalah simulator elektronika berbasis web yang mendukung ESP32 beserta berbagai komponen virtual. Wokwi menyediakan jaringan Wi-Fi virtual dengan SSID Wokwi-GUEST sehingga program dapat terhubung ke internet dan broker publik tanpa perangkat keras (Wokwi, t.t.).

2.2 Komponen

Berikut adalah komponen perangkat keras (virtual) dan perangkat lunak yang digunakan pada sistem.

Tabel 1. Komponen Perangkat Keras

NoKomponenFungsi dalam Sistem
1ESP32 DevKit C V4Mikrokontroler utama: membaca sensor, menentukan status, dan berkomunikasi dengan broker MQTT.
2Sensor HC-SR04Mengukur jarak permukaan air dari sensor.
3Servo motorSimulasi pintu air (0°, 45°, 90°).
4LCD I2C 20×4Menampilkan level, status, sudut pintu, dan mode.
5LED hijau, kuning, merahIndikator visual status AMAN, WASPADA, BAHAYA.
6Resistor 220 Ω (3 buah)Pembatas arus untuk setiap LED.
7BuzzerAlarm suara dengan pola berbeda pada WASPADA dan BAHAYA.
8Kabel jumper virtual dan Wi-Fi Wokwi-GUESTMenghubungkan komponen dan memberi akses internet ke ESP32.

Tabel 2. Perangkat Lunak dan Pustaka

Perangkat / PustakaPeran
WokwiSimulasi rangkaian dan program ESP32.
WiFi.hMenghubungkan ESP32 ke jaringan Wi-Fi.
PubSubClient.hKoneksi MQTT, publish, subscribe, dan Last Will pada ESP32.
ESP32Servo.hMengendalikan servo pada ESP32.
LiquidCrystal_I2C.hMengendalikan LCD melalui I2C.
HTML, CSS, JavaScriptMembangun dashboard, grafik, dan logika kontrol.
Paho MQTT JavaScript 1.0.1Klien MQTT pada browser melalui WebSocket Secure.
Broker HiveMQBroker MQTT publik (broker.hivemq.com) sebagai perantara pesan.
Gambar 1. Komponen yang Digunakan pada Wokwi

BAB III
PEMBAHASAN

3.1 Penjelasan Sistem

3.1.1 Arsitektur dan Alur Kerja

Sistem terdiri dari dua bagian yang dihubungkan oleh broker MQTT, yaitu program ESP32 dan dashboard web index.html. Berbeda dengan contoh pada modul yang hanya mengirim perintah dari web ke ESP32, kedua bagian di sini saling mengirim dan menerima pesan. ESP32 menerbitkan data sensor ke topic data dan berlangganan pada topic cmd/#, sedangkan dashboard berlangganan pada topic data dan status serta menerbitkan perintah ke topic cmd. Arsitekturnya ditunjukkan pada Gambar 2 dan alur kerjanya dirangkum pada Tabel 3.

Diagram arsitektur sistem EWS banjir
Gambar 2. Arsitektur Sistem

Tabel 3. Alur Kerja Sistem

TahapKomponenProsesKeluaran
1HC-SR04Memancarkan gelombang ultrasonik dan mengukur waktu pantulan setiap 500 ms.Jarak (cm)
2ESP32Merata-ratakan 5 pembacaan terakhir, lalu menghitung level = 200 − jarak.Level air (cm)
3ESP32Menentukan status dengan histeresis, lalu mengatur servo, LED, buzzer, dan LCD.Status dan aksi lokal
4ESP32 → BrokerMenerbitkan data JSON ke topic data setiap 1 detik atau saat status berubah.Pesan MQTT
5Broker → DashboardMeneruskan pesan melalui WebSocket Secure port 8884.Data tiba di browser
6DashboardMemperbarui tangki, status, dan grafik. Pengguna dapat mengirim perintah.Tampilan dan perintah
7Dashboard → ESP32Perintah mode, sudut pintu, atau bisu dikirim ke topic cmd/..., lalu diterapkan ESP32.Aktuator berubah

3.1.2 Rancangan Rangkaian

Rangkaian dibuat pada Wokwi dengan koneksi pin seperti pada Tabel 4. Pin ECHO HC-SR04 pada perangkat nyata mengeluarkan sinyal 5 V, sedangkan pin GPIO ESP32 bekerja pada 3,3 V. Pada simulasi hal ini tidak menjadi masalah, tetapi pada perangkat fisik sebaiknya dipasang pembagi tegangan pada jalur ECHO.

Tabel 4. Konfigurasi Pin

KomponenPin KomponenPin ESP32Fungsi
HC-SR04TRIGGPIO 5Mengirim pulsa pemicu.
HC-SR04ECHOGPIO 18Menerima pulsa pantulan.
HC-SR04VCC / GND5V / GNDCatu daya sensor.
LCD I2CSDAGPIO 21Jalur data I2C.
LCD I2CSCLGPIO 22Jalur clock I2C.
LCD I2CVCC / GND5V / GNDCatu daya LCD.
ServoPWMGPIO 13Sinyal kendali sudut pintu air.
ServoV+ / GND5V / GNDCatu daya servo.
BuzzerPin 2 / Pin 1GPIO 14 / GNDSinyal alarm suara.
LED hijauAnoda (via 220 Ω)GPIO 25Indikator AMAN.
LED kuningAnoda (via 220 Ω)GPIO 26Indikator WASPADA.
LED merahAnoda (via 220 Ω)GPIO 27Indikator BAHAYA.
Gambar 3. Rangkaian Sistem pada Wokwi

3.1.3 Struktur Topic dan Format Data MQTT

Seluruh topic memakai awalan unesa/informatika/kel9/banjir agar tidak bertabrakan dengan pengguna lain pada broker publik. Rincian topic ditunjukkan pada Tabel 5, dengan awalan tersebut dihilangkan agar ringkas.

Tabel 5. Daftar Topic MQTT

TopicArahPayloadKeterangan
/dataESP32 → DashboardJSONBerisi jarak, level, status, pintu, mode, mute, rssi, dan uptime.
/statusESP32 → Dashboardonline / offlineBersifat retained. Nilai offline diterbitkan broker lewat Last Will.
/cmd/modeDashboard → ESP32AUTO / MANUALMemilih mode kendali pintu air.
/cmd/pintuDashboard → ESP320 sampai 90Sudut pintu air. Hanya diterima pada mode MANUAL.
/cmd/muteDashboard → ESP32ON / OFFMembisukan atau mengaktifkan alarm.

Contoh isi pesan pada topic data adalah {"jarak":80.0,"level":120.0,"status":"WASPADA","pintu":45,"mode":"AUTO","mute":false,"rssi":-50,"uptime":125}. Format JSON dipilih agar dashboard cukup memanggil JSON.parse() untuk membaca seluruh nilai sekaligus.

3.1.4 Logika Status dan Aksi Otomatis

Tinggi muka air dihitung dengan rumus level = 200 − jarak, dengan 200 cm sebagai tinggi pemasangan sensor dari dasar. Nilai level kemudian dibandingkan dengan dua ambang, yaitu 100 cm untuk WASPADA dan 150 cm untuk BAHAYA. Aksi untuk setiap status pada mode AUTO ditunjukkan pada Tabel 6.

Tabel 6. Status dan Aksi pada Mode AUTO

StatusLevel airLEDBuzzerPintu air
AMANdi bawah 100 cmHijauDiam0°
WASPADA100 sampai 149 cmKuningBunyi 1 kHz, 200 ms setiap 2 detik45°
BAHAYA150 cm ke atasMerahBunyi 2 kHz, putus-putus cepat (150 ms)90°

Agar status tidak berkedip ketika level berada tepat di sekitar ambang, program memakai histeresis sebesar 5 cm. Status naik segera setelah level mencapai ambang, tetapi baru turun bila level berada 5 cm di bawah ambang tersebut. Sebagai contoh, status BAHAYA baru berganti menjadi WASPADA ketika level kurang dari 145 cm. Selain itu, jarak yang dipakai adalah rata-rata dari lima pembacaan terakhir untuk meredam derau pengukuran. Akibatnya, perubahan jarak pada sensor baru tercermin penuh sekitar 2,5 detik kemudian (5 sampel × 500 ms).

Tabel 7. Parameter Konfigurasi Sistem

ParameterNilai pada ProgramKeterangan
Broker MQTTbroker.hivemq.comBroker publik, memerlukan internet.
Port ESP321883Koneksi TCP oleh PubSubClient.
Port browser8884WebSocket Secure (useSSL: true).
Wi-FiWokwi-GUESTJaringan virtual Wokwi tanpa kata sandi.
Tinggi sensor200 cmJarak sensor ke dasar sungai.
Ambang WASPADA / BAHAYA100 cm / 150 cmBatas level penentu status.
Histeresis5 cmPencegah status berkedip.
Interval baca / kirim500 ms / 1000 msPeriode sensor dan publikasi data.
Reconnect MQTT / Wi-Fi5000 msPercobaan sambung ulang tanpa memblokir program.
Serial Monitor115200 baudUntuk melihat log ESP32.

3.1.5 Cara Kerja Program ESP32

Program ESP32 menggunakan pustaka WiFi.h, PubSubClient.h, ESP32Servo.h, dan LiquidCrystal_I2C.h. Fungsi-fungsi utamanya dijelaskan sebagai berikut.

1.setup(): mengatur pin, memasang servo pada GPIO 13 dengan frekuensi 50 Hz, menyiapkan LCD, menyambung Wi-Fi, mengatur server dan callback MQTT, lalu melakukan pembacaan awal.

2.bacaJarakCm() dan bacaSensor(): memicu HC-SR04, mengukur durasi pantulan dengan batas waktu 30 ms, mengubahnya menjadi sentimeter, lalu merata-ratakan lima sampel terakhir menjadi jarakCm dan levelCm.

3.hitungStatus() dan perbaruiStatus(): menentukan status baru dengan histeresis. Bila status berubah, program mencatat ke Serial Monitor dan langsung menerbitkan data ke broker.

4.terapkanPintu(): pada mode AUTO sudut pintu mengikuti status (0°, 45°, 90°), sedangkan pada mode MANUAL mengikuti sudutManual dari dashboard. Servo hanya digerakkan bila sudut target berubah.

5.perbaruiAlarm(): mengatur bunyi buzzer dengan millis() tanpa delay(), sehingga client.loop() tetap berjalan selama alarm berbunyi. Alarm otomatis diaktifkan kembali ketika status kembali AMAN.

6.sambungMqtt(): membuat client ID acak, terhubung ke broker dengan Last Will pada topic status, menerbitkan online secara retained, lalu berlangganan cmd/#. Bila gagal, percobaan diulang setiap 5 detik tanpa memblokir program.

7.onPesan(): mengurai topic dan payload perintah. Perintah mode mengubah modeAuto, perintah pintu mengubah sudutManual (dibatasi 0 sampai 90 dan diabaikan pada mode AUTO), dan perintah mute mengubah alarmMute. Setelah itu data terbaru langsung dikirim kembali sebagai konfirmasi.

8.loop(): menjaga Wi-Fi dan MQTT, memanggil client.loop(), membaca sensor setiap 500 ms, mengirim data setiap 1 detik, dan memperbarui alarm pada setiap putaran.

3.1.6 Cara Kerja Dashboard Web

Halaman index.html memuat pustaka Paho MQTT dari CDN dan membuat klien dengan client ID acak berawalan "web-kel9-". Klien terhubung ke broker melalui port 8884 dengan SSL, kemudian berlangganan topic data dan status. Bila koneksi terputus, onConnectionLost menjadwalkan sambungan ulang setelah 3 detik.

Setiap pesan pada topic data diubah dari JSON menjadi objek, lalu fungsi terimaData() memperbarui angka level dan jarak, tinggi air pada gambar tangki, warna status, posisi pintu, mode, dan grafik garis berisi 60 data terakhir. Garis putus-putus pada tangki dan grafik menandai ambang WASPADA dan BAHAYA. Badge di bagian atas menunjukkan status koneksi broker dan status online atau offline ESP32 dari topic status. Bila tidak ada data selama lebih dari 5 detik, status berubah menjadi DATA KEDALUWARSA.

Tombol Mode AUTO dan Mode MANUAL menerbitkan pesan ke cmd/mode. Slider sudut pintu hanya aktif pada mode MANUAL dan mengirim nilai ke cmd/pintu saat slider dilepas, bukan sepanjang geseran, agar tidak membanjiri broker. Tombol Bisukan Alarm menerbitkan ON atau OFF ke cmd/mute. Dashboard tidak mengubah tampilan sendiri setelah perintah dikirim, melainkan menunggu data konfirmasi dari ESP32 sehingga tampilan selalu mencerminkan keadaan perangkat yang sebenarnya.

Gambar 4. Tampilan Dashboard Web

3.1.7 Prosedur Menjalankan Sistem

1.Buka wokwi.com, buat proyek baru dengan papan ESP32, lalu ganti isi berkas sketch.ino, diagram.json, dan libraries.txt dengan kode pada Subbab 3.2.

2.Jalankan simulasi dengan tombol Start. Buka Serial Monitor dan pastikan muncul pesan Wi-Fi terhubung serta MQTT terhubung.

3.Simpan kode dashboard sebagai index.html, lalu buka dengan browser (klik ganda berkas atau melalui hosting statis seperti GitHub Pages).

4.Pastikan badge pada dashboard berubah menjadi Broker: terhubung dan ESP32: online.

5.Klik sensor HC-SR04 pada Wokwi dan ubah nilai Distance untuk mensimulasikan naik turunnya muka air. Amati LCD, LED, buzzer, servo, dan dashboard.

6.Uji tombol Mode MANUAL, slider sudut pintu, dan tombol Bisukan Alarm pada dashboard.

3.2 Program

Berikut kode program yang digunakan pada sistem, terdiri dari program ESP32 (Kode Sumber 1), konfigurasi rangkaian Wokwi (Kode Sumber 2), daftar pustaka Wokwi (Kode Sumber 3), dan dashboard web (Kode Sumber 4).

Kode Sumber 1. Program ESP32 (sketch.ino, Arduino IDE, C++)

/*
Kelompok 9 - Modul 6 : MQTT melalui WebSocket
Sistem Peringatan Dini Banjir dan Kendali Pintu Air Berbasis ESP32

Sensor : HC-SR04 (jarak permukaan air)
Aktuator : Servo (pintu air), Buzzer, 3 LED, LCD I2C 20x4
Protokol : MQTT (ESP32 -> TCP 1883, Browser -> WebSocket Secure 8884)
*/

#include <WiFi.h>
#include <PubSubClient.h>
#include <ESP32Servo.h>
#include <LiquidCrystal_I2C.h>

// ---------- Konfigurasi WiFi dan MQTT ----------
const char* WIFI_SSID = "Wokwi-GUEST";
const char* WIFI_PASS = "";
const char* MQTT_HOST = "broker.hivemq.com";
const uint16_t MQTT_PORT = 1883;

#define TOPIC_BASE "unesa/informatika/kel9/banjir"
const char* TOPIC_DATA = TOPIC_BASE "/data"; // ESP32 -> Dashboard
const char* TOPIC_STATUS = TOPIC_BASE "/status"; // online / offline (LWT)
const char* TOPIC_CMD_ALL = TOPIC_BASE "/cmd/#"; // Dashboard -> ESP32
const char* TOPIC_MODE = TOPIC_BASE "/cmd/mode"; // AUTO / MANUAL
const char* TOPIC_PINTU = TOPIC_BASE "/cmd/pintu"; // 0 - 90 (derajat)
const char* TOPIC_MUTE = TOPIC_BASE "/cmd/mute"; // ON / OFF

// ---------- Pin ----------
const int PIN_TRIG = 5;
const int PIN_ECHO = 18;
const int PIN_SERVO = 13;
const int PIN_BUZZER = 14;
const int PIN_LED_HIJAU = 25;
const int PIN_LED_KUNING = 26;
const int PIN_LED_MERAH = 27;

// ---------- Parameter sistem ----------
const float TINGGI_SENSOR_CM = 200.0; // jarak sensor ke dasar sungai
const float BATAS_WASPADA_CM = 100.0; // level >= 100 cm -> WASPADA
const float BATAS_BAHAYA_CM = 150.0; // level >= 150 cm -> BAHAYA
const float HISTERESIS_CM = 5.0; // mencegah status berkedip
const int SUDUT_AMAN = 0; // pintu air tertutup
const int SUDUT_WASPADA = 45; // pintu air terbuka setengah
const int SUDUT_BAHAYA = 90; // pintu air terbuka penuh

const unsigned long INTERVAL_BACA_MS = 500;
const unsigned long INTERVAL_KIRIM_MS = 1000;
const unsigned long INTERVAL_RECONNECT_MS = 5000;

enum Status { AMAN, WASPADA, BAHAYA };
const char* NAMA_STATUS[] = { "AMAN", "WASPADA", "BAHAYA" };

// ---------- Objek ----------
Servo pintuAir;
LiquidCrystal_I2C lcd(0x27, 20, 4);
WiFiClient espClient;
PubSubClient client(espClient);

// ---------- Variabel keadaan ----------
const int JUMLAH_SAMPEL = 5;
float sampel[JUMLAH_SAMPEL];
int indeksSampel = 0;
int sampelTerisi = 0;

float jarakCm = TINGGI_SENSOR_CM;
float levelCm = 0;
Status status = AMAN;
bool modeAuto = true;
bool alarmMute = false;
int sudutPintu = -1; // -1 = belum pernah digerakkan
int sudutManual = 0;

unsigned long tBaca = 0, tKirim = 0, tReconnect = 0, tWifi = 0;

void kirimTelemetri(); // deklarasi awal (dipakai sebelum didefinisikan)

// ================= Sensor =================
float bacaJarakCm() {
digitalWrite(PIN_TRIG, LOW);
delayMicroseconds(2);
digitalWrite(PIN_TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(PIN_TRIG, LOW);
unsigned long durasi = pulseIn(PIN_ECHO, HIGH, 30000UL);
if (durasi == 0) return -1; // tidak ada pantulan
return durasi * 0.0343f / 2.0f;
}

void bacaSensor() {
float j = bacaJarakCm();
if (j < 0) {
Serial.println("[SENSOR] Tidak ada pantulan, data dilewati");
return;
}
sampel[indeksSampel] = j;
indeksSampel = (indeksSampel + 1) % JUMLAH_SAMPEL;
if (sampelTerisi < JUMLAH_SAMPEL) sampelTerisi++;

float total = 0;
for (int i = 0; i < sampelTerisi; i++) total += sampel[i];
jarakCm = total / sampelTerisi; // rata-rata bergerak

levelCm = constrain(TINGGI_SENSOR_CM - jarakCm, 0, TINGGI_SENSOR_CM);
}

// ================= Logika status =================
Status hitungStatus(float level, Status sekarang) {
switch (sekarang) {
case AMAN:
if (level >= BATAS_BAHAYA_CM) return BAHAYA;
if (level >= BATAS_WASPADA_CM) return WASPADA;
return AMAN;
case WASPADA:
if (level >= BATAS_BAHAYA_CM) return BAHAYA;
if (level < BATAS_WASPADA_CM - HISTERESIS_CM) return AMAN;
return WASPADA;
default: // BAHAYA
if (level < BATAS_BAHAYA_CM - HISTERESIS_CM)
return (level >= BATAS_WASPADA_CM) ? WASPADA : AMAN;
return BAHAYA;
}
}

int sudutOtomatis(Status s) {
if (s == BAHAYA) return SUDUT_BAHAYA;
if (s == WASPADA) return SUDUT_WASPADA;
return SUDUT_AMAN;
}

void perbaruiStatus() {
Status baru = hitungStatus(levelCm, status);
if (baru != status) {
Serial.printf("[STATUS] %s -> %s (level %.1f cm)\n",
NAMA_STATUS[status], NAMA_STATUS[baru], levelCm);
status = baru;
if (status == AMAN) alarmMute = false; // alarm aktif lagi untuk kejadian berikutnya
kirimTelemetri(); // kabari dashboard seketika
}
}

// ================= Aktuator =================
void terapkanPintu() {
int target = modeAuto ? sudutOtomatis(status) : sudutManual;
if (target != sudutPintu) {
sudutPintu = target;
pintuAir.write(sudutPintu);
Serial.printf("[PINTU] Sudut %d derajat (%s)\n", sudutPintu, modeAuto ? "AUTO" : "MANUAL");
}
}

void perbaruiLed() {
digitalWrite(PIN_LED_HIJAU, status == AMAN);
digitalWrite(PIN_LED_KUNING, status == WASPADA);
digitalWrite(PIN_LED_MERAH, status == BAHAYA);
}

// Alarm tanpa delay() agar loop() tetap berjalan dan MQTT tetap terlayani
void perbaruiAlarm() {
static bool bunyi = false;
static unsigned long tUbah = 0;

if (alarmMute || status == AMAN) {
if (bunyi) { noTone(PIN_BUZZER); bunyi = false; }
return;
}

unsigned long durasiOn = (status == WASPADA) ? 200 : 150;
unsigned long durasiOff = (status == WASPADA) ? 1800 : 150;
int nada = (status == WASPADA) ? 1000 : 2000;

unsigned long batas = bunyi ? durasiOn : durasiOff;
if (millis() - tUbah >= batas) {
tUbah = millis();
bunyi = !bunyi;
if (bunyi) tone(PIN_BUZZER, nada);
else noTone(PIN_BUZZER);
}
}

void tulisBaris(int baris, const char* teks) {
char buf[21];
snprintf(buf, sizeof(buf), "%-20s", teks);
lcd.setCursor(0, baris);
lcd.print(buf);
}

void perbaruiLcd() {
char b[24];
tulisBaris(0, "EWS BANJIR - KEL 9");
snprintf(b, sizeof(b), "Level : %.1f cm", levelCm);
tulisBaris(1, b);
snprintf(b, sizeof(b), "Status: %s", NAMA_STATUS[status]);
tulisBaris(2, b);
snprintf(b, sizeof(b), "Pintu:%3d %s%s", sudutPintu,
modeAuto ? "AUTO" : "MANUAL", alarmMute ? " MUTE" : "");
tulisBaris(3, b);
}

// ================= WiFi dan MQTT =================
void sambungWifi() {
Serial.print("Menghubungkan ke WiFi ");
Serial.println(WIFI_SSID);
tulisBaris(1, "Menyambung WiFi...");
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS, 6);
int hitung = 0;
while (WiFi.status() != WL_CONNECTED && hitung < 40) {
delay(500);
Serial.print(".");
hitung++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi terhubung, IP: " + WiFi.localIP().toString());
} else {
Serial.println("\n[ERROR] WiFi gagal, akan dicoba lagi di loop()");
}
}

void jagaWifi() {
if (WiFi.status() == WL_CONNECTED) return;
if (millis() - tWifi < INTERVAL_RECONNECT_MS) return;
tWifi = millis();
Serial.println("WiFi terputus, mencoba menyambung ulang...");
WiFi.disconnect();
WiFi.begin(WIFI_SSID, WIFI_PASS, 6);
}

void kirimTelemetri() {
if (!client.connected()) return;
char buf[200];
snprintf(buf, sizeof(buf),
"{\"jarak\":%.1f,\"level\":%.1f,\"status\":\"%s\",\"pintu\":%d,"
"\"mode\":\"%s\",\"mute\":%s,\"rssi\":%d,\"uptime\":%lu}",
jarakCm, levelCm, NAMA_STATUS[status], sudutPintu,
modeAuto ? "AUTO" : "MANUAL", alarmMute ? "true" : "false",
WiFi.RSSI(), millis() / 1000UL);
client.publish(TOPIC_DATA, buf);
}

void onPesan(char* topic, byte* payload, unsigned int length) {
char isi[32];
unsigned int n = (length < sizeof(isi) - 1) ? length : sizeof(isi) - 1;
memcpy(isi, payload, n);
isi[n] = '\0';

String t(topic);
String p(isi);
p.trim();
p.toUpperCase();
Serial.printf("[MQTT] %s = %s\n", topic, p.c_str());

if (t == TOPIC_MODE) {
if (p == "AUTO") {
modeAuto = true;
} else if (p == "MANUAL") {
modeAuto = false;
sudutManual = (sudutPintu < 0) ? 0 : sudutPintu;
}
} else if (t == TOPIC_PINTU) {
if (modeAuto) {
Serial.println("[MQTT] Perintah pintu diabaikan: mode AUTO");
} else {
sudutManual = constrain(p.toInt(), 0, 90);
}
} else if (t == TOPIC_MUTE) {
alarmMute = (p == "ON" || p == "1" || p == "TRUE");
}

terapkanPintu();
perbaruiLcd();
kirimTelemetri();
}

void sambungMqtt() {
if (client.connected() || WiFi.status() != WL_CONNECTED) return;
if (millis() - tReconnect < INTERVAL_RECONNECT_MS) return;
tReconnect = millis();

String id = "ESP32-EWS-" + String((uint32_t)esp_random(), HEX);
Serial.print("Menghubungkan ke MQTT broker... ");
// Last Will: broker menerbitkan "offline" bila ESP32 terputus tiba-tiba
if (client.connect(id.c_str(), nullptr, nullptr, TOPIC_STATUS, 1, true, "offline")) {
Serial.println("terhubung");
client.publish(TOPIC_STATUS, "online", true);
client.subscribe(TOPIC_CMD_ALL);
kirimTelemetri();
} else {
Serial.printf("gagal, rc=%d\n", client.state());
}
}

// ================= setup dan loop =================
void setup() {
Serial.begin(115200);
delay(500);
Serial.println("\n--- EWS Banjir Kelompok 9 ---");

pinMode(PIN_TRIG, OUTPUT);
pinMode(PIN_ECHO, INPUT);
pinMode(PIN_LED_HIJAU, OUTPUT);
pinMode(PIN_LED_KUNING, OUTPUT);
pinMode(PIN_LED_MERAH, OUTPUT);
pinMode(PIN_BUZZER, OUTPUT);

ESP32PWM::allocateTimer(0);
ESP32PWM::allocateTimer(1);
pintuAir.setPeriodHertz(50);
pintuAir.attach(PIN_SERVO, 500, 2400);

lcd.init();
lcd.backlight();
lcd.clear();
tulisBaris(0, "EWS BANJIR - KEL 9");

sambungWifi();

client.setServer(MQTT_HOST, MQTT_PORT);
client.setCallback(onPesan);
client.setBufferSize(512);

bacaSensor();
perbaruiStatus();
terapkanPintu();
perbaruiLed();
perbaruiLcd();
}

void loop() {
jagaWifi();
sambungMqtt();
client.loop();

unsigned long sekarang = millis();

if (sekarang - tBaca >= INTERVAL_BACA_MS) {
tBaca = sekarang;
bacaSensor();
perbaruiStatus();
terapkanPintu();
perbaruiLed();
perbaruiLcd();
}

if (sekarang - tKirim >= INTERVAL_KIRIM_MS) {
tKirim = sekarang;
kirimTelemetri();
}

perbaruiAlarm();
}

Kode Sumber 2. Konfigurasi Rangkaian Wokwi (diagram.json)

{
"version": 1,
"author": "Kelompok 9",
"editor": "wokwi",
"parts": [
{"type": "board-esp32-devkit-c-v4", "id": "esp", "top": 0, "left": 0, "attrs": {}},
{"type": "wokwi-hc-sr04", "id": "ultra1", "top": -40, "left": -290, "attrs": {"distance": "150"}},
{"type": "wokwi-lcd2004", "id": "lcd1", "top": -275, "left": 120, "attrs": {"pins": "i2c"}},
{"type": "wokwi-servo", "id": "servo1", "top": 40, "left": 230, "attrs": {}},
{"type": "wokwi-buzzer", "id": "bz1", "top": 170, "left": 240, "attrs": {"volume": "0.1"}},
{"type": "wokwi-led", "id": "led1", "top": 250, "left": 20, "attrs": {"color": "green"}},
{"type": "wokwi-led", "id": "led2", "top": 250, "left": 80, "attrs": {"color": "yellow"}},
{"type": "wokwi-led", "id": "led3", "top": 250, "left": 140, "attrs": {"color": "red"}},
{"type": "wokwi-resistor", "id": "r1", "top": 320, "left": 10, "rotate": 90, "attrs": {"value": "220"}},
{"type": "wokwi-resistor", "id": "r2", "top": 320, "left": 70, "rotate": 90, "attrs": {"value": "220"}},
{"type": "wokwi-resistor", "id": "r3", "top": 320, "left": 130, "rotate": 90, "attrs": {"value": "220"}}
],
"connections": [
["esp:5V", "ultra1:VCC", "red", []],
["esp:GND.1", "ultra1:GND", "black", []],
["esp:5", "ultra1:TRIG", "green", []],
["esp:18", "ultra1:ECHO", "orange", []],
["esp:5V", "lcd1:VCC", "red", []],
["esp:GND.2", "lcd1:GND", "black", []],
["esp:21", "lcd1:SDA", "blue", []],
["esp:22", "lcd1:SCL", "purple", []],
["esp:5V", "servo1:V+", "red", []],
["esp:GND.2", "servo1:GND", "black", []],
["esp:13", "servo1:PWM", "gold", []],
["esp:14", "bz1:2", "orange", []],
["esp:GND.2", "bz1:1", "black", []],
["esp:25", "r1:1", "green", []],
["r1:2", "led1:A", "green", []],
["led1:C", "esp:GND.1", "black", []],
["esp:26", "r2:1", "green", []],
["r2:2", "led2:A", "green", []],
["led2:C", "esp:GND.1", "black", []],
["esp:27", "r3:1", "green", []],
["r3:2", "led3:A", "green", []],
["led3:C", "esp:GND.1", "black", []]
],
"dependencies": {}
}

Kode Sumber 3. Daftar Pustaka Wokwi (libraries.txt)

PubSubClient
ESP32Servo
LiquidCrystal I2C

Kode Sumber 4. Dashboard Web (index.html)

<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>EWS Banjir - Kelompok 9</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.min.js"></script>
<style>
:root { --aman:#2e9e5b; --waspada:#e0a800; --bahaya:#d93636; --abu:#6b7280; }
* { box-sizing: border-box; }
body { margin:0; font-family: Arial, sans-serif; background:#eef2f6; color:#1f2937; }
header { background:#0f3d5e; color:#fff; padding:14px 20px; display:flex;
justify-content:space-between; align-items:center; flex-wrap:wrap; gap:8px; }
header h1 { margin:0; font-size:18px; }
.badge { padding:4px 12px; border-radius:14px; font-size:13px; color:#fff; background:var(--abu); }
main { display:grid; grid-template-columns:repeat(auto-fit,minmax(300px,1fr)); gap:14px; padding:14px; }
.card { background:#fff; border-radius:10px; padding:16px; box-shadow:0 1px 4px rgba(0,0,0,.12); }
.card h2 { margin:0 0 10px; font-size:15px; color:#0f3d5e; }
.lebar { grid-column:1 / -1; }
.tangki { display:flex; gap:16px; align-items:center; }
.angka { font-size:40px; font-weight:bold; }
.kecil { font-size:13px; color:var(--abu); margin:3px 0; }
#status { display:inline-block; padding:6px 16px; border-radius:6px; color:#fff;
font-weight:bold; background:var(--abu); margin:8px 0; }
button { padding:8px 14px; border:0; border-radius:6px; background:#0f3d5e; color:#fff;
cursor:pointer; font-size:14px; margin:4px 4px 4px 0; }
button.aktif { background:var(--aman); }
button:disabled { background:#b6bcc6; cursor:not-allowed; }
input[type=range] { width:100%; }
canvas { width:100%; height:240px; display:block; }
#log { height:150px; overflow-y:auto; font-family:monospace; font-size:12px;
background:#f4f6f8; padding:8px; border-radius:6px; }
</style>
</head>
<body>
<header>
<h1>Sistem Peringatan Dini Banjir - Kelompok 9</h1>
<div>
<span id="badgeBroker" class="badge">Broker: menghubungkan...</span>
<span id="badgePerangkat" class="badge">ESP32: tidak diketahui</span>
</div>
</header>

<main>
<section class="card">
<h2>Tinggi Muka Air</h2>
<div class="tangki">
<svg width="110" height="220" viewBox="0 0 110 220">
<rect x="15" y="10" width="80" height="200" fill="#e8eef5" stroke="#334155" stroke-width="2"/>
<rect id="air" x="16" y="210" width="78" height="0" fill="#3b82f6"/>
<line x1="15" x2="95" y1="110" y2="110" stroke="#e0a800" stroke-dasharray="4"/>
<line x1="15" x2="95" y1="60" y2="60" stroke="#d93636" stroke-dasharray="4"/>
</svg>
<div>
<div class="angka"><span id="level">--</span> <small>cm</small></div>
<div class="kecil">Jarak sensor ke air: <b id="jarak">--</b> cm</div>
<div id="status">MENUNGGU DATA</div>
<div class="kecil">Garis kuning: 100 cm (waspada)</div>
<div class="kecil">Garis merah: 150 cm (bahaya)</div>
</div>
</div>
</section>

<section class="card">
<h2>Kontrol Pintu Air</h2>
<div class="kecil">Posisi pintu saat ini: <b id="pintu">--</b>&deg;</div>
<div class="kecil">Mode: <b id="mode">--</b> | Alarm: <b id="alarm">--</b></div>
<div>
<button id="btnAuto">Mode AUTO</button>
<button id="btnManual">Mode MANUAL</button>
<button id="btnMute">Bisukan Alarm</button>
</div>
<label class="kecil">Sudut pintu manual: <b id="nilaiSlider">0</b>&deg;</label>
<input type="range" id="slider" min="0" max="90" step="5" value="0" disabled>
<div class="kecil">Slider aktif hanya pada mode MANUAL.</div>
<div class="kecil">Update terakhir: <span id="waktu">-</span> | RSSI: <span id="rssi">-</span> dBm</div>
</section>

<section class="card lebar">
<h2>Grafik Level Air (60 data terakhir)</h2>
<canvas id="grafik" width="900" height="240"></canvas>
</section>

<section class="card lebar">
<h2>Log Kejadian</h2>
<div id="log"></div>
</section>
</main>

<script>
// ---------- Konfigurasi ----------
const BROKER = "broker.hivemq.com";
const PORT = 8884; // WebSocket Secure
const BASE = "unesa/informatika/kel9/banjir";
const T_DATA = BASE + "/data", T_STATUS = BASE + "/status";
const T_MODE = BASE + "/cmd/mode", T_PINTU = BASE + "/cmd/pintu", T_MUTE = BASE + "/cmd/mute";
const MAKS_LEVEL = 200, BATAS_WASPADA = 100, BATAS_BAHAYA = 150, MAKS_TITIK = 60;
const WARNA = { AMAN:"#2e9e5b", WASPADA:"#e0a800", BAHAYA:"#d93636" };

const $ = (id) => document.getElementById(id);
let data = [], terakhirMasuk = 0, statusLama = "", muteSekarang = false, modeSekarang = "AUTO";

const client = new Paho.MQTT.Client(BROKER, PORT, "/mqtt", "web-kel9-" + Math.floor(Math.random() * 100000));
client.onConnectionLost = (e) => {
badge("badgeBroker", "Broker: terputus", "#d93636");
catat("Koneksi broker terputus: " + (e.errorMessage || "-"));
setTimeout(hubungkan, 3000);
};
client.onMessageArrived = (m) => {
if (m.destinationName === T_DATA) { try { terimaData(JSON.parse(m.payloadString)); } catch (e) { catat("Data tidak valid"); } }
else if (m.destinationName === T_STATUS) {
const on = m.payloadString === "online";
badge("badgePerangkat", "ESP32: " + (on ? "online" : "offline"), on ? "#2e9e5b" : "#d93636");
catat("Perangkat " + m.payloadString);
}
};

function hubungkan() {
badge("badgeBroker", "Broker: menghubungkan...", "#6b7280");
client.connect({
useSSL: true, timeout: 5, keepAliveInterval: 30,
onSuccess: () => {
badge("badgeBroker", "Broker: terhubung", "#2e9e5b");
client.subscribe(T_DATA); client.subscribe(T_STATUS);
catat("Terhubung ke " + BROKER + ":" + PORT);
},
onFailure: (e) => {
badge("badgeBroker", "Broker: gagal", "#d93636");
catat("Gagal terhubung: " + e.errorMessage);
setTimeout(hubungkan, 5000);
}
});
}

function kirim(topik, isi) {
if (!client.isConnected()) { catat("Belum terhubung, perintah dibatalkan"); return; }
const pesan = new Paho.MQTT.Message(String(isi));
pesan.destinationName = topik;
client.send(pesan);
catat("Kirim " + topik.replace(BASE + "/", "") + " = " + isi);
}

function terimaData(d) {
terakhirMasuk = Date.now();
$("level").textContent = d.level.toFixed(1);
$("jarak").textContent = d.jarak.toFixed(1);
$("pintu").textContent = d.pintu;
$("mode").textContent = d.mode;
$("alarm").textContent = d.mute ? "DIBISUKAN" : "AKTIF";
$("rssi").textContent = d.rssi;
$("waktu").textContent = new Date().toLocaleTimeString("id-ID");
$("status").textContent = d.status;
$("status").style.background = WARNA[d.status] || "#6b7280";

const tinggi = Math.max(0, Math.min(MAKS_LEVEL, d.level));
$("air").setAttribute("height", tinggi);
$("air").setAttribute("y", 210 - tinggi);
$("air").setAttribute("fill", WARNA[d.status] || "#3b82f6");

modeSekarang = d.mode; muteSekarang = d.mute;
$("btnAuto").classList.toggle("aktif", d.mode === "AUTO");
$("btnManual").classList.toggle("aktif", d.mode === "MANUAL");
$("btnMute").textContent = d.mute ? "Aktifkan Alarm" : "Bisukan Alarm";
$("slider").disabled = (d.mode !== "MANUAL");
if (d.mode === "AUTO") { $("slider").value = d.pintu; $("nilaiSlider").textContent = d.pintu; }

if (d.status !== statusLama) { catat("Status: " + d.status + " (level " + d.level.toFixed(1) + " cm)"); statusLama = d.status; }
data.push(d.level); if (data.length > MAKS_TITIK) data.shift();
gambarGrafik();
}

function gambarGrafik() {
const c = $("grafik"), g = c.getContext("2d"), W = c.width, H = c.height, pad = 34;
g.clearRect(0, 0, W, H);
const y = (v) => H - pad - (v / MAKS_LEVEL) * (H - 2 * pad);
g.font = "11px Arial"; g.fillStyle = "#6b7280"; g.strokeStyle = "#e5e7eb";
for (let v = 0; v <= MAKS_LEVEL; v += 50) {
g.beginPath(); g.moveTo(pad, y(v)); g.lineTo(W - 10, y(v)); g.stroke();
g.fillText(v, 4, y(v) + 4);
}
[[BATAS_WASPADA, "#e0a800"], [BATAS_BAHAYA, "#d93636"]].forEach(([v, w]) => {
g.strokeStyle = w; g.setLineDash([6, 4]);
g.beginPath(); g.moveTo(pad, y(v)); g.lineTo(W - 10, y(v)); g.stroke();
});
g.setLineDash([]);
if (data.length < 2) return;
g.strokeStyle = "#2563eb"; g.lineWidth = 2; g.beginPath();
data.forEach((v, i) => {
const x = pad + (i / (MAKS_TITIK - 1)) * (W - pad - 10);
i ? g.lineTo(x, y(v)) : g.moveTo(x, y(v));
});
g.stroke(); g.lineWidth = 1;
}

function badge(id, teks, warna) { $(id).textContent = teks; $(id).style.background = warna; }
function catat(teks) {
const baris = document.createElement("div");
baris.textContent = new Date().toLocaleTimeString("id-ID") + " " + teks;
$("log").prepend(baris);
while ($("log").children.length > 40) $("log").lastChild.remove();
}

// ---------- Kontrol ----------
$("btnAuto").onclick = () => kirim(T_MODE, "AUTO");
$("btnManual").onclick = () => kirim(T_MODE, "MANUAL");
$("btnMute").onclick = () => kirim(T_MUTE, muteSekarang ? "OFF" : "ON");
$("slider").oninput = (e) => { $("nilaiSlider").textContent = e.target.value; };
$("slider").onchange = (e) => kirim(T_PINTU, e.target.value);

// Tandai data kedaluwarsa bila ESP32 berhenti mengirim > 5 detik
setInterval(() => {
if (terakhirMasuk && Date.now() - terakhirMasuk > 5000) {
$("status").textContent = "DATA KEDALUWARSA";
$("status").style.background = "#6b7280";
}
}, 1000);

gambarGrafik();
hubungkan();
</script>
</body>
</html>

3.3 Analisis Kelebihan dan Keterbatasan

3.3.1 Kelebihan

•Komunikasi dua arah: data sensor mengalir ke dashboard dan perintah mengalir ke ESP32 melalui satu broker yang sama.

•Program tidak memblokir. Alarm, pembacaan sensor, dan pengiriman data diatur dengan millis() sehingga MQTT tetap terlayani saat buzzer berbunyi.

•Histeresis dan rata-rata bergerak membuat status lebih stabil dan tidak mudah berkedip akibat derau sensor.

•Last Will dan pemeriksaan data kedaluwarsa membuat pengguna mengetahui bila perangkat berhenti bekerja.

•Mode MANUAL memungkinkan operator mengambil alih pintu air, sedangkan mode AUTO menjaga sistem tetap bekerja tanpa operator.

•Dashboard hanya berupa satu berkas HTML sehingga mudah dijalankan dan dibagikan.

3.3.2 Keterbatasan

•Broker bersifat publik dan tanpa autentikasi. Siapa pun yang mengetahui nama topic dapat mengirim perintah mode atau sudut pintu, sehingga sistem belum aman untuk pengendalian pintu air yang sebenarnya.

•ESP32 hanya membatasi rentang sudut 0 sampai 90 dan tidak memverifikasi asal perintah.

•Data pengujian berasal dari simulasi, sedangkan sensor ultrasonik pada kondisi nyata dipengaruhi gelombang air, suhu udara, dan benda mengambang.

•Grafik hanya menyimpan 60 data terakhir pada memori browser dan hilang ketika halaman dimuat ulang, karena belum ada penyimpanan riwayat.

•Pesan MQTT memakai QoS 0 sehingga pengiriman tidak dijamin bila jaringan tidak stabil.

•Peringatan hanya muncul pada dashboard dan perangkat lokal, belum ada notifikasi ke ponsel atau pesan singkat.

•Fungsi pulseIn() menahan program hingga 30 ms pada setiap pembacaan, dan pada perangkat nyata jalur ECHO memerlukan pembagi tegangan.

BAB IV
PENUTUP

4.1 Kesimpulan

Berdasarkan pembahasan pada bab sebelumnya, dapat disimpulkan sebagai berikut.

1.Sistem peringatan dini banjir berhasil dirancang dengan mengintegrasikan sensor HC-SR04, ESP32, LCD I2C, LED, buzzer, dan servo pada Wokwi, serta dashboard web yang berkomunikasi melalui MQTT dan WebSocket menggunakan Paho.

2.ESP32 menghitung level air dari selisih tinggi sensor dan jarak terukur, menentukan status AMAN, WASPADA, atau BAHAYA dengan histeresis 5 cm, lalu menggerakkan pintu air ke 0°, 45°, atau 90° pada mode AUTO.

3.Komunikasi dua arah dicapai dengan lima topic: data dan status dari ESP32 ke dashboard, serta cmd/mode, cmd/pintu, dan cmd/mute dari dashboard ke ESP32.

4.Sistem sesuai untuk demonstrasi integrasi web dan IoT, tetapi belum cukup aman untuk pengendalian pintu air nyata karena memakai broker publik tanpa autentikasi.

4.2 Saran

1.Gunakan broker privat dengan autentikasi, TLS, dan pembatasan akses topic (ACL), serta verifikasi perintah di sisi ESP32.

2.Tambahkan penyimpanan riwayat ketinggian air pada basis data agar grafik tidak hilang saat halaman dimuat ulang.

3.Tambahkan notifikasi ke ponsel, misalnya melalui bot pesan, ketika status mencapai BAHAYA.

4.Gunakan sensor tahan air dan lakukan kalibrasi pada kondisi nyata, serta pasang pembagi tegangan pada jalur ECHO.

5.Gunakan QoS 1 untuk pesan perintah penting dan tambahkan sensor posisi untuk memastikan pintu air benar-benar mencapai sudut yang diminta.

DAFTAR PUSTAKA

ahocool-aisi555. (2025). boomgate: Workshop IOT Informatika UNESA 2025 [Repositori kode]. GitHub. https://github.com/ahocool-aisi555/boomgate Modul 6. (t.t.). MQTT melalui WebSocket: Memanfaatkan JavaScript untuk membuat aplikasi IoT - HTML [Materi perkuliahan]. modul6.pptx.

O'Leary, N. (t.t.). PubSubClient: A client library for MQTT messaging [Repositori kode]. GitHub. https://github.com/knolleary/pubsubclient Random Nerd Tutorials. (t.t.). ESP32 WebSocket Server: Display Sensor Readings. https://randomnerdtutorials.com/esp32-websocket-server-sensor/ Steve's Internet Guide. (t.t.). Using the JavaScript MQTT client with WebSockets. http://www.steves-internet-guide.com/using-javascript-mqtt-client-websockets/ Wokwi. (t.t.). Wokwi: World's most advanced ESP32 simulator. https://wokwi.com

LAMPIRAN

Link Video: https://drive.google.com/drive/folders/11fHlgYJsTspVbom2_wbqFp3pKXwhhOCE?usp=sharing

Tidak ada komentar:

Posting Komentar