#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
// Untuk Hardware Serial, gunakan Serial1 atau Serial2 pada ESP32.
// Serial1: RX (GPIO9), TX (GPIO10)
// Serial2: RX (GPIO16), TX (GPIO17)
// Pastikan pin ini tidak digunakan untuk hal lain.
HardwareSerial sim800l(2); // Menggunakan Hardware Serial '2' (Serial2)
// --- Pengaturan Dasar ---
// Variabel Global
Preferences preferences; // Objek untuk menyimpan dan memuat pengaturan dari NVS (Non-Volatile Storage)
WebServer server; // Inisialisasi WebServer tanpa port spesifik di sini
int web_port = 80; // Port default server web
// Pengaturan Klien WiFi (Default)
String client_ssid = "Dara@home";
String client_pass = "rejeki88";
// Variabel global baru untuk konfigurasi IP statis
String client_ip = ""; // Alamat IP statis
String client_gateway = ""; // Gateway statis
String client_subnet = ""; // Subnet Mask statis
String client_dns1 = ""; // Opsional: Server DNS Primer
String client_dns2 = ""; // Opsional: Server DNS Sekunder
// Pengaturan Access Point (Fallback) - Digunakan jika koneksi WiFi klien gagal
const char* ap_ssid = "PUTU@TECKNO-SIM800L";
const char* ap_pass = "rejeki88";
// Variabel global baru untuk nomor telepon penerima
String targetPhoneNumber1 = "";
String targetPhoneNumber2 = "";
String targetPhoneNumber3 = "";
// Variabel untuk menyimpan status saat ini
String lastNotification = "Belum ada"; // Menampilkan notifikasi terakhir yang diterima
String ipAddress = "N/A"; // Menampilkan alamat IP ESP32 saat ini
String testActionStatus = ""; // Variabel untuk menyimpan status aksi tes (SMS/Panggilan)
// Static variable untuk melacak nomor telepon yang akan dipanggil berikutnya dari notifikasi
static int nextCallIndex = 0;
// === Variabel Baru untuk Mengontrol SMS Saat Booting ===
// Akan diset false setelah setup selesai.
// Ini membantu mencegah SMS/panggilan terkirim saat ESP32 pertama kali booting.
bool isFirstBoot = true;
// --- PENGATURAN UNTUK SIM800L ---
// PIN untuk Hardware Serial 2 (Serial2): RX2 (GPIO16), TX2 (GPIO17)
// Pastikan Anda menghubungkan SIM800L TX ke RX2 (GPIO16) dan SIM800L RX ke TX2 (GPIO17)
// Anda dapat mengubah pin ini jika perlu saat inisialisasi HardwareSerial,
// tetapi GPIO16 dan GPIO17 adalah pin default untuk Serial2.
/**
* @brief Membaca respons dari modul SIM800L.
* @param timeout Waktu maksimum (ms) untuk menunggu respons.
* @return String respons dari modul.
*/
String readSIMResponse(unsigned long timeout = 2000) {
String response = "";
unsigned long start = millis();
while (millis() - start < timeout) {
while (sim800l.available()) {
char c = sim800l.read();
response += c;
}
}
return response;
}
/**
* @brief Mengirim perintah AT ke modul SIM800L dan membaca respons.
* @param command Perintah AT yang akan dikirim.
* @param timeout Waktu maksimum (ms) untuk menunggu respons.
* @return String respons dari modul.
*/
String sendATCommand(String command, unsigned long timeout = 2000) {
sim800l.println(command);
Serial.println("Mengirim AT: " + command);
String response = readSIMResponse(timeout);
Serial.println("Respons AT: " + response);
return response;
}
/**
* @brief Memeriksa apakah modul SIM800L merespons.
* @return true jika modul merespons OK, false jika tidak.
*/
bool checkSIM800L() {
Serial.println("Memeriksa koneksi SIM800L...");
String response = sendATCommand("AT", 3000); // Beri waktu lebih untuk respons pertama
if (response.indexOf("OK") != -1) {
Serial.println("SIM800L terhubung dan merespons.");
return true;
} else {
Serial.println("SIM800L tidak merespons atau ada masalah.");
return false;
}
}
/**
* @brief Mengirim SMS ke semua nomor telepon yang dikonfigurasi.
*
* @param message Pesan SMS yang akan dikirim.
*/
void sendSMS(String message) {
Serial.println("Mengirim SMS: " + message);
// Array nomor telepon target untuk iterasi yang lebih mudah
String phoneNumbers[] = {targetPhoneNumber1, targetPhoneNumber2, targetPhoneNumber3};
bool smsSent = false; // Flag untuk melacak apakah setidaknya satu SMS berhasil dikirim
for (int i = 0; i < 3; i++) {
if (phoneNumbers[i] != "") { // Hanya kirim jika nomor tidak kosong
Serial.println("Mengirim ke: " + phoneNumbers[i]);
sendATCommand("AT+CMGF=1", 1000); // Atur mode teks
sendATCommand("AT+CMGS=\"" + phoneNumbers[i] + "\"", 1000);
sim800l.print(message); // Isi pesan
delay(100);
sim800l.write(26); // Karakter CTRL+Z (ASCII 26) untuk mengirim SMS
String simResponse = readSIMResponse(5000); // Beri waktu modul untuk mengirim SMS
if (simResponse.indexOf("OK") != -1) {
Serial.println("\nSMS terkirim ke " + phoneNumbers[i]);
smsSent = true;
} else {
Serial.println("\nGagal mengirim SMS ke " + phoneNumbers[i]);
}
}
}
// Perbarui status aksi tes berdasarkan keberhasilan pengiriman SMS
if (smsSent) {
// Hanya perbarui testActionStatus jika ini adalah SMS tes,
// bukan SMS pra-panggilan dari notifikasi.
if (message.indexOf("Tes SMS") != -1) { // Periksa apakah ini pesan tes
testActionStatus = "SMS tes berhasil dikirim!";
}
} else {
if (message.indexOf("Tes SMS") != -1) { // Periksa apakah ini pesan tes
testActionStatus = "SMS tes gagal dikirim atau tidak ada nomor terdaftar.";
}
}
}
/**
* @brief Melakukan panggilan ke nomor telepon yang dikonfigurasi.
*
* @param phoneNumber Nomor telepon tujuan panggilan.
*/
void makeCall(String phoneNumber) {
if (phoneNumber == "") {
Serial.println("Tidak ada nomor telepon untuk melakukan panggilan.");
testActionStatus = "Gagal: Tidak ada nomor telepon terdaftar.";
return;
}
Serial.println("Melakukan panggilan ke: " + phoneNumber);
String simResponse = sendATCommand("ATD" + phoneNumber + ";", 10000); // Perintah AT untuk melakukan panggilan
if (simResponse.indexOf("OK") != -1) {
Serial.println("\nPanggilan berhasil dimulai ke " + phoneNumber);
testActionStatus = "Panggilan berhasil dimulai ke " + phoneNumber + ".";
} else {
Serial.println("\nGagal memulai panggilan ke " + phoneNumber);
testActionStatus = "Panggilan gagal dimulai ke " + phoneNumber + ".";
}
// Anda mungkin ingin menambahkan perintah ATH untuk mengakhiri panggilan setelah beberapa waktu
delay(15000); // Biarkan panggilan berdering sebentar (15 detik)
sendATCommand("ATH", 1000); // Akhiri panggilan
}
/**
* @brief Melakukan panggilan berulang ke semua nomor terdaftar secara bergantian.
* @param callAttempts Jumlah upaya panggilan per nomor.
* @param numbers Array string nomor telepon.
*/
void makeRepeatedCalls(int callAttempts, String numbers[]) {
Serial.println("Memulai proses panggilan berulang...");
for (int attempt = 0; attempt < callAttempts; attempt++) {
Serial.println("Upaya Panggilan ke-" + String(attempt + 1));
for (int i = 0; i < 3; i++) { // Iterasi melalui setiap nomor
if (numbers[i] != "") { // Hanya panggil jika nomor tidak kosong
makeCall(numbers[i]);
// Tambahkan jeda antara panggilan ke nomor yang berbeda atau upaya yang berbeda
delay(5000); // Jeda 5 detik antar panggilan ke nomor/upaya berbeda
}
}
}
Serial.println("Proses panggilan berulang selesai.");
}
// Fungsi Web Server
// handleRoot()
// Menangani permintaan ke jalur root ("/"). Ini membangun dan mengirim halaman HTML untuk konfigurasi perangkat.
void handleRoot() {
// Bangun halaman HTML untuk konfigurasi
String html = "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width, initial-scale=1.0'>"
"<title>ESP32 Receiver Config</title>"
"<style>"
"body{font-family:Arial,sans-serif;background:#f2f2f2;margin:0;padding:15px;}"
".container{max-width:800px;margin:auto;background:white;padding:20px;border-radius:10px;box-shadow:0 2px 10px rgba(0,0,0,0.1);}"
"h1,h2{color:#333;}"
"form label{display:block;margin-top:10px;font-weight:bold;}"
"form input[type='text'],form input[type='password']{width:calc(100% - 20px);padding:8px;margin-top:5px;border:1px solid #ccc;border-radius:5px;}"
"form input[type='submit'], .test-button{background-color:#007bff;color:white;border:none;padding:12px 20px;border-radius:5px;cursor:pointer;font-size:16px;margin-top:20px;display:inline-block;margin-right:10px;}"
".test-button.call{background-color:#28a745;}"
".test-button.sms{background-color:#ffc107; color:black;}"
".status{background:#e9f5ff;border-left:5px solid #007bff;padding:10px;margin-top:20px;}"
".grid{display:grid;grid-template-columns:repeat(auto-fit, minmax(250px, 1fr));grid-gap:10px;}"
".message {background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; padding: 10px; border-radius: 5px; margin-bottom: 15px;}"
".error-message {background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; padding: 10px; border-radius: 5px; margin-bottom: 15px;}"
"</style></head><body>"
"<div class='container'>"
"<h1>Konfigurasi Penerima Notifikasi</h1>";
// Tampilkan pesan status aksi tes (SMS/Panggilan) jika ada
if (testActionStatus != "") {
if (testActionStatus.startsWith("Gagal")) {
html += "<div class='error-message'>" + testActionStatus + "</div>";
} else {
html += "<div class='message'>" + testActionStatus + "</div>";
}
testActionStatus = ""; // Hapus status setelah ditampilkan untuk mencegah tampilan berulang
}
html += "<div class='status'>"
"<h2>Status Saat Ini</h2>"
"<strong>Status WiFi:</strong> " + (WiFi.status() == WL_CONNECTED ? "Terhubung ke " + client_ssid : "Mode Access Point Aktif") + "<br>"
"<strong>Alamat IP:</strong> " + ipAddress + "<br>"
"<strong>Port Web:</strong> " + String(web_port) + "<br>" // Tampilkan port web saat ini
"<strong>Notifikasi Terakhir:</strong> " + lastNotification +
"</div>"
"<form action='/save' method='post'>"
"<h2>Pengaturan WiFi Client</h2>"
"<label for='ssid'>Nama WiFi (SSID):</label>"
"<input type='text' name='ssid' value='" + client_ssid + "'>"
"<label for='password'>Password WiFi:</label>"
"<input type='password' name='password' value='" + client_pass + "'>"
"<h2>Pengaturan Port Web</h2>" // Bagian baru untuk port web
"<label for='port'>Port Web (misalnya 80, 8080):</label>"
"<input type='text' name='port' value='" + String(web_port) + "'>"
"<h2>Pengaturan IP Statis (Kosongkan untuk DHCP)</h2>" // Bagian untuk IP statis
"<label for='ip'>Alamat IP:</label>"
"<input type='text' name='ip' value='" + client_ip + "'>"
"<label for='gateway'>Gateway:</label>"
"<input type='text' name='gateway' value='" + client_gateway + "'>"
"<label for='subnet'>Subnet Mask:</label>"
"<input type='text' name='subnet' value='" + client_subnet + "'>"
"<label for='dns1'>DNS Primer (Opsional):</label>"
"<input type='text' name='dns1' value='" + client_dns1 + "'>"
"<label for='dns2'>DNS Sekunder (Opsional):</label>"
"<input type='text' name='dns2' value='" + client_dns2 + "'>"
"<h2>Nomor Telepon Penerima Notifikasi</h2>" // Bagian untuk nomor telepon
"<label for='phone1'>Nomor HP 1:</label>"
"<input type='text' name='phone1' value='" + targetPhoneNumber1 + "'>"
"<label for='phone2'>Nomor HP 2:</label>"
"<input type='text' name='phone2' value='" + targetPhoneNumber2 + "'>"
"<label for='phone3'>Nomor HP 3:</label>"
"<input type='text' name='phone3' value='" + targetPhoneNumber3 + "'>"
"<input type='submit' value='Simpan Pengaturan & Restart'>"
"</form>"
"<h2>Tes SIM800L</h2>"
"<a href='/testCall' class='test-button call'>Tes Panggilan ke HP1</a>"
"<a href='/testSms' class='test-button sms'>Tes SMS ke HP1</a>"
"</div></body></html>";
// Kirim respons HTML ke klien
server.send(200, "text/html", html);
}
// handleSave()
// Menangani permintaan untuk menyimpan konfigurasi yang dikirim melalui formulir POST. Ini menyimpan pengaturan ke NVS dan me-restart ESP32.
void handleSave() {
// Mulai preferensi dalam mode baca-tulis
preferences.begin("config", false);
// Simpan kredensial WiFi
client_ssid = server.arg("ssid");
client_pass = server.arg("password");
preferences.putString("ssid", client_ssid);
preferences.putString("password", client_pass);
// Simpan pengaturan port web
web_port = server.arg("port").toInt();
if (web_port == 0) web_port = 80; // Pastikan port valid, default ke 80 jika 0 atau tidak valid
preferences.putInt("web_port", web_port);
// Simpan pengaturan IP statis
client_ip = server.arg("ip");
client_gateway = server.arg("gateway");
client_subnet = server.arg("subnet");
client_dns1 = server.arg("dns1");
client_dns2 = server.arg("dns2");
preferences.putString("ip", client_ip);
preferences.putString("gateway", client_gateway);
preferences.putString("subnet", client_subnet);
preferences.putString("dns1", client_dns1);
preferences.putString("dns2", client_dns2);
// Simpan nomor telepon penerima
targetPhoneNumber1 = server.arg("phone1");
targetPhoneNumber2 = server.arg("phone2");
targetPhoneNumber3 = server.arg("phone3");
preferences.putString("phone1", targetPhoneNumber1);
preferences.putString("phone2", targetPhoneNumber2);
preferences.putString("phone3", targetPhoneNumber3);
// Akhiri preferensi, melakukan perubahan ke NVS
preferences.end();
// Respons HTML setelah menyimpan
String html = "<!DOCTYPE html><html><head><title>Saved</title><style>"
"body{font-family:Arial,sans-serif;text-align:center;padding-top:50px;}"
"h1{color:#4CAF50;}"
"</style></head><body>"
"<h1>Pengaturan Disimpan!</h1>"
"<p>Perangkat akan restart dalam 2 detik...</p>"
"</body></html>";
server.send(200, "text/html", html);
delay(2000); // Tunggu 2 detik sebelum memulai ulang
ESP.restart(); // Mulai ulang ESP32 untuk menerapkan pengaturan baru
}
// handleNotif()
// Menangani permintaan notifikasi yang masuk ke jalur "/notif". Menerima kode apa saja dan memicu aksi (SMS & panggilan bergantian).
void handleNotif() {
// Periksa apakah argumen 'src' ada dalam permintaan GET
if (server.hasArg("src")) {
String receivedCode = server.arg("src"); // Dapatkan kode dari parameter 'src'
Serial.println("Notifikasi diterima: " + receivedCode);
lastNotification = receivedCode; // Perbarui status notifikasi terakhir di UI
// === Perubahan di sini: Hanya kirim SMS/Panggilan jika bukan boot pertama kali ===
// Ini bertujuan mencegah notifikasi terkirim otomatis saat ESP32 dinyalakan.
if (isFirstBoot) {
Serial.println("Mengabaikan notifikasi saat boot pertama kali.");
server.send(200, "text/plain", "OK. Notifikasi diterima (ignored during boot).");
return; // Keluar dari fungsi jika ini adalah boot pertama
}
// Setiap kode yang diterima akan langsung memicu aksi SMS dan panggilan
Serial.println("Kode diterima. Memicu aksi SMS kemudian panggilan...");
server.send(200, "text/plain", "OK. Notifikasi diterima dan aksi dipicu.");
String phoneNumbers[3] = {targetPhoneNumber1, targetPhoneNumber2, targetPhoneNumber3};
// === PERUBAHAN DI SINI: Kalimat pesan SMS yang baru ===
String notificationMessage = "INI PESAN PENTING DARI SERVER GLOSINDO ADA PERINGATAN : " + receivedCode + ", HARAP SEGERA CEK SERVER PASTIKAN SEMUA BERJALAN DENGAN BAIK";
// 1. Kirim SMS notifikasi ke SEMUA nomor terdaftar
Serial.println("Mengirim SMS notifikasi pra-panggilan ke semua nomor...");
sendSMS(notificationMessage);
// 2. Jeda 15 detik
Serial.println("Jeda 15 detik sebelum panggilan...");
delay(15000); // 15 detik
// 3. Lakukan panggilan berulang secara bergantian ke setiap nomor
makeRepeatedCalls(3, phoneNumbers); // Panggil setiap nomor hingga 3 kali
Serial.println("Aksi notifikasi selesai.");
// ------------------------------------
} else {
// Jika parameter 'src' hilang
server.send(400, "text/plain", "Permintaan Buruk. Parameter 'src' hilang.");
}
}
// handleTestCall() dan handleTestSms()
// Menangani permintaan tes panggilan dan SMS.
void handleTestCall() {
Serial.println("Permintaan tes panggilan diterima.");
// Pastikan flag diset false jika tes manual dilakukan saat boot pertama
isFirstBoot = false;
makeCall(targetPhoneNumber1); // Lakukan panggilan ke nomor HP pertama
// Setelah aksi, arahkan kembali ke halaman root untuk memperbarui status
server.sendHeader("Location", "/");
server.send(302, "text/plain", "Redirecting to home...");
}
void handleTestSms() {
Serial.println("Permintaan tes SMS diterima.");
// Pastikan flag diset false jika tes manual dilakukan saat boot pertama
isFirstBoot = false;
sendSMS("Tes SMS dari ESP32!"); // Kirim SMS tes
// Setelah aksi, arahkan kembali ke halaman root untuk memperbarui status
server.sendHeader("Location", "/");
server.send(302, "text/plain", "Redirecting to home...");
}
// Fungsi Setup dan Loop
// loadSettings()
// Memuat pengaturan yang disimpan dari NVS (Non-Volatile Storage).
void loadSettings() {
preferences.begin("config", true); // Buka NVS dalam mode hanya-baca
client_ssid = preferences.getString("ssid", "Dara@home"); // Muat SSID, dengan nilai default
client_pass = preferences.getString("password", "rejeki88"); // Muat password, dengan nilai default
web_port = preferences.getInt("web_port", 80); // Muat port web, default ke 80
// Muat pengaturan IP statis
client_ip = preferences.getString("ip", "");
client_gateway = preferences.getString("gateway", "");
client_subnet = preferences.getString("subnet", "");
client_dns1 = preferences.getString("dns1", "");
client_dns2 = preferences.getString("dns2", "");
// Muat nomor telepon penerima
targetPhoneNumber1 = preferences.getString("phone1", "");
targetPhoneNumber2 = preferences.getString("phone2", "");
targetPhoneNumber3 = preferences.getString("phone3", "");
preferences.end(); // Tutup preferensi
}
// setupWiFi()
// Mengatur koneksi WiFi, mencoba mode klien terlebih dahulu, kemudian beralih ke Access Point jika koneksi klien gagal.
void setupWiFi() {
WiFi.mode(WIFI_STA); // Atur WiFi ke mode Station (klien)
// Periksa apakah pengaturan IP statis disediakan
if (client_ip != "" && client_gateway != "" && client_subnet != "") {
IPAddress ip, gateway, subnet, dns1, dns2;
// Coba konversi string IP ke objek IPAddress
if (ip.fromString(client_ip) && gateway.fromString(client_gateway) && subnet.fromString(client_subnet)) {
if (client_dns1 != "" && dns1.fromString(client_dns1)) {
if (client_dns2 != "" && dns2.fromString(client_dns2)) {
WiFi.config(ip, gateway, subnet, dns1, dns2); // Gunakan IP statis dengan DNS primer dan sekunder
} else {
WiFi.config(ip, gateway, subnet, dns1); // Gunakan IP statis dengan hanya DNS primer
}
} else {
WiFi.config(ip, gateway, subnet); // Gunakan hanya IP, Gateway, Subnet
}
Serial.println("Menggunakan IP Statis.");
} else {
Serial.println("IP Statis tidak valid, menggunakan DHCP.");
// Hapus pengaturan IP statis yang tidak valid untuk kembali ke DHCP
client_ip = "";
client_gateway = "";
client_subnet = "";
client_dns1 = "";
client_dns2 = "";
}
} else {
Serial.println("IP Statis tidak dikonfigurasi, menggunakan DHCP.");
}
WiFi.begin(client_ssid.c_str(), client_pass.c_str()); // Coba sambungkan ke WiFi yang disimpan
Serial.print("Menyambungkan ke " + client_ssid);
int attempts = 0;
// Tunggu koneksi atau batas waktu (10 detik = 20 * 500ms)
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500); // Tunggu 0.5 detik
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nTersambung!");
ipAddress = WiFi.localIP().toString(); // Dapatkan alamat IP yang ditetapkan
Serial.println("Alamat IP: " + ipAddress);
} else {
Serial.println("\nKoneksi Gagal. Mengaktifkan Mode Access Point.");
WiFi.mode(WIFI_AP); // Beralih ke mode Access Point
WiFi.softAP(ap_ssid, ap_pass); // Mulai Access Point
ipAddress = WiFi.softAPIP().toString(); // Dapatkan alamat IP AP
Serial.println("SSID AP: " + String(ap_ssid));
Serial.println("Alamat IP AP: " + ipAddress);
}
}
// setup()
// Fungsi setup Arduino (berjalan sekali saat power-on atau reset).
// Menginisialisasi serial, SIM800L, memuat pengaturan, mengatur WiFi,
// dan memulai server web.
void setup() {
Serial.begin(115200); // Inisialisasi komunikasi serial untuk debugging
Serial.println("\nMemulai ESP32 Receiver...");
// Setel flag isFirstBoot menjadi true saat setup dimulai
isFirstBoot = true;
Serial.println("[DEBUG] isFirstBoot di SET UP: TRUE"); // Debugging
// Inisialisasi SIM800L dengan Hardware Serial 2 (RX2=GPIO16, TX2=GPIO17)
// Baud rate standar untuk SIM800L adalah 9600.
sim800l.begin(9600, SERIAL_8N1, 16, 17);
Serial.println("SIM800L (Hardware Serial) diinisialisasi.");
// Coba cek koneksi dengan SIM800L
if (!checkSIM800L()) {
Serial.println("Peringatan: Modul SIM800L mungkin tidak terhubung atau tidak berfungsi dengan benar.");
// Lanjutkan startup, tetapi fungsi SMS/Panggilan mungkin gagal
}
loadSettings(); // Muat pengaturan yang disimpan dari NVS
setupWiFi(); // Pengaturan koneksi WiFi (klien atau AP)
// Daftarkan handler server web untuk URL yang berbeda
server.on("/", HTTP_GET, handleRoot); // Jalur root untuk halaman konfigurasi
server.on("/save", HTTP_POST, handleSave); // Simpan konfigurasi dari formulir
server.on("/notif", HTTP_GET, handleNotif); // Endpoint notifikasi untuk menerima kode
server.on("/testCall", HTTP_GET, handleTestCall); // Handler untuk tes panggilan SIM800L
server.on("/testSms", HTTP_GET, handleTestSms); // Handler untuk tes SMS SIM800L
server.begin(web_port); // Mulai server web pada port yang dikonfigurasi
Serial.println("Server Web dimulai pada port: " + String(web_port));
// Setelah semua setup selesai dan server sudah siap, setel isFirstBoot menjadi false
isFirstBoot = false;
Serial.println("[DEBUG] isFirstBoot di AKHIR SETUP: FALSE"); // Debugging
}
// loop()
// Fungsi loop Arduino (berjalan berulang kali).
// Secara terus-menerus menangani permintaan web yang masuk.
void loop() {
server.handleClient(); // Harus dipanggil secara terus-menerus untuk menangani permintaan web yang masuk
}
No comments:
Post a Comment